Skip to content

Commit

Permalink
Initial pile of hacks
Browse files Browse the repository at this point in the history
  • Loading branch information
davidpdrsn committed Nov 28, 2019
0 parents commit 5c6428e
Show file tree
Hide file tree
Showing 7 changed files with 387 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Change Log

All user visible changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/), as described
for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/text/1105-api-evolution.md)

## Unreleased

None.

### Breaking changes

None.

## [0.1.0]

- Support adding extensions to the ["never type"](https://doc.rust-lang.org/std/primitive.never.html).

### Breaking changes

- Simplify names of traits generates for complex types.

## [0.0.2]

- Move "trybuild" to dev-dependency.

## [0.0.1]

Initial release.
26 changes: 26 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "bae"
version = "0.0.1"
authors = ["David Pedersen <[email protected]>"]
edition = "2018"
description = "A Rust proc-macro attribute parser"
homepage = "https://github.com/davidpdrsn/bae"
documentation = "https://docs.rs/bae"
keywords = ["proc-macro", "attr", "attribute"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/davidpdrsn/bae.git"

[dependencies]
syn = { version = "1.0.7", features = ["full", "extra-traits"] }
quote = "1.0.2"
proc-macro2 = "1.0.6"
proc-macro-error = "0.3.4"
heck = "0.3.1"

[dev_dependencies]
trybuild = "1.0.17"

[lib]
proc-macro = true
path = "src/lib.rs"
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `bae`

Coming soon.
1 change: 1 addition & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
merge_imports = true
279 changes: 279 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
// #![doc(html_root_url = "https://docs.rs/bae/0.0.1")]
#![allow(clippy::let_and_return)]
#![deny(
unused_variables,
mutable_borrow_reservation_conflict,
dead_code,
unused_must_use,
unused_imports
)]

extern crate proc_macro;

use heck::SnakeCase;
use proc_macro2::TokenStream;
use proc_macro_error::*;
use quote::*;
use syn::{spanned::Spanned, *};

#[proc_macro_derive(FromAttributes, attributes())]
#[proc_macro_error]
pub fn from_attributes(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let item = parse_macro_input!(input as ItemStruct);
FromAttributes::new(item).expand().into()
}

#[derive(Debug)]
struct FromAttributes {
item: ItemStruct,
tokens: TokenStream,
}

impl FromAttributes {
fn new(item: ItemStruct) -> Self {
Self {
item,
tokens: TokenStream::new(),
}
}

fn expand(mut self) -> TokenStream {
self.expand_from_attributes_method();
self.expand_parse_impl();

if std::env::var("BAE_DEBUG").is_ok() {
eprintln!("{}", self.tokens);
}

self.tokens
}

fn struct_name(&self) -> &Ident {
&self.item.ident
}

fn attr_name(&self) -> LitStr {
let struct_name = self.struct_name();
let name = struct_name.to_string().to_snake_case();
LitStr::new(&name, struct_name.span())
}

fn expand_from_attributes_method(&mut self) {
let struct_name = self.struct_name();
let attr_name = self.attr_name();

let code = quote! {
impl #struct_name {
pub fn from_attributes(attrs: &[syn::Attribute]) -> syn::Result<Self> {
use syn::spanned::Spanned;

for attr in attrs {
match attr.path.get_ident() {
Some(ident) if ident == #attr_name => {
return syn::parse2::<Self>(attr.tokens.clone());
}
// Ignore other attributes
_ => {},
}
}

if attrs.is_empty() {
Err(syn::Error::new(
proc_macro2::Span::call_site(),
&format!("missing attribute `#[{}]`", #attr_name),
))
} else {
let full_span = attrs
.iter()
.fold(attrs[0].span(), |acc, attr| acc.join(attr.span()).unwrap());
Err(syn::Error::new(full_span, &format!("missing attribute `#[{}]`", #attr_name)))
}
}
}
};
self.tokens.extend(code);
}

fn expand_parse_impl(&mut self) {
let struct_name = self.struct_name();
let attr_name = self.attr_name();

let variable_declarations = self.item.fields.iter().map(|field| {
let name = &field.ident;
quote! { let mut #name = std::option::Option::None; }
});

let match_arms = self.item.fields.iter().map(|field| {
let field_name = get_field_name(field);
let pattern = LitStr::new(&field_name.to_string(), field.span());

if field_is_switch(field) {
quote! {
#pattern => {
#field_name = std::option::Option::Some(());
}
}
} else {
quote! {
#pattern => {
content.parse::<syn::Token![=]>()?;
#field_name = std::option::Option::Some(content.parse()?);
}
}
}
});

let unwrap_mandatory_fields = self
.item
.fields
.iter()
.filter(|field| !field_is_optional(field))
.map(|field| {
let field_name = get_field_name(field);
let arg_name = LitStr::new(&field_name.to_string(), field.span());

quote! {
let #field_name = if let std::option::Option::Some(#field_name) = #field_name {
#field_name
} else {
return syn::Result::Err(
input.error(
&format!("`#[{}]` is missing `{}` argument", #attr_name, #arg_name),
)
);
};
}
});

let set_fields = self.item.fields.iter().map(|field| {
let field_name = get_field_name(field);
quote! { #field_name, }
});

let mut supported_args = self
.item
.fields
.iter()
.map(|field| get_field_name(field))
.map(|field_name| format!("`{}`", field_name))
.collect::<Vec<_>>();
supported_args.sort_unstable();
let supported_args = supported_args.join(", ");

let code = quote! {
impl syn::parse::Parse for #struct_name {
#[allow(unreachable_code, unused_imports, unused_variables)]
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
#(#variable_declarations)*

let content;
syn::parenthesized!(content in input);

while !content.is_empty() {
let bae_attr_ident = content.parse::<syn::Ident>()?;

match &*bae_attr_ident.to_string() {
#(#match_arms)*
other => {
return syn::Result::Err(
syn::Error::new(
bae_attr_ident.span(),
&format!(
"`#[{}]` got unknown `{}` argument. Supported arguments are {}",
#attr_name,
other,
#supported_args,
),
)
);
}
}

content.parse::<syn::Token![,]>().ok();
}

#(#unwrap_mandatory_fields)*

syn::Result::Ok(Self { #(#set_fields)* })
}
}
};
self.tokens.extend(code);
}
}

fn get_field_name(field: &Field) -> &Ident {
field
.ident
.as_ref()
.unwrap_or_else(|| abort!(field.span(), "Field without a name"))
}

fn field_is_optional(field: &Field) -> bool {
let type_path = if let Type::Path(type_path) = &field.ty {
type_path
} else {
return false;
};

let ident = &type_path
.path
.segments
.last()
.unwrap_or_else(|| abort!(field.span(), "Empty type path"))
.ident;

ident == "Option"
}

fn field_is_switch(field: &Field) -> bool {
let unit_type = syn::parse_str::<Type>("()").unwrap();
inner_type(&field.ty) == Some(&unit_type)
}

fn inner_type(ty: &Type) -> Option<&Type> {
let type_path = if let Type::Path(type_path) = ty {
type_path
} else {
return None;
};

let ty_args = &type_path
.path
.segments
.last()
.unwrap_or_else(|| abort!(ty.span(), "Empty type path"))
.arguments;

let ty_args = if let PathArguments::AngleBracketed(ty_args) = ty_args {
ty_args
} else {
return None;
};

let generic_arg = &ty_args
.args
.last()
.unwrap_or_else(|| abort!(ty_args.span(), "Empty generic argument"));

let ty = if let GenericArgument::Type(ty) = generic_arg {
ty
} else {
return None;
};

Some(ty)
}

#[cfg(test)]
mod test {
#[allow(unused_imports)]
use super::*;

#[test]
fn test_ui() {
let t = trybuild::TestCases::new();
t.pass("tests/compile_pass/*.rs");
t.compile_fail("tests/compile_fail/*.rs");
}
}
46 changes: 46 additions & 0 deletions tests/compile_pass/hello_world.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use bae::FromAttributes;

#[derive(Debug, Eq, PartialEq, FromAttributes)]
pub struct MyAttr {
mandatory: syn::Type,
optional_missing: Option<syn::Type>,
optional_given: Option<syn::Type>,
switch: Option<()>,
ident: syn::Ident,
}

fn main() {
use quote::*;
use syn::*;

let code = quote! {
#[other_random_attr]
#[my_attr(
switch,
ident = foo,
mandatory = SomeType,
optional_given = OtherType,
)]
struct Foo;
};

let item_struct = syn::parse2::<ItemStruct>(code).unwrap();
let attrs = &item_struct.attrs;
let my_attr = MyAttr::from_attributes(&attrs).unwrap();

assert_eq!(
my_attr.mandatory,
syn::parse_str::<Type>("SomeType").unwrap()
);

assert_eq!(my_attr.optional_missing, None,);

assert_eq!(
my_attr.optional_given,
Some(syn::parse_str::<Type>("OtherType").unwrap())
);

assert_eq!(my_attr.ident, syn::parse_str::<Ident>("foo").unwrap());

assert_eq!(my_attr.switch.is_some(), true);
}

0 comments on commit 5c6428e

Please sign in to comment.