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

Diagnostic derives: allow specifying multiple alternative suggestions #103209

Merged
merged 1 commit into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions compiler/rustc_errors/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,24 @@ impl Diagnostic {
msg: impl Into<SubdiagnosticMessage>,
suggestions: impl Iterator<Item = String>,
applicability: Applicability,
) -> &mut Self {
self.span_suggestions_with_style(
sp,
msg,
suggestions,
applicability,
SuggestionStyle::ShowCode,
)
}

/// [`Diagnostic::span_suggestions()`] but you can set the [`SuggestionStyle`].
pub fn span_suggestions_with_style(
&mut self,
sp: Span,
msg: impl Into<SubdiagnosticMessage>,
suggestions: impl Iterator<Item = String>,
applicability: Applicability,
style: SuggestionStyle,
) -> &mut Self {
let mut suggestions: Vec<_> = suggestions.collect();
suggestions.sort();
Expand All @@ -706,14 +724,15 @@ impl Diagnostic {
self.push_suggestion(CodeSuggestion {
substitutions,
msg: self.subdiagnostic_message_to_diagnostic_message(msg),
style: SuggestionStyle::ShowCode,
style,
applicability,
});
self
}

/// Prints out a message with multiple suggested edits of the code.
/// See also [`Diagnostic::span_suggestion()`].
/// Prints out a message with multiple suggested edits of the code, where each edit consists of
/// multiple parts.
/// See also [`Diagnostic::multipart_suggestion()`].
pub fn multipart_suggestions(
&mut self,
msg: impl Into<SubdiagnosticMessage>,
Expand Down Expand Up @@ -745,6 +764,7 @@ impl Diagnostic {
});
self
}

/// Prints out a message with a suggested edit of the code. If the suggestion is presented
/// inline, it will only show the message and not the suggestion.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ impl<'a> DiagnosticDeriveVariantBuilder<'a> {

self.formatting_init.extend(code_init);
Ok(quote! {
#diag.span_suggestion_with_style(
#diag.span_suggestions_with_style(
#span_field,
rustc_errors::fluent::#slug,
#code_field,
Expand Down
21 changes: 12 additions & 9 deletions compiler/rustc_macros/src/diagnostics/subdiagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ use crate::diagnostics::utils::{
};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{spanned::Spanned, Attribute, Meta, MetaList, MetaNameValue, NestedMeta, Path};
use syn::{spanned::Spanned, Attribute, Meta, MetaList, NestedMeta, Path};
use synstructure::{BindingInfo, Structure, VariantInfo};

use super::utils::{build_suggestion_code, AllowMultipleAlternatives};

/// The central struct for constructing the `add_to_diagnostic` method from an annotated struct.
pub(crate) struct SubdiagnosticDeriveBuilder {
diag: syn::Ident,
Expand Down Expand Up @@ -414,30 +416,31 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
let nested_name = meta.path().segments.last().unwrap().ident.to_string();
let nested_name = nested_name.as_str();

let Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(value), .. }) = meta else {
throw_invalid_nested_attr!(attr, &nested_attr);
};

match nested_name {
"code" => {
let formatted_str = self.build_format(&value.value(), value.span());
let code_field = new_code_ident();
code.set_once((code_field, formatted_str), span);
let formatting_init = build_suggestion_code(
&code_field,
meta,
self,
AllowMultipleAlternatives::No,
);
code.set_once((code_field, formatting_init), span);
}
_ => throw_invalid_nested_attr!(attr, &nested_attr, |diag| {
diag.help("`code` is the only valid nested attribute")
}),
}
}

let Some((code_field, formatted_str)) = code.value() else {
let Some((code_field, formatting_init)) = code.value() else {
span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
.emit();
return Ok(quote! {});
};
let binding = info.binding;

self.formatting_init.extend(quote! { let #code_field = #formatted_str; });
self.formatting_init.extend(formatting_init);
let code_field = if clone_suggestion_code {
quote! { #code_field.clone() }
} else {
Expand Down
105 changes: 94 additions & 11 deletions compiler/rustc_macros/src/diagnostics/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::diagnostics::error::{
span_err, throw_invalid_attr, throw_invalid_nested_attr, throw_span_err, DiagnosticDeriveError,
};
use proc_macro::Span;
use proc_macro2::TokenStream;
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote, ToTokens};
use std::cell::RefCell;
use std::collections::{BTreeSet, HashMap};
Expand Down Expand Up @@ -395,6 +395,82 @@ pub(super) fn build_field_mapping<'v>(variant: &VariantInfo<'v>) -> HashMap<Stri
fields_map
}

#[derive(Copy, Clone, Debug)]
pub(super) enum AllowMultipleAlternatives {
No,
Yes,
}

/// 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,
meta: &Meta,
fields: &impl HasFieldMap,
allow_multiple: AllowMultipleAlternatives,
) -> TokenStream {
let values = match meta {
// `code = "foo"`
Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(s), .. }) => vec![s],
// `code("foo", "bar")`
Meta::List(MetaList { nested, .. }) => {
if let AllowMultipleAlternatives::No = allow_multiple {
span_err(
meta.span().unwrap(),
"expected exactly one string literal for `code = ...`",
)
.emit();
vec![]
} else if nested.is_empty() {
span_err(
meta.span().unwrap(),
"expected at least one string literal for `code(...)`",
)
.emit();
vec![]
} else {
nested
.into_iter()
.filter_map(|item| {
if let NestedMeta::Lit(syn::Lit::Str(s)) = item {
Some(s)
} else {
span_err(
item.span().unwrap(),
"`code(...)` must contain only string literals",
)
.emit();
None
}
})
.collect()
}
}
_ => {
span_err(
meta.span().unwrap(),
r#"`code = "..."`/`code(...)` must contain only string literals"#,
)
.emit();
vec![]
}
};

if let AllowMultipleAlternatives::Yes = allow_multiple {
let formatted_strings: Vec<_> = values
.into_iter()
.map(|value| fields.build_format(&value.value(), value.span()))
.collect();
quote! { let #code_field = [#(#formatted_strings),*].into_iter(); }
} else if let [value] = values.as_slice() {
let formatted_str = fields.build_format(&value.value(), value.span());
quote! { let #code_field = #formatted_str; }
} else {
// error handled previously
quote! { let #code_field = String::new(); }
}
}

/// Possible styles for suggestion subdiagnostics.
#[derive(Clone, Copy)]
pub(super) enum SuggestionKind {
Expand Down Expand Up @@ -571,28 +647,35 @@ impl SubdiagnosticKind {
let nested_name = meta.path().segments.last().unwrap().ident.to_string();
let nested_name = nested_name.as_str();

let value = match meta {
Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(value), .. }) => value,
let string_value = match meta {
Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(value), .. }) => Some(value),

Meta::Path(_) => throw_invalid_nested_attr!(attr, &nested_attr, |diag| {
diag.help("a diagnostic slug must be the first argument to the attribute")
}),
_ => {
invalid_nested_attr(attr, &nested_attr).emit();
continue;
}
_ => None,
};

match (nested_name, &mut kind) {
("code", SubdiagnosticKind::Suggestion { code_field, .. }) => {
let formatted_str = fields.build_format(&value.value(), value.span());
let code_init = quote! { let #code_field = #formatted_str; };
let code_init = build_suggestion_code(
code_field,
meta,
fields,
AllowMultipleAlternatives::Yes,
);
code.set_once(code_init, span);
}
(
"applicability",
SubdiagnosticKind::Suggestion { ref mut applicability, .. }
| SubdiagnosticKind::MultipartSuggestion { ref mut applicability, .. },
) => {
let Some(value) = string_value else {
invalid_nested_attr(attr, &nested_attr).emit();
continue;
};

let value = Applicability::from_str(&value.value()).unwrap_or_else(|()| {
span_err(span, "invalid applicability").emit();
Applicability::Unspecified
Expand Down Expand Up @@ -623,7 +706,7 @@ impl SubdiagnosticKind {
init
} else {
span_err(span, "suggestion without `code = \"...\"`").emit();
quote! { let #code_field: String = unreachable!(); }
quote! { let #code_field = std::iter::empty(); }
};
}
SubdiagnosticKind::Label
Expand All @@ -644,7 +727,7 @@ impl quote::IdentFragment for SubdiagnosticKind {
SubdiagnosticKind::Note => write!(f, "note"),
SubdiagnosticKind::Help => write!(f, "help"),
SubdiagnosticKind::Warn => write!(f, "warn"),
SubdiagnosticKind::Suggestion { .. } => write!(f, "suggestion_with_style"),
SubdiagnosticKind::Suggestion { .. } => write!(f, "suggestions_with_style"),
SubdiagnosticKind::MultipartSuggestion { .. } => {
write!(f, "multipart_suggestion_with_style")
}
Expand Down
38 changes: 38 additions & 0 deletions src/test/ui-fulldeps/session-diagnostic/diagnostic-derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -758,3 +758,41 @@ struct WithDocComment {
#[primary_span]
span: Span,
}

#[derive(Diagnostic)]
#[diag(compiletest_example)]
struct SuggestionsGood {
#[suggestion(code("foo", "bar"))]
sub: Span,
}

#[derive(Diagnostic)]
#[diag(compiletest_example)]
struct SuggestionsSingleItem {
#[suggestion(code("foo"))]
sub: Span,
}

#[derive(Diagnostic)]
#[diag(compiletest_example)]
struct SuggestionsNoItem {
#[suggestion(code())]
//~^ ERROR expected at least one string literal for `code(...)`
sub: Span,
}

#[derive(Diagnostic)]
#[diag(compiletest_example)]
struct SuggestionsInvalidItem {
#[suggestion(code(foo))]
//~^ ERROR `code(...)` must contain only string literals
sub: Span,
}

#[derive(Diagnostic)]
#[diag(compiletest_example)]
struct SuggestionsInvalidLiteral {
#[suggestion(code = 3)]
//~^ ERROR `code = "..."`/`code(...)` must contain only string literals
sub: Span,
}
20 changes: 19 additions & 1 deletion src/test/ui-fulldeps/session-diagnostic/diagnostic-derive.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,24 @@ LL | #[subdiagnostic(eager)]
|
= help: eager subdiagnostics are not supported on lints

error: expected at least one string literal for `code(...)`
--> $DIR/diagnostic-derive.rs:779:18
|
LL | #[suggestion(code())]
| ^^^^^^

error: `code(...)` must contain only string literals
--> $DIR/diagnostic-derive.rs:787:23
|
LL | #[suggestion(code(foo))]
| ^^^

error: `code = "..."`/`code(...)` must contain only string literals
--> $DIR/diagnostic-derive.rs:795:18
|
LL | #[suggestion(code = 3)]
| ^^^^^^^^

error: cannot find attribute `nonsense` in this scope
--> $DIR/diagnostic-derive.rs:55:3
|
Expand Down Expand Up @@ -647,7 +665,7 @@ LL | arg: impl IntoDiagnosticArg,
| ^^^^^^^^^^^^^^^^^ required by this bound in `DiagnosticBuilder::<'a, G>::set_arg`
= note: this error originates in the derive macro `Diagnostic` (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to 80 previous errors
error: aborting due to 83 previous errors

Some errors have detailed explanations: E0277, E0425.
For more information about an error, try `rustc --explain E0277`.
45 changes: 45 additions & 0 deletions src/test/ui-fulldeps/session-diagnostic/subdiagnostic-derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -661,3 +661,48 @@ enum BL {
span: Span,
}
}

#[derive(Subdiagnostic)]
#[multipart_suggestion(parser_add_paren)]
struct BM {
#[suggestion_part(code("foo"))]
//~^ ERROR expected exactly one string literal for `code = ...`
span: Span,
r#type: String,
}

#[derive(Subdiagnostic)]
#[multipart_suggestion(parser_add_paren)]
struct BN {
#[suggestion_part(code("foo", "bar"))]
//~^ ERROR expected exactly one string literal for `code = ...`
span: Span,
r#type: String,
}

#[derive(Subdiagnostic)]
#[multipart_suggestion(parser_add_paren)]
struct BO {
#[suggestion_part(code(3))]
//~^ ERROR expected exactly one string literal for `code = ...`
span: Span,
r#type: String,
}

#[derive(Subdiagnostic)]
#[multipart_suggestion(parser_add_paren)]
struct BP {
#[suggestion_part(code())]
//~^ ERROR expected exactly one string literal for `code = ...`
span: Span,
r#type: String,
}

#[derive(Subdiagnostic)]
#[multipart_suggestion(parser_add_paren)]
struct BQ {
#[suggestion_part(code = 3)]
//~^ ERROR `code = "..."`/`code(...)` must contain only string literals
span: Span,
r#type: String,
}
Loading