-
-
Notifications
You must be signed in to change notification settings - Fork 86
Pass tokens that failed to parse as Expr to FromMeta::from_verbatim
#408
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
Conversation
e245596 to
7920cfe
Compare
TedDriggs
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am very impressed with this change, but also very nervous.
My big fear is that this will somehow degrade darling errors in common cases, such as imbalanced parentheses, brackets, or braces. I don't know exactly how that would manifest, since until now I haven't looked closely at the error experiences when parsing into the syn::Meta.
I would be interested to see some before/after comparisons of the errors you get from darling with these changes in place.
Option A: Add more compile-fail tests
This is the higher-effort option. Add some tests like trying to put a Type that's not an Expr in the RHS position of a Meta, and commit the generated errors. Then reapply this change and regenerate the error messages, so that this PR can show the change in the errors.
Option B: Provide some examples loose in the PR comments
Since we've not had any previous expectations about behavior here, just showing some examples of how it changes would be good for assuaging my immediate fears. We can then add some tests later that capture the behavior in more detail.
|
I found a way to implement this feature without making the errors any worse
The initial version of this PR just disregarded the error completely But I have found an approach that's essentially the best of both worlds. Instead of ignoring the error, we store it, and then:
RepresentationWhen we come across an invalid expression: #[args(tag = <br />, foo = 10)]
^^^^^^
We get something like this: let input = quote! {
<br />
};
let error = quote! {
compile_error! { "invalid expression" }
};We must store the above 2 arbitrary Verbatim(TokenStream)We could store our input and error, each in their own Expr::Verbatim(quote! { (#input) (#error) })We could then parse this to get our There are 2 problems with that approach:
That's why I opted for a different representation: let input = quote! {
<br />
};
let error = quote! {
compile_error! { "invalid expression" }
};
Expr::Macro(parse_quote! {
#[__darling_expr_verbatim(
#error
)]
__darling_expr_verbatim! {
#input
}
})Inside of fn from_expr(expr: &Expr) -> Result<Self> {
match *expr {
// ...
_ => {
if let Some((input, error)) = expr_verbatim::decode(expr) {
// `input` is the tokens we failed to parse, and `error` is
// the `TokenStream` corresponding to the rendered `syn::Error` that we got
// when we failed to parse `input` as an expression
} else {
Err(Error::unexpected_expr_type(expr))
}
}
}
}That Once we have this fn from_verbatim(tokens: &TokenStream) -> Option<Result<Self>>;If this method returns If this method returns if let Some((input, error)) = expr_verbatim::decode(expr) {
if let Some(handled) = Self::from_verbatim(input) {
input
} else {
Err(Error::from_syn_rendered(error.clone()))
}
} else {
Err(Error::unexpected_expr_type(expr))
}That compile_error! { "unexpected expr" }
compile_error! { "something went wrong" }And parses them into a |
1d946ab to
441dd7e
Compare
40b877d to
afaa13f
Compare
Expr::VerbatimExpr to FromMeta::from_verbatim
964851a to
0da12b8
Compare
Why do we have this restriction? If we failed to parse to an Note that in general, |
Because these arbitrary tokens are parsed inside of fn parse_meta_name_value_after_path<'a>(
path: Path,
input: ParseStream<'a>,
) -> syn::Result<MetaNameValue> {This type requires the pub struct MetaNameValue {
pub path: Path,
pub eq_token: Token![=],
pub value: Expr,
}We try to parse the RHS into an expression here: match input.parse() {
Ok(expr) => expr,
// This isn't a valid expression, it might be something like `pub(in crate::module)`
// so we want to save that and let the user parse it (e.g. into a [`syn::Visibility`]).
//
// For more details, see docs of `darling::util::decode_if_verbatim`
Err(err) => {
let error: TokenStream = err.into_compile_error();
let verbatim_input: TokenStream = ...;
expr_verbatim::encode(verbatim_input, Some(error))
}
}This expression (the whole
In order to call After all, we are basically trying to support something like this: #[html(tag = <br />)]
^^^^^^To do that we need to return a MetaNameValue {
path: parse_quote!(tag),
eq_token: Token![=],
value: parse_quote!(<br />),
}But since
In this case there won't be an ergonomic advantage to storing this in /// Encodes an arbitrary stream of tokens + error stream into an `syn::Expr`
pub(crate) fn encode(input: TokenStream, error: Option<TokenStream>) -> Expr
/// Decodes a darling verbatim expression into the user's input + error that describes
/// why the expression failed to parse
pub fn decode(maybe_verbatim: &syn::Expr) -> Option<(&TokenStream, &TokenStream)>The only thing that will be different is internal representation of that |
TedDriggs
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have general concerns about this, and think we may need to discuss pros and cons of different approaches before continuing review of a specific PR.
core/src/from_meta.rs
Outdated
| Err(Error::unexpected_type("bool")) | ||
| } | ||
|
|
||
| /// Any other tokens that are not valid expressions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This current design feels dangerous.
Up until now, parsing has not produced a syn::Meta that would output different tokens than one parsed by syn itself. All previous discussions have been limited to changes around whether keywords are considered idents, which didn't affect output code.
This seems to change that; attempting to output the Meta could now produce tokens that are different than the input and that won't compile.
I fear this may push us to having a darling::ast::Meta so we can store this information in a safer way. That's a huge breaking change though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fear this may push us to having a darling::ast::Meta so we can store this information in a safer way. That's a huge breaking change though.
What about if we add a new variant to NestedMeta? This should break people who do a full match on NestedMeta but the impact should be minimal.
There are 700 usages of NestedMeta on GitHub: https://github.com/search?q=%2Fdarling%3A%3Aast%3A%3ANestedMeta%2F&type=code
I have opened about 40 of those and I didn't find a single match on a NestedMeta that would be broken by adding a new variant. Typical usages look like this:
let VisibilityAttrInternal {
public,
private,
vis: explicit,
} = VisibilityAttrInternal::from_list(items)?; let Inner {
array,
count,
nul_terminated,
opaque,
socket,
} = Inner::from_list(items)?; let args = darling::ast::NestedMeta::parse_meta_list(input.into())
.map_err(|e| syn::Error::new(proc_macro2::Span::call_site(), e))?; let values = items
.iter()
.map(|item| match item {
darling::ast::NestedMeta::Meta(meta) => PickArgs::from_meta(meta),
_ => Err(darling::Error::unexpected_type("non meta").with_span(item)),
})
.collect::<darling::Result<Vec<PickArgs>>>()?;Most usages of NestedMeta out there seem to just pass it to another function. If any pattern match it, they're usually in it for the NestedMeta::Meta and don't do a comprehensive match
I believe if we added a new variant we could represent this in a safe way, without causing too much of a headache for people. If we replaced syn::Meta that would be a huge change because it would need to be updated in all 1.1k places that it is used on GitHub. However, adding a new variant to NestedMeta might only require a few dozen updates
We can add a variant like this:
pub enum NestedMeta {
Meta(syn::Meta),
Lit(syn::Lit),
VerbatimMetaNameValue {
path: Path,
eq_token: Token![=],
// the original input
input: TokenStream,
// error that resulted from trying to parse into an Expr
error: syn::Error
}
}We can of course make that an opaque object like VerbatimMetaNameValue(VerbatimMetaNameValue) and then provide accessor methods on it. We also won't need to render/parse the syn::Error and can just directly store it then turn into darling::Error
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given this code:
#[derive(FromField)]
#[darling(attributes(outer))]
struct Field {
thing: Thing
}
#[derive(FromMeta)]
struct Thing {
arg: usize,
inner: Inner,
}
#[derive(FromMeta)]
struct Inner {
other: syn::LitStr,
vis: syn::Visibility
}For the input:
#[outer(thing(
arg = 4,
inner(
other = "Hello",
vis = pub(crate)
)
))]If I'm understanding correctly, our version of parsing a Meta would use syn::Expr::Verbatim where syn's parsing would've produced an error, but we won't retain that error initially.
Later on, when we call Inner::from_meta, that will see the value is a list and call Inner::from_list having just parsed the items into NestedMeta. This is the moment at which we produced the syn::Error that we actually hold onto.
Question 1: What function of FromMeta does that call, and what is the parameter it receives? I think we need the NestedMetaVerbatimNameValue struct so it can be the parameter type for this new from_invalid_meta method.
Question 2: In this example, there is no comma after the vis; will we stop consuming tokens in the right place?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm understanding correctly, our version of parsing a Meta would use syn::Expr::Verbatim where syn's parsing would've produced an error, but we won't retain that error initially.
Right now, we do retain the error as a TokenStream when parsing the NestedMeta, there's no way around that if we want good error messages. We first have syn::Error, then we call error.into_token_stream() to turn that into a bunch of compile_error!("...") invocations (the TokenStream)
With the approach I mentioned above (adding new variant to NestedMeta), we store the syn::Error directly in the field, without needing to do any kind of "encoding"
Later on, when we call Inner::from_meta, that will see the value is a list and call Inner::from_list having just parsed the items into NestedMeta. This is the moment at which we produced the syn::Error that we actually hold onto.
Yes.
Meta::List contains this TokenStream:
(
other = "Hello",
vis = pub(crate)
)Which is then parsed into the following 2 NestedMetas:
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
path: parse_quote(other),
value: parse_quote!("Hello"),
}))
NestedMeta::VerbatimNameValue(NestedMetaVerbatimNameValue {
path: parse_quote!(vis),
input: quote!(pub(crate)),
error: <syn error from Expr::parse(input)>
})Question 1: What function of FromMeta does that call, and what is the parameter it receives? I think we need the NestedMetaVerbatimNameValue struct so it can be the parameter type for this new from_invalid_meta method.
I like the name FromMeta::from_invalid_expr, maybe we can call the variant NestedMeta::InvalidNameValueExpr, it can receive the NestedMetaInvalidNameValueExpr as a parameter:
struct NestedMetaInvalidNameValueExpr {
path: Path,
eq_token: Token![=],
// the original input
input: TokenStream,
// error that resulted from trying to parse into an Expr
error: syn::Error
}We have FromMeta::from_expr that strictly deals with valid expressions, so FromMeta::from_invalid_expr will deal with invalid expressions
I believe this is better than from_invalid_meta or from_invalid_meta_name_value because the name part of the NestedMeta is valid (e.g. name = ) it is the value part which is not (the RHS is invalid):
vis = pub(crate)
^^^^^ valid
^^^^^^^^^^ invalidQuestion 2: In this example, there is no comma after the vis; will we stop consuming tokens in the right place?
Yes
This is the logic responsible for that:
fn eat_until_comma<'c>(
cursor: StepCursor<'c, '_>,
) -> syn::Result<(TokenStream, syn::buffer::Cursor<'c>)> {
let mut rest = *cursor;
let mut ts = TokenStream::new();
while let Some((tt, next)) = rest.token_tree() {
match tt {
TokenTree::Punct(punct) if punct.as_char() == ',' => {
break;
}
tt => {
ts.extend([tt]);
rest = next
}
}
}
Ok((ts, rest))
}This eats every token until it encounters a comma (the break), or end of input (while let aborts)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is sounding good: Existing crate users won't need to make any changes to preserve current behavior, calling ToTokens will always produce back the input we got, and we can unblock things like types and visibilities in RHS positions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just pushed a new iteration of the design. as discussed, it adds a new variant to NestedMeta without forcing us to resort to hacks.
e3df561 to
51bad4b
Compare
51bad4b to
5624026
Compare
TedDriggs
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are closing in.
5624026 to
ad5e7cb
Compare
ad5e7cb to
7d7a9c8
Compare
TedDriggs
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would probably benefit from additional tests to capture error behaviors, but I am content with merging the changes as-is and adding those tests in another PR.
This PR adds a new
FromMeta::from_invalid_exprwhich accepts an arbitraryTokenStreamas an input. This can be used to parse arbitrary code that fails to parse as asyn::Expr. For example, this failed to parse previously:#[method(vis = pub(crate))]You used to have to use quotation marks for that:
#[method(vis = "pub(crate)")]Because
pub(crate)is not a valid expression. Now, it will call thefrom_invalid_exprmethod, passing it theTokenStreamcorresponding to the tokenspub(crate)because we failed to parsepub(crate)as an expressionAlso,
syn::Type,syn::Type*,syn::Visibilityandsyn::WhereClausedon't require quotation marks anymoreCloses #378