Skip to content

Commit

Permalink
Allow specifying multiple alternative suggestions
Browse files Browse the repository at this point in the history
This allows porting uses of span_suggestions() to diagnostic structs.

Doesn't work for multipart_suggestions() because the rank would be
reversed - the struct would specify multiple spans, each of which has
multiple possible replacements, while multipart_suggestions() creates
multiple possible replacements, each with multiple spans.
  • Loading branch information
Xiretza committed Oct 23, 2022
1 parent 9be2f35 commit 8bc43f9
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 26 deletions.
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

0 comments on commit 8bc43f9

Please sign in to comment.