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

parse field names in the #[error(...)] attr #14

Open
onkoe opened this issue Jul 10, 2024 · 3 comments
Open

parse field names in the #[error(...)] attr #14

onkoe opened this issue Jul 10, 2024 · 3 comments

Comments

@onkoe
Copy link
Owner

onkoe commented Jul 10, 2024

users should be able to reference variant fields in their error messages

@onkoe
Copy link
Owner Author

onkoe commented Jul 10, 2024

note: after an hour of trying to implement this, i realized that you don't need to touch the attrs at all, and that self.field on an enum variant is completely invalid syntax lmao

nonetheless, i learned a lot, so here's what i bolted on to display.rs trying to get it to work

use proc_macro2::{Span as Span2, TokenStream as TokenStream2};
use quote::quote;
use syn::{punctuated::Punctuated, token::Comma, Ident, LitStr, Meta, MetaList, Token, Variant};

use crate::util::{create_path, variant::make_match_head};

/**
To implement `Display`, we need to parse the given error message for each
variant.

However, there needs to be one error attribute per - not more, not less.

I've made some tests below verifying this assumption.

First, having no `#[error]` attribute should fail:
```compile_fail
use macros::Error;
use std::error::Error;

#[derive(Debug, Error)]
#[allow(unused)]
enum MyError {
    VariantOne,
}
\```

Also, you can't have too many of them, either:

\```compile_fail
use macros::Error;
use std::error::Error;

#[derive(Debug, Error)]
#[allow(unused)]
enum MyError {
    #[error("first attr")]
    #[error("second attr")]
    VariantOne,
}
\``` */
pub fn fmt(
    span: Span2,
    variants: &Punctuated<Variant, Comma>,
    enum_name: &Ident,
) -> syn::Result<TokenStream2> {
    // just an attribute that looks like `#[error(...)]`.
    let error_attr = create_path(span, &["error"]);

    // all the lines of `match`
    let mut vec = vec![];

    // make sure each variant has the error attribute.
    // then, grab each one for use in the impl
    for v in variants {
        let mut has_error_attribute = false;

        for attr in &v.attrs {
            if attr.meta.path() == &error_attr {
                // TODO: maybe respect inherited Display on `#[from]` variants
                //       where we get Meta::Path instead.

                // complain if user gave didn't give an error message
                let Meta::List(ref attr_args) = attr.meta else {
                    return Err(syn::Error::new_spanned(
                        attr,
                        "All variants must be given \
                        something to print, as the trait is defined as: `Error: Debug + Display`.",
                    ));
                };

                // complain if user used multiple error attrs on one variant
                if has_error_attribute {
                    return Err(syn::Error::new_spanned(
                        attr,
                        "Each variant may only have one `#[error(...)]` attribute.",
                    ));
                }

                // make sure the attribute has something inside of it
                // TODO: when a `#[from]` attr is present, don't check for this.
                if attr_args.tokens.is_empty() {
                    return Err(syn::Error::new_spanned(
                        attr_args,
                        "An `#[error(...)]` attribute must contain a value.",
                    ));
                }

                has_error_attribute = true;
                let match_head = make_match_head(enum_name, v);
                let tokens = add_self_to_idents(attr_args.clone())?;

                // FIXME: avoid this annoying clone :p
                //        i'll use unsafe if i have to, but i'm not deep copying
                //        the args to EVERY error at compile time...
                vec.push(quote! {
                    #match_head => {f.write_str(format!(#tokens).as_str())},
                });
            }
        }

        // if we don't have an error attribute, complain
        if !has_error_attribute {
            return Err(syn::Error::new_spanned(
                v,
                "Each variant must have a corresponding `#[error(...)]` attribute.",
            ));
        }
    }

    Ok(quote! {
        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
            match *self {
                #(#vec)*
            }
        }
    })
}

pub(crate) enum IdentOrLitStr {
    Ident(Ident),
    LitStr(LitStr),
}

impl syn::parse::Parse for IdentOrLitStr {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        if input.peek(Ident) {
            Ok(IdentOrLitStr::Ident(input.parse()?))
        } else if input.peek(LitStr) {
            Ok(IdentOrLitStr::LitStr(input.parse()?))
        } else {
            Err(input.error("uh oh"))
        }
    }
}

type Parser = Punctuated<IdentOrLitStr, Token![,]>;

/// Given an existing MetaList, this function will create another MetaList with
/// all `Idents` having `self.` appended to them.
///
/// This allows the `Display` trait to use fields within a struct-like Variant.
pub(crate) fn add_self_to_idents(list: MetaList) -> syn::Result<MetaList> {
    // parse list tokens into a dichotomic list: either a LitStr or Ident
    let Ok(parsed_args) = list.parse_args_with(Parser::parse_terminated) else {
        return Err(syn::Error::new_spanned(
            list,
            "The arguments in this `#[error(...)]` attribute failed to parse. \
            Please ensure you are using commas to separate them and that any \
            referenced fields exist.",
        ));
    };

    let parsed_args = parsed_args
        .iter()
        .map(|a| match a {
            IdentOrLitStr::Ident(i) => quote!(self.#i),
            IdentOrLitStr::LitStr(s) => quote!(#s),
        })
        .collect::<TokenStream2>(); // Punctuated<TokenStream2, Comma> ?

    // if we find an Ident, alter it to have `self.` in front.
    Ok(MetaList {
        path: list.path,
        delimiter: list.delimiter,
        tokens: parsed_args,
    })
}

@onkoe
Copy link
Owner Author

onkoe commented Jul 10, 2024

this is now half-implemented! now need to get tuplelike variants working without the appended underscore:

    #[error("my favorite color is: {_0}")] // FIXME: this is cruel
    MyTuplelikeVariant(String),

i think this might require manipulating the tokens. consider refactoring to stepped design first!

@onkoe
Copy link
Owner Author

onkoe commented Aug 21, 2024

current plan:

/// Checks a given f-string for tuple-like accesses, then appends an underscore
/// to the beginning for a correct `Display` implementation.
///
/// ex: `"{0}"` will invisibly become `"{_0}"`
fn add_underscores_to_bare_tuple_accesses(input: &TokenStream2) -> syn::Result<TokenStream2> {
    enum FStringPlaceholder {
        /// `"{}", 0`
        Positional(usize),
        /// `"{0}"`
        Named(usize),
    }

    // create a list of unescaped `{}` (positional) or `{...}` (named) exprs.
    //
    // for each positional expr, try matching an argument to it. otherwise, error

    // now, for each tuple-like positional, convert it to a named expr.
    //
    // e.g. `"hello, {}! you are {0} years old.", name`
    //        ===> `"hello, {}! you are {} years old.", name, _0`

    // for each tuple-like named, replace its argument with a value
    //
    // e.g. `"{}", 0` ===> `"{}", _0`

    // modify + return the stream.
    // IMPORTANT: DO NOT CREATE A NEW STREAM FROM SCRATCH. otherwise, you'll lose escapes, etc.
    //
    // this is only to fix the tuple stuff.

    todo!()
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant