Skip to content

Commit d96b68f

Browse files
committed
Allow specifying multiple alternative suggestions
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.
1 parent 21b2465 commit d96b68f

File tree

8 files changed

+263
-26
lines changed

8 files changed

+263
-26
lines changed

compiler/rustc_errors/src/diagnostic.rs

+23-3
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,24 @@ impl Diagnostic {
681681
msg: impl Into<SubdiagnosticMessage>,
682682
suggestions: impl Iterator<Item = String>,
683683
applicability: Applicability,
684+
) -> &mut Self {
685+
self.span_suggestions_with_style(
686+
sp,
687+
msg,
688+
suggestions,
689+
applicability,
690+
SuggestionStyle::ShowCode,
691+
)
692+
}
693+
694+
/// [`Diagnostic::span_suggestions()`] but you can set the [`SuggestionStyle`].
695+
pub fn span_suggestions_with_style(
696+
&mut self,
697+
sp: Span,
698+
msg: impl Into<SubdiagnosticMessage>,
699+
suggestions: impl Iterator<Item = String>,
700+
applicability: Applicability,
701+
style: SuggestionStyle,
684702
) -> &mut Self {
685703
let mut suggestions: Vec<_> = suggestions.collect();
686704
suggestions.sort();
@@ -691,14 +709,15 @@ impl Diagnostic {
691709
self.push_suggestion(CodeSuggestion {
692710
substitutions,
693711
msg: self.subdiagnostic_message_to_diagnostic_message(msg),
694-
style: SuggestionStyle::ShowCode,
712+
style,
695713
applicability,
696714
});
697715
self
698716
}
699717

700-
/// Prints out a message with multiple suggested edits of the code.
701-
/// See also [`Diagnostic::span_suggestion()`].
718+
/// Prints out a message with multiple suggested edits of the code, where each edit consists of
719+
/// multiple parts.
720+
/// See also [`Diagnostic::multipart_suggestion()`].
702721
pub fn multipart_suggestions(
703722
&mut self,
704723
msg: impl Into<SubdiagnosticMessage>,
@@ -720,6 +739,7 @@ impl Diagnostic {
720739
});
721740
self
722741
}
742+
723743
/// Prints out a message with a suggested edit of the code. If the suggestion is presented
724744
/// inline, it will only show the message and not the suggestion.
725745
///

compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ impl<'a> DiagnosticDeriveVariantBuilder<'a> {
433433

434434
self.formatting_init.extend(code_init);
435435
Ok(quote! {
436-
#diag.span_suggestion_with_style(
436+
#diag.span_suggestions_with_style(
437437
#span_field,
438438
rustc_errors::fluent::#slug,
439439
#code_field,

compiler/rustc_macros/src/diagnostics/subdiagnostic.rs

+12-9
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ use crate::diagnostics::utils::{
1111
};
1212
use proc_macro2::TokenStream;
1313
use quote::{format_ident, quote};
14-
use syn::{spanned::Spanned, Attribute, Meta, MetaList, MetaNameValue, NestedMeta, Path};
14+
use syn::{spanned::Spanned, Attribute, Meta, MetaList, NestedMeta, Path};
1515
use synstructure::{BindingInfo, Structure, VariantInfo};
1616

17+
use super::utils::{build_suggestion_code, AllowMultipleAlternatives};
18+
1719
/// The central struct for constructing the `add_to_diagnostic` method from an annotated struct.
1820
pub(crate) struct SubdiagnosticDeriveBuilder {
1921
diag: syn::Ident,
@@ -393,30 +395,31 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
393395
let nested_name = meta.path().segments.last().unwrap().ident.to_string();
394396
let nested_name = nested_name.as_str();
395397

396-
let Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(value), .. }) = meta else {
397-
throw_invalid_nested_attr!(attr, &nested_attr);
398-
};
399-
400398
match nested_name {
401399
"code" => {
402-
let formatted_str = self.build_format(&value.value(), value.span());
403400
let code_field = new_code_ident();
404-
code.set_once((code_field, formatted_str), span);
401+
let formatting_init = build_suggestion_code(
402+
&code_field,
403+
meta,
404+
self,
405+
AllowMultipleAlternatives::No,
406+
);
407+
code.set_once((code_field, formatting_init), span);
405408
}
406409
_ => throw_invalid_nested_attr!(attr, &nested_attr, |diag| {
407410
diag.help("`code` is the only valid nested attribute")
408411
}),
409412
}
410413
}
411414

412-
let Some((code_field, formatted_str)) = code.value() else {
415+
let Some((code_field, formatting_init)) = code.value() else {
413416
span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
414417
.emit();
415418
return Ok(quote! {});
416419
};
417420
let binding = info.binding;
418421

419-
self.formatting_init.extend(quote! { let #code_field = #formatted_str; });
422+
self.formatting_init.extend(formatting_init);
420423
let code_field = if clone_suggestion_code {
421424
quote! { #code_field.clone() }
422425
} else {

compiler/rustc_macros/src/diagnostics/utils.rs

+94-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::diagnostics::error::{
22
span_err, throw_invalid_attr, throw_invalid_nested_attr, throw_span_err, DiagnosticDeriveError,
33
};
44
use proc_macro::Span;
5-
use proc_macro2::TokenStream;
5+
use proc_macro2::{Ident, TokenStream};
66
use quote::{format_ident, quote, ToTokens};
77
use std::cell::RefCell;
88
use std::collections::{BTreeSet, HashMap};
@@ -395,6 +395,82 @@ pub(super) fn build_field_mapping<'v>(variant: &VariantInfo<'v>) -> HashMap<Stri
395395
fields_map
396396
}
397397

398+
#[derive(Copy, Clone, Debug)]
399+
pub(super) enum AllowMultipleAlternatives {
400+
No,
401+
Yes,
402+
}
403+
404+
/// Constructs the `format!()` invocation(s) necessary for a `#[suggestion*(code = "foo")]` or
405+
/// `#[suggestion*(code("foo", "bar"))]` attribute field
406+
pub(super) fn build_suggestion_code(
407+
code_field: &Ident,
408+
meta: &Meta,
409+
fields: &impl HasFieldMap,
410+
allow_multiple: AllowMultipleAlternatives,
411+
) -> TokenStream {
412+
let values = match meta {
413+
// `code = "foo"`
414+
Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(s), .. }) => vec![s],
415+
// `code("foo", "bar")`
416+
Meta::List(MetaList { nested, .. }) => {
417+
if let AllowMultipleAlternatives::No = allow_multiple {
418+
span_err(
419+
meta.span().unwrap(),
420+
"expected exactly one string literal for `code = ...`",
421+
)
422+
.emit();
423+
vec![]
424+
} else if nested.is_empty() {
425+
span_err(
426+
meta.span().unwrap(),
427+
"expected at least one string literal for `code(...)`",
428+
)
429+
.emit();
430+
vec![]
431+
} else {
432+
nested
433+
.into_iter()
434+
.filter_map(|item| {
435+
if let NestedMeta::Lit(syn::Lit::Str(s)) = item {
436+
Some(s)
437+
} else {
438+
span_err(
439+
item.span().unwrap(),
440+
"`code(...)` must contain only string literals",
441+
)
442+
.emit();
443+
None
444+
}
445+
})
446+
.collect()
447+
}
448+
}
449+
_ => {
450+
span_err(
451+
meta.span().unwrap(),
452+
r#"`code = "..."`/`code(...)` must contain only string literals"#,
453+
)
454+
.emit();
455+
vec![]
456+
}
457+
};
458+
459+
if let AllowMultipleAlternatives::Yes = allow_multiple {
460+
let formatted_strings: Vec<_> = values
461+
.into_iter()
462+
.map(|value| fields.build_format(&value.value(), value.span()))
463+
.collect();
464+
quote! { let #code_field = [#(#formatted_strings),*].into_iter(); }
465+
} else if let [value] = values.as_slice() {
466+
let formatted_str = fields.build_format(&value.value(), value.span());
467+
quote! { let #code_field = #formatted_str; }
468+
} else {
469+
// error handled previously
470+
quote! { let #code_field = String::new(); }
471+
}
472+
}
473+
398474
/// Possible styles for suggestion subdiagnostics.
399475
#[derive(Clone, Copy)]
400476
pub(super) enum SuggestionKind {
@@ -564,28 +640,35 @@ impl SubdiagnosticKind {
564640
let nested_name = meta.path().segments.last().unwrap().ident.to_string();
565641
let nested_name = nested_name.as_str();
566642

567-
let value = match meta {
568-
Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(value), .. }) => value,
643+
let string_value = match meta {
644+
Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(value), .. }) => Some(value),
645+
569646
Meta::Path(_) => throw_invalid_nested_attr!(attr, &nested_attr, |diag| {
570647
diag.help("a diagnostic slug must be the first argument to the attribute")
571648
}),
572-
_ => {
573-
invalid_nested_attr(attr, &nested_attr).emit();
574-
continue;
575-
}
649+
_ => None,
576650
};
577651

578652
match (nested_name, &mut kind) {
579653
("code", SubdiagnosticKind::Suggestion { code_field, .. }) => {
580-
let formatted_str = fields.build_format(&value.value(), value.span());
581-
let code_init = quote! { let #code_field = #formatted_str; };
654+
let code_init = build_suggestion_code(
655+
code_field,
656+
meta,
657+
fields,
658+
AllowMultipleAlternatives::Yes,
659+
);
582660
code.set_once(code_init, span);
583661
}
584662
(
585663
"applicability",
586664
SubdiagnosticKind::Suggestion { ref mut applicability, .. }
587665
| SubdiagnosticKind::MultipartSuggestion { ref mut applicability, .. },
588666
) => {
667+
let Some(value) = string_value else {
668+
invalid_nested_attr(attr, &nested_attr).emit();
669+
continue;
670+
};
671+
589672
let value = Applicability::from_str(&value.value()).unwrap_or_else(|()| {
590673
span_err(span, "invalid applicability").emit();
591674
Applicability::Unspecified
@@ -616,7 +699,7 @@ impl SubdiagnosticKind {
616699
init
617700
} else {
618701
span_err(span, "suggestion without `code = \"...\"`").emit();
619-
quote! { let #code_field: String = unreachable!(); }
702+
quote! { let #code_field = std::iter::empty(); }
620703
};
621704
}
622705
SubdiagnosticKind::Label
@@ -637,7 +720,7 @@ impl quote::IdentFragment for SubdiagnosticKind {
637720
SubdiagnosticKind::Note => write!(f, "note"),
638721
SubdiagnosticKind::Help => write!(f, "help"),
639722
SubdiagnosticKind::Warn => write!(f, "warn"),
640-
SubdiagnosticKind::Suggestion { .. } => write!(f, "suggestion_with_style"),
723+
SubdiagnosticKind::Suggestion { .. } => write!(f, "suggestions_with_style"),
641724
SubdiagnosticKind::MultipartSuggestion { .. } => {
642725
write!(f, "multipart_suggestion_with_style")
643726
}

src/test/ui-fulldeps/session-diagnostic/diagnostic-derive.rs

+38
Original file line numberDiff line numberDiff line change
@@ -749,3 +749,41 @@ struct SubdiagnosticEagerSuggestion {
749749
#[subdiagnostic(eager)]
750750
sub: SubdiagnosticWithSuggestion,
751751
}
752+
753+
#[derive(Diagnostic)]
754+
#[diag(compiletest::example)]
755+
struct SuggestionsGood {
756+
#[suggestion(code("foo", "bar"))]
757+
sub: Span,
758+
}
759+
760+
#[derive(Diagnostic)]
761+
#[diag(compiletest::example)]
762+
struct SuggestionsSingleItem {
763+
#[suggestion(code("foo"))]
764+
sub: Span,
765+
}
766+
767+
#[derive(Diagnostic)]
768+
#[diag(compiletest::example)]
769+
struct SuggestionsNoItem {
770+
#[suggestion(code())]
771+
//~^ ERROR expected at least one string literal for `code(...)`
772+
sub: Span,
773+
}
774+
775+
#[derive(Diagnostic)]
776+
#[diag(compiletest::example)]
777+
struct SuggestionsInvalidItem {
778+
#[suggestion(code(foo))]
779+
//~^ ERROR `code(...)` must contain only string literals
780+
sub: Span,
781+
}
782+
783+
#[derive(Diagnostic)]
784+
#[diag(compiletest::example)]
785+
struct SuggestionsInvalidLiteral {
786+
#[suggestion(code = 3)]
787+
//~^ ERROR `code = "..."`/`code(...)` must contain only string literals
788+
sub: Span,
789+
}

src/test/ui-fulldeps/session-diagnostic/diagnostic-derive.stderr

+19-1
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,24 @@ LL | #[subdiagnostic(eager)]
573573
|
574574
= help: eager subdiagnostics are not supported on lints
575575

576+
error: expected at least one string literal for `code(...)`
577+
--> $DIR/diagnostic-derive.rs:770:18
578+
|
579+
LL | #[suggestion(code())]
580+
| ^^^^^^
581+
582+
error: `code(...)` must contain only string literals
583+
--> $DIR/diagnostic-derive.rs:778:23
584+
|
585+
LL | #[suggestion(code(foo))]
586+
| ^^^
587+
588+
error: `code = "..."`/`code(...)` must contain only string literals
589+
--> $DIR/diagnostic-derive.rs:786:18
590+
|
591+
LL | #[suggestion(code = 3)]
592+
| ^^^^^^^^
593+
576594
error: cannot find attribute `nonsense` in this scope
577595
--> $DIR/diagnostic-derive.rs:55:3
578596
|
@@ -647,7 +665,7 @@ LL | arg: impl IntoDiagnosticArg,
647665
| ^^^^^^^^^^^^^^^^^ required by this bound in `DiagnosticBuilder::<'a, G>::set_arg`
648666
= note: this error originates in the derive macro `Diagnostic` (in Nightly builds, run with -Z macro-backtrace for more info)
649667

650-
error: aborting due to 80 previous errors
668+
error: aborting due to 83 previous errors
651669

652670
Some errors have detailed explanations: E0277, E0425.
653671
For more information about an error, try `rustc --explain E0277`.

src/test/ui-fulldeps/session-diagnostic/subdiagnostic-derive.rs

+45
Original file line numberDiff line numberDiff line change
@@ -641,3 +641,48 @@ struct BJ {
641641
span: Span,
642642
r#type: String,
643643
}
644+
645+
#[derive(Subdiagnostic)]
646+
#[multipart_suggestion(parser::add_paren)]
647+
struct BK {
648+
#[suggestion_part(code("foo"))]
649+
//~^ ERROR expected exactly one string literal for `code = ...`
650+
span: Span,
651+
r#type: String,
652+
}
653+
654+
#[derive(Subdiagnostic)]
655+
#[multipart_suggestion(parser::add_paren)]
656+
struct BL {
657+
#[suggestion_part(code("foo", "bar"))]
658+
//~^ ERROR expected exactly one string literal for `code = ...`
659+
span: Span,
660+
r#type: String,
661+
}
662+
663+
#[derive(Subdiagnostic)]
664+
#[multipart_suggestion(parser::add_paren)]
665+
struct BM {
666+
#[suggestion_part(code(3))]
667+
//~^ ERROR expected exactly one string literal for `code = ...`
668+
span: Span,
669+
r#type: String,
670+
}
671+
672+
#[derive(Subdiagnostic)]
673+
#[multipart_suggestion(parser::add_paren)]
674+
struct BN {
675+
#[suggestion_part(code())]
676+
//~^ ERROR expected exactly one string literal for `code = ...`
677+
span: Span,
678+
r#type: String,
679+
}
680+
681+
#[derive(Subdiagnostic)]
682+
#[multipart_suggestion(parser::add_paren)]
683+
struct BO {
684+
#[suggestion_part(code = 3)]
685+
//~^ ERROR `code = "..."`/`code(...)` must contain only string literals
686+
span: Span,
687+
r#type: String,
688+
}

0 commit comments

Comments
 (0)