Convert parse_nested_meta to parse_args_with for #[subdiagnostic]

This commit is contained in:
Jonathan Brouwer 2026-01-28 22:18:44 +01:00
parent 9e61014a8a
commit 5d21a21695
No known key found for this signature in database
GPG key ID: 13619B051B673C52
6 changed files with 165 additions and 197 deletions

View file

@ -1,9 +1,10 @@
#![deny(unused_must_use)]
use proc_macro2::TokenStream;
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use syn::parse::ParseStream;
use syn::spanned::Spanned;
use syn::{Attribute, Meta, MetaList, Path};
use syn::{Attribute, Meta, MetaList, Path, Token};
use synstructure::{BindingInfo, Structure, VariantInfo};
use super::utils::SubdiagnosticVariant;
@ -437,23 +438,35 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
let mut code = None;
list.parse_nested_meta(|nested| {
if nested.path.is_ident("code") {
let code_field = new_code_ident();
let span = nested.path.span().unwrap();
let formatting_init = build_suggestion_code(
&code_field,
nested,
self,
AllowMultipleAlternatives::No,
);
code.set_once((code_field, formatting_init), span);
} else {
span_err(
nested.path.span().unwrap(),
"`code` is the only valid nested attribute",
)
.emit();
list.parse_args_with(|input: ParseStream<'_>| {
while !input.is_empty() {
let arg_name = input.parse::<Ident>()?;
match arg_name.to_string().as_str() {
"code" => {
let code_field = new_code_ident();
let formatting_init = build_suggestion_code(
&code_field,
input,
self,
AllowMultipleAlternatives::No,
)?;
code.set_once(
(code_field, formatting_init),
arg_name.span().unwrap(),
);
}
_ => {
span_err(
arg_name.span().unwrap(),
"`code` is the only valid nested attribute",
)
.emit();
}
}
if input.is_empty() {
break;
}
input.parse::<Token![,]>()?;
}
Ok(())
})?;

View file

@ -6,7 +6,7 @@ use std::str::FromStr;
use proc_macro::Span;
use proc_macro2::{Ident, TokenStream};
use quote::{ToTokens, format_ident, quote};
use syn::meta::ParseNestedMeta;
use syn::parse::ParseStream;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{Attribute, Field, LitStr, Meta, Path, Token, Type, TypeTuple, parenthesized};
@ -428,64 +428,51 @@ pub(super) enum AllowMultipleAlternatives {
}
fn parse_suggestion_values(
nested: ParseNestedMeta<'_>,
nested: ParseStream<'_>,
allow_multiple: AllowMultipleAlternatives,
) -> syn::Result<Vec<LitStr>> {
let values = if let Ok(val) = nested.value() {
vec![val.parse()?]
} else {
let content;
parenthesized!(content in nested.input);
if nested.parse::<Token![=]>().is_ok() {
return Ok(vec![nested.parse::<LitStr>()?]);
}
if let AllowMultipleAlternatives::No = allow_multiple {
let content;
parenthesized!(content in nested);
if let AllowMultipleAlternatives::No = allow_multiple {
span_err(content.span().unwrap(), "expected exactly one string literal for `code = ...`")
.emit();
return Ok(vec![]);
}
let literals = Punctuated::<LitStr, Token![,]>::parse_terminated(&content);
Ok(match literals {
Ok(p) if p.is_empty() => {
span_err(
nested.input.span().unwrap(),
"expected exactly one string literal for `code = ...`",
content.span().unwrap(),
"expected at least one string literal for `code(...)`",
)
.emit();
vec![]
} else {
let literals = Punctuated::<LitStr, Token![,]>::parse_terminated(&content);
match literals {
Ok(p) if p.is_empty() => {
span_err(
content.span().unwrap(),
"expected at least one string literal for `code(...)`",
)
.emit();
vec![]
}
Ok(p) => p.into_iter().collect(),
Err(_) => {
span_err(
content.span().unwrap(),
"`code(...)` must contain only string literals",
)
.emit();
vec![]
}
}
}
};
Ok(values)
Ok(p) => p.into_iter().collect(),
Err(_) => {
span_err(content.span().unwrap(), "`code(...)` must contain only string literals")
.emit();
vec![]
}
})
}
/// Constructs the `format!()` invocation(s) necessary for a `#[suggestion*(code = "foo")]` or
/// `#[suggestion*(code("foo", "bar"))]` attribute field
pub(super) fn build_suggestion_code(
code_field: &Ident,
nested: ParseNestedMeta<'_>,
nested: ParseStream<'_>,
fields: &impl HasFieldMap,
allow_multiple: AllowMultipleAlternatives,
) -> TokenStream {
let values = match parse_suggestion_values(nested, allow_multiple) {
Ok(x) => x,
Err(e) => return e.into_compile_error(),
};
) -> Result<TokenStream, syn::Error> {
let values = parse_suggestion_values(nested, allow_multiple)?;
if let AllowMultipleAlternatives::Yes = allow_multiple {
Ok(if let AllowMultipleAlternatives::Yes = allow_multiple {
let formatted_strings: Vec<_> = values
.into_iter()
.map(|value| fields.build_format(&value.value(), value.span()))
@ -497,7 +484,7 @@ pub(super) fn build_suggestion_code(
} else {
// error handled previously
quote! { let #code_field = String::new(); }
}
})
}
/// Possible styles for suggestion subdiagnostics.
@ -709,112 +696,95 @@ impl SubdiagnosticVariant {
let mut code = None;
let mut suggestion_kind = None;
let mut first = true;
let mut slug = None;
let mut no_span = false;
list.parse_nested_meta(|nested| {
if nested.input.is_empty() || nested.input.peek(Token![,]) {
if first {
slug = Some(nested.path);
} else if nested.path.is_ident("no_span") {
no_span = true;
} else {
span_err(nested.input.span().unwrap(), "a diagnostic slug must be the first argument to the attribute").emit();
list.parse_args_with(|input: ParseStream<'_>| {
let mut is_first = true;
while !input.is_empty() {
let arg_name: Path = input.parse::<Path>()?;
let arg_name_span = arg_name.span().unwrap();
if input.is_empty() || input.parse::<Token![,]>().is_ok() {
if is_first {
slug = Some(arg_name);
is_first = false;
} else {
span_err(arg_name_span, "a diagnostic slug must be the first argument to the attribute").emit();
}
continue
}
is_first = false;
first = false;
return Ok(());
}
match (arg_name.require_ident()?.to_string().as_str(), &mut kind) {
// ("no_span", _) => no_span = true,
("code", SubdiagnosticKind::Suggestion { code_field, .. }) => {
let code_init = build_suggestion_code(
&code_field,
&input,
fields,
AllowMultipleAlternatives::Yes,
)?;
code.set_once(code_init, arg_name_span);
}
(
"applicability",
SubdiagnosticKind::Suggestion { applicability, .. }
| SubdiagnosticKind::MultipartSuggestion { applicability, .. },
) => {
input.parse::<Token![=]>()?;
let value = input.parse::<LitStr>()?;
let value = Applicability::from_str(&value.value()).unwrap_or_else(|()| {
span_err(value.span().unwrap(), "invalid applicability").emit();
Applicability::Unspecified
});
applicability.set_once(value, span);
}
(
"style",
SubdiagnosticKind::Suggestion { .. }
| SubdiagnosticKind::MultipartSuggestion { .. },
) => {
input.parse::<Token![=]>()?;
let value = input.parse::<LitStr>()?;
first = false;
let value = value.value().parse().unwrap_or_else(|()| {
span_err(value.span().unwrap(), "invalid suggestion style")
.help("valid styles are `normal`, `short`, `hidden`, `verbose` and `tool-only`")
.emit();
SuggestionKind::Normal
});
let nested_name = nested.path.segments.last().unwrap().ident.to_string();
let nested_name = nested_name.as_str();
suggestion_kind.set_once(value, span);
}
let path_span = nested.path.span().unwrap();
let val_span = nested.input.span().unwrap();
macro_rules! get_string {
() => {{
let Ok(value) = nested.value().and_then(|x| x.parse::<LitStr>()) else {
span_err(val_span, "expected `= \"xxx\"`").emit();
return Ok(());
};
value
}};
}
let mut has_errors = false;
let input = nested.input;
match (nested_name, &mut kind) {
("code", SubdiagnosticKind::Suggestion { code_field, .. }) => {
let code_init = build_suggestion_code(
code_field,
nested,
fields,
AllowMultipleAlternatives::Yes,
);
code.set_once(code_init, path_span);
}
(
"applicability",
SubdiagnosticKind::Suggestion { applicability, .. }
| SubdiagnosticKind::MultipartSuggestion { applicability, .. },
) => {
let value = get_string!();
let value = Applicability::from_str(&value.value()).unwrap_or_else(|()| {
span_err(value.span().unwrap(), "invalid applicability").emit();
has_errors = true;
Applicability::Unspecified
});
applicability.set_once(value, span);
}
(
"style",
SubdiagnosticKind::Suggestion { .. }
| SubdiagnosticKind::MultipartSuggestion { .. },
) => {
let value = get_string!();
let value = value.value().parse().unwrap_or_else(|()| {
span_err(value.span().unwrap(), "invalid suggestion style")
.help("valid styles are `normal`, `short`, `hidden`, `verbose` and `tool-only`")
// Invalid nested attribute
(_, SubdiagnosticKind::Suggestion { .. }) => {
span_err(arg_name_span, "invalid nested attribute")
.help(
"only `no_span`, `style`, `code` and `applicability` are valid nested attributes",
)
.emit();
has_errors = true;
SuggestionKind::Normal
});
suggestion_kind.set_once(value, span);
// Consume the rest of the input to avoid spamming errors
let _ = input.parse::<TokenStream>();
}
(_, SubdiagnosticKind::MultipartSuggestion { .. }) => {
span_err(arg_name_span, "invalid nested attribute")
.help("only `no_span`, `style` and `applicability` are valid nested attributes")
.emit();
// Consume the rest of the input to avoid spamming errors
let _ = input.parse::<TokenStream>();
}
_ => {
span_err(arg_name_span, "only `no_span` is a valid nested attribute").emit();
// Consume the rest of the input to avoid spamming errors
let _ = input.parse::<TokenStream>();
}
}
// Invalid nested attribute
(_, SubdiagnosticKind::Suggestion { .. }) => {
span_err(path_span, "invalid nested attribute")
.help(
"only `no_span`, `style`, `code` and `applicability` are valid nested attributes",
)
.emit();
has_errors = true;
}
(_, SubdiagnosticKind::MultipartSuggestion { .. }) => {
span_err(path_span, "invalid nested attribute")
.help("only `no_span`, `style` and `applicability` are valid nested attributes")
.emit();
has_errors = true;
}
_ => {
span_err(path_span, "only `no_span` is a valid nested attribute").emit();
has_errors = true;
}
if input.is_empty() { break }
input.parse::<Token![,]>()?;
}
if has_errors {
// Consume the rest of the input to avoid spamming errors
let _ = input.parse::<TokenStream>();
}
Ok(())
})?;

View file

@ -803,7 +803,7 @@ struct SuggestionsInvalidItem {
sub: Span,
}
#[derive(Diagnostic)] //~ ERROR cannot find value `__code_34` in this scope
#[derive(Diagnostic)]
#[diag(no_crate_example)]
struct SuggestionsInvalidLiteral {
#[suggestion(code = 3)]

View file

@ -277,10 +277,10 @@ LL | #[help(no_crate_help)]
| ^
error: derive(Diagnostic): a diagnostic slug must be the first argument to the attribute
--> $DIR/diagnostic-derive.rs:533:32
--> $DIR/diagnostic-derive.rs:533:29
|
LL | #[label(no_crate_label, foo)]
| ^
| ^^^
error: derive(Diagnostic): only `no_span` is a valid nested attribute
--> $DIR/diagnostic-derive.rs:541:29
@ -606,14 +606,6 @@ error[E0425]: cannot find value `nonsense` in module `crate::fluent_generated`
LL | #[diag(nonsense, code = E0123)]
| ^^^^^^^^ not found in `crate::fluent_generated`
error[E0425]: cannot find value `__code_34` in this scope
--> $DIR/diagnostic-derive.rs:806:10
|
LL | #[derive(Diagnostic)]
| ^^^^^^^^^^ not found in this scope
|
= note: this error originates in the derive macro `Diagnostic` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `Hello: IntoDiagArg` is not satisfied
--> $DIR/diagnostic-derive.rs:347:12
|
@ -636,7 +628,7 @@ note: required by a bound in `Diag::<'a, G>::arg`
= note: in this macro invocation
= note: this error originates in the macro `with_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to 81 previous errors
error: aborting due to 80 previous errors
Some errors have detailed explanations: E0277, E0425.
For more information about an error, try `rustc --explain E0277`.

View file

@ -95,7 +95,7 @@ struct G {
#[derive(Subdiagnostic)]
#[label("...")]
//~^ ERROR unexpected literal in nested attribute, expected ident
//~^ ERROR expected identifier
struct H {
#[primary_span]
span: Span,
@ -775,7 +775,7 @@ struct SuggestionStyleInvalid1 {
#[derive(Subdiagnostic)]
#[suggestion(no_crate_example, code = "", style = 42)]
//~^ ERROR expected `= "xxx"`
//~^ ERROR expected string literal
struct SuggestionStyleInvalid2 {
#[primary_span]
sub: Span,
@ -791,8 +791,7 @@ struct SuggestionStyleInvalid3 {
#[derive(Subdiagnostic)]
#[suggestion(no_crate_example, code = "", style("foo"))]
//~^ ERROR expected `= "xxx"`
//~| ERROR expected `,`
//~^ ERROR expected `=`
struct SuggestionStyleInvalid4 {
#[primary_span]
sub: Span,

View file

@ -34,7 +34,7 @@ error: derive(Diagnostic): diagnostic slug must be first argument of a `#[label(
LL | #[label(bug = "...")]
| ^
error: unexpected literal in nested attribute, expected ident
error: expected identifier
--> $DIR/subdiagnostic-derive.rs:97:9
|
LL | #[label("...")]
@ -175,10 +175,10 @@ LL | | }
| |_^
error: derive(Diagnostic): a diagnostic slug must be the first argument to the attribute
--> $DIR/subdiagnostic-derive.rs:317:44
--> $DIR/subdiagnostic-derive.rs:317:27
|
LL | #[label(no_crate_example, no_crate::example)]
| ^
| ^^^^^^^^
error: derive(Diagnostic): attribute specified multiple times
--> $DIR/subdiagnostic-derive.rs:330:5
@ -381,10 +381,10 @@ LL | #[applicability]
| ^
error: derive(Diagnostic): expected exactly one string literal for `code = ...`
--> $DIR/subdiagnostic-derive.rs:663:34
--> $DIR/subdiagnostic-derive.rs:663:28
|
LL | #[suggestion_part(code("foo"))]
| ^
| ^^^^^
error: unexpected token, expected `)`
--> $DIR/subdiagnostic-derive.rs:663:28
@ -393,10 +393,10 @@ LL | #[suggestion_part(code("foo"))]
| ^^^^^
error: derive(Diagnostic): expected exactly one string literal for `code = ...`
--> $DIR/subdiagnostic-derive.rs:673:41
--> $DIR/subdiagnostic-derive.rs:673:28
|
LL | #[suggestion_part(code("foo", "bar"))]
| ^
| ^^^^^
error: unexpected token, expected `)`
--> $DIR/subdiagnostic-derive.rs:673:28
@ -405,10 +405,10 @@ LL | #[suggestion_part(code("foo", "bar"))]
| ^^^^^
error: derive(Diagnostic): expected exactly one string literal for `code = ...`
--> $DIR/subdiagnostic-derive.rs:683:30
--> $DIR/subdiagnostic-derive.rs:683:28
|
LL | #[suggestion_part(code(3))]
| ^
| ^
error: unexpected token, expected `)`
--> $DIR/subdiagnostic-derive.rs:683:28
@ -417,10 +417,10 @@ LL | #[suggestion_part(code(3))]
| ^
error: derive(Diagnostic): expected exactly one string literal for `code = ...`
--> $DIR/subdiagnostic-derive.rs:693:29
--> $DIR/subdiagnostic-derive.rs:693:28
|
LL | #[suggestion_part(code())]
| ^
| ^
error: expected string literal
--> $DIR/subdiagnostic-derive.rs:702:30
@ -464,32 +464,26 @@ LL | #[suggestion(no_crate_example, code = "", style = "foo")]
|
= help: valid styles are `normal`, `short`, `hidden`, `verbose` and `tool-only`
error: derive(Diagnostic): expected `= "xxx"`
--> $DIR/subdiagnostic-derive.rs:777:49
error: expected string literal
--> $DIR/subdiagnostic-derive.rs:777:51
|
LL | #[suggestion(no_crate_example, code = "", style = 42)]
| ^
| ^^
error: derive(Diagnostic): a diagnostic slug must be the first argument to the attribute
--> $DIR/subdiagnostic-derive.rs:785:48
--> $DIR/subdiagnostic-derive.rs:785:43
|
LL | #[suggestion(no_crate_example, code = "", style)]
| ^
| ^^^^^
error: derive(Diagnostic): expected `= "xxx"`
--> $DIR/subdiagnostic-derive.rs:793:48
|
LL | #[suggestion(no_crate_example, code = "", style("foo"))]
| ^
error: expected `,`
error: expected `=`
--> $DIR/subdiagnostic-derive.rs:793:48
|
LL | #[suggestion(no_crate_example, code = "", style("foo"))]
| ^
error: derive(Diagnostic): `#[primary_span]` is not a valid attribute
--> $DIR/subdiagnostic-derive.rs:805:5
--> $DIR/subdiagnostic-derive.rs:804:5
|
LL | #[primary_span]
| ^
@ -498,7 +492,7 @@ LL | #[primary_span]
= help: to create a suggestion with multiple spans, use `#[multipart_suggestion]` instead
error: derive(Diagnostic): suggestion without `#[primary_span]` field
--> $DIR/subdiagnostic-derive.rs:802:1
--> $DIR/subdiagnostic-derive.rs:801:1
|
LL | #[suggestion(no_crate_example, code = "")]
| ^
@ -557,5 +551,5 @@ error: cannot find attribute `bar` in this scope
LL | #[bar("...")]
| ^^^
error: aborting due to 84 previous errors
error: aborting due to 83 previous errors