Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throw compile errors when unexpected proc_macros attributes are provided #394

Open
billy1624 opened this issue Dec 21, 2021 · 5 comments · May be fixed by #1412 or #1560
Open

Throw compile errors when unexpected proc_macros attributes are provided #394

billy1624 opened this issue Dec 21, 2021 · 5 comments · May be fixed by #1412 or #1560

Comments

@billy1624
Copy link
Member

Related to #391

@billy1624 billy1624 changed the title Produce compile errors when unexpected proc_macros attributes are provided Throw compile errors when unexpected proc_macros attributes are provided Dec 21, 2021
@billy1624 billy1624 moved this to Next Up in SeaQL Dev Tracker Jul 12, 2022
@billy1624
Copy link
Member Author

Create a whitelist for all supported #[sea_orm(xxx)] proc_macros attributes. Thrown compile errors when it parse unknown attributes.

@billy1624 billy1624 moved this from Next Up to Triage in SeaQL Dev Tracker Jan 13, 2023
@Diwakar-Gupta
Copy link
Contributor

I was researching this issue and I believe there should be something like

#[proc_macro_attribute]
pub fn sea_orm(attr: TokenStream, item: TokenStream) -> TokenStream {

where all the attributes would pass from like column_name and table_name but I was unable to find this code in repo.

Is it because we are using proc-macro2 and serde libraries?
Please guide me.

Thanks.

@billy1624
Copy link
Member Author

Hey @Diwakar-Gupta, thanks for the investigation!!

First, every procedural macros is self contained in a .rs file under the sea-orm-macros/src/derives folder.

For example, DeriveActiveEnum is defined in a file below:

struct ActiveEnum {
ident: syn::Ident,
enum_name: String,
rs_type: TokenStream,
db_type: TokenStream,
is_string: bool,
variants: Vec<ActiveEnumVariant>,
}
struct ActiveEnumVariant {
ident: syn::Ident,
string_value: Option<LitStr>,
num_value: Option<LitInt>,
}
impl ActiveEnum {
fn new(input: syn::DeriveInput) -> Result<Self, Error> {
let ident_span = input.ident.span();
let ident = input.ident;
let mut enum_name = ident.to_string().to_camel_case();
let mut rs_type = Err(Error::TT(quote_spanned! {
ident_span => compile_error!("Missing macro attribute `rs_type`");
}));
let mut db_type = Err(Error::TT(quote_spanned! {
ident_span => compile_error!("Missing macro attribute `db_type`");
}));
for attr in input.attrs.iter() {
if let Some(ident) = attr.path.get_ident() {
if ident != "sea_orm" {
continue;
}
} else {
continue;
}
if let Ok(list) = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated) {
for meta in list.iter() {
if let Meta::NameValue(nv) = meta {
if let Some(name) = nv.path.get_ident() {
if name == "rs_type" {
if let Lit::Str(litstr) = &nv.lit {
rs_type = syn::parse_str::<TokenStream>(&litstr.value())
.map_err(Error::Syn);
}
} else if name == "db_type" {
if let Lit::Str(litstr) = &nv.lit {
let s = litstr.value();
match s.as_ref() {
"Enum" => {
db_type = Ok(quote! {
Enum {
name: Self::name(),
variants: Self::iden_values(),
}
})
}
_ => {
db_type = syn::parse_str::<TokenStream>(&s)
.map_err(Error::Syn);
}
}
}
} else if name == "enum_name" {
if let Lit::Str(litstr) = &nv.lit {
enum_name = litstr.value();
}
}
}
}
}
}
}
let variant_vec = match input.data {
syn::Data::Enum(syn::DataEnum { variants, .. }) => variants,
_ => return Err(Error::InputNotEnum),
};
let mut is_string = false;
let mut is_int = false;
let mut variants = Vec::new();
for variant in variant_vec {
let variant_span = variant.ident.span();
let mut string_value = None;
let mut num_value = None;
for attr in variant.attrs.iter() {
if let Some(ident) = attr.path.get_ident() {
if ident != "sea_orm" {
continue;
}
} else {
continue;
}
if let Ok(list) = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)
{
for meta in list {
if let Meta::NameValue(nv) = meta {
if let Some(name) = nv.path.get_ident() {
if name == "string_value" {
if let Lit::Str(lit) = nv.lit {
is_string = true;
string_value = Some(lit);
}
} else if name == "num_value" {
if let Lit::Int(lit) = nv.lit {
is_int = true;
num_value = Some(lit);
}
}
}
}
}
}
}
if is_string && is_int {
return Err(Error::TT(quote_spanned! {
ident_span => compile_error!("All enum variants should specify the same `*_value` macro attribute, either `string_value` or `num_value` but not both");
}));
}
if string_value.is_none() && num_value.is_none() {
match variant.discriminant {
Some((_, Expr::Lit(exprlit))) => {
if let Lit::Int(litint) = exprlit.lit {
is_int = true;
num_value = Some(litint);
} else {
return Err(Error::TT(quote_spanned! {
variant_span => compile_error!("Enum variant discriminant is not an integer");
}));
}
}
//rust doesn't provide negative variants in enums as a single LitInt, this workarounds that
Some((_, Expr::Unary(exprnlit))) => {
if let UnOp::Neg(_) = exprnlit.op {
if let Expr::Lit(exprlit) = *exprnlit.expr {
if let Lit::Int(litint) = exprlit.lit {
let negative_token = quote! { -#litint };
let litint = parse(negative_token.into()).unwrap();
is_int = true;
num_value = Some(litint);
}
}
} else {
return Err(Error::TT(quote_spanned! {
variant_span => compile_error!("Only - token is supported in enum variants, not ! and *");
}));
}
}
_ => {
return Err(Error::TT(quote_spanned! {
variant_span => compile_error!("Missing macro attribute, either `string_value` or `num_value` should be specified or specify repr[X] and have a value for every entry");
}));
}
}
}
variants.push(ActiveEnumVariant {
ident: variant.ident,
string_value,
num_value,
});
}
Ok(ActiveEnum {
ident,
enum_name,
rs_type: rs_type?,
db_type: db_type?,
is_string,
variants,
})
}

Each procedural macros starts by parsing the inputting TokenStream manually. Yes, it's done manually and it's prone to mistake. And some old procedural macros don't even has a explicite parsing stage.

Ideally we want each procedural macro should be testable. With that in mind we better implement it in two stages:

  1. Parsing: take the input TokenStream into a struct that contains all necessary data for the next code generating stage.
  2. Code Generating: take a data struct from the parsing stage and from that we can use quote! macro to generate necessary implementations.

Then, we can test the parsing and code generating parts separately and thoroughly.

We already has the code generating inplace, we just need to rewrite all of them. I think https://github.com/TedDriggs/darling could helps us do the parsing.

Does that make sense to you?

@Diwakar-Gupta
Copy link
Contributor

Diwakar-Gupta commented Jan 19, 2023

Thanks for the explanation @billy1624

What I understood is each derive/* receives DeriveInput from which it extracts necessary attributes and values in enum/struct called Parsing then it is passed for code addon using quote! called code generation.

This parsing step can be replaced with use of darling as you mentioned.

We wanted to test both parsing and code generation step's separately for each derive/*.

Create a whitelist for all supported #[sea_orm(xxx)] proc_macros attributes. Thrown compile errors when it parse unknown attributes.

and this too right.

@billy1624
Copy link
Member Author

Sounds like we have a plan now :)

@Diwakar-Gupta Diwakar-Gupta linked a pull request Jan 21, 2023 that will close this issue
2 tasks
@billy1624 billy1624 linked a pull request Mar 21, 2023 that will close this issue
This was linked to pull requests Jun 30, 2023
@billy1624 billy1624 modified the milestone: 0.12.x Jun 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Triage
Development

Successfully merging a pull request may close this issue.

2 participants