Skip to content

Commit 8bc43f9

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 9be2f35 commit 8bc43f9

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
@@ -690,6 +690,24 @@ impl Diagnostic {
690690
msg: impl Into<SubdiagnosticMessage>,
691691
suggestions: impl Iterator<Item = String>,
692692
applicability: Applicability,
693+
) -> &mut Self {
694+
self.span_suggestions_with_style(
695+
sp,
696+
msg,
697+
suggestions,
698+
applicability,
699+
SuggestionStyle::ShowCode,
700+
)
701+
}
702+
703+
/// [`Diagnostic::span_suggestions()`] but you can set the [`SuggestionStyle`].
704+
pub fn span_suggestions_with_style(
705+
&mut self,
706+
sp: Span,
707+
msg: impl Into<SubdiagnosticMessage>,
708+
suggestions: impl Iterator<Item = String>,
709+
applicability: Applicability,
710+
style: SuggestionStyle,
693711
) -> &mut Self {
694712
let mut suggestions: Vec<_> = suggestions.collect();
695713
suggestions.sort();
@@ -706,14 +724,15 @@ impl Diagnostic {
706724
self.push_suggestion(CodeSuggestion {
707725
substitutions,
708726
msg: self.subdiagnostic_message_to_diagnostic_message(msg),
709-
style: SuggestionStyle::ShowCode,
727+
style,
710728
applicability,
711729
});
712730
self
713731
}
714732

715-
/// Prints out a message with multiple suggested edits of the code.
716-
/// See also [`Diagnostic::span_suggestion()`].
733+
/// Prints out a message with multiple suggested edits of the code, where each edit consists of
734+
/// multiple parts.
735+
/// See also [`Diagnostic::multipart_suggestion()`].
717736
pub fn multipart_suggestions(
718737
&mut self,
719738
msg: impl Into<SubdiagnosticMessage>,
@@ -745,6 +764,7 @@ impl Diagnostic {
745764
});
746765
self
747766
}
767+
748768
/// Prints out a message with a suggested edit of the code. If the suggestion is presented
749769
/// inline, it will only show the message and not the suggestion.
750770
///

compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs

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

455455
self.formatting_init.extend(code_init);
456456
Ok(quote! {
457-
#diag.span_suggestion_with_style(
457+
#diag.span_suggestions_with_style(
458458
#span_field,
459459
rustc_errors::fluent::#slug,
460460
#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,
@@ -414,30 +416,31 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
414416
let nested_name = meta.path().segments.last().unwrap().ident.to_string();
415417
let nested_name = nested_name.as_str();
416418

417-
let Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(value), .. }) = meta else {
418-
throw_invalid_nested_attr!(attr, &nested_attr);
419-
};
420-
421419
match nested_name {
422420
"code" => {
423-
let formatted_str = self.build_format(&value.value(), value.span());
424421
let code_field = new_code_ident();
425-
code.set_once((code_field, formatted_str), span);
422+
let formatting_init = build_suggestion_code(
423+
&code_field,
424+
meta,
425+
self,
426+
AllowMultipleAlternatives::No,
427+
);
428+
code.set_once((code_field, formatting_init), span);
426429
}
427430
_ => throw_invalid_nested_attr!(attr, &nested_attr, |diag| {
428431
diag.help("`code` is the only valid nested attribute")
429432
}),
430433
}
431434
}
432435

433-
let Some((code_field, formatted_str)) = code.value() else {
436+
let Some((code_field, formatting_init)) = code.value() else {
434437
span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
435438
.emit();
436439
return Ok(quote! {});
437440
};
438441
let binding = info.binding;
439442

440-
self.formatting_init.extend(quote! { let #code_field = #formatted_str; });
443+
self.formatting_init.extend(formatting_init);
441444
let code_field = if clone_suggestion_code {
442445
quote! { #code_field.clone() }
443446
} 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 {
@@ -571,28 +647,35 @@ impl SubdiagnosticKind {
571647
let nested_name = meta.path().segments.last().unwrap().ident.to_string();
572648
let nested_name = nested_name.as_str();
573649

574-
let value = match meta {
575-
Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(value), .. }) => value,
650+
let string_value = match meta {
651+
Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(value), .. }) => Some(value),
652+
576653
Meta::Path(_) => throw_invalid_nested_attr!(attr, &nested_attr, |diag| {
577654
diag.help("a diagnostic slug must be the first argument to the attribute")
578655
}),
579-
_ => {
580-
invalid_nested_attr(attr, &nested_attr).emit();
581-
continue;
582-
}
656+
_ => None,
583657
};
584658

585659
match (nested_name, &mut kind) {
586660
("code", SubdiagnosticKind::Suggestion { code_field, .. }) => {
587-
let formatted_str = fields.build_format(&value.value(), value.span());
588-
let code_init = quote! { let #code_field = #formatted_str; };
661+
let code_init = build_suggestion_code(
662+
code_field,
663+
meta,
664+
fields,
665+
AllowMultipleAlternatives::Yes,
666+
);
589667
code.set_once(code_init, span);
590668
}
591669
(
592670
"applicability",
593671
SubdiagnosticKind::Suggestion { ref mut applicability, .. }
594672
| SubdiagnosticKind::MultipartSuggestion { ref mut applicability, .. },
595673
) => {
674+
let Some(value) = string_value else {
675+
invalid_nested_attr(attr, &nested_attr).emit();
676+
continue;
677+
};
678+
596679
let value = Applicability::from_str(&value.value()).unwrap_or_else(|()| {
597680
span_err(span, "invalid applicability").emit();
598681
Applicability::Unspecified
@@ -623,7 +706,7 @@ impl SubdiagnosticKind {
623706
init
624707
} else {
625708
span_err(span, "suggestion without `code = \"...\"`").emit();
626-
quote! { let #code_field: String = unreachable!(); }
709+
quote! { let #code_field = std::iter::empty(); }
627710
};
628711
}
629712
SubdiagnosticKind::Label
@@ -644,7 +727,7 @@ impl quote::IdentFragment for SubdiagnosticKind {
644727
SubdiagnosticKind::Note => write!(f, "note"),
645728
SubdiagnosticKind::Help => write!(f, "help"),
646729
SubdiagnosticKind::Warn => write!(f, "warn"),
647-
SubdiagnosticKind::Suggestion { .. } => write!(f, "suggestion_with_style"),
730+
SubdiagnosticKind::Suggestion { .. } => write!(f, "suggestions_with_style"),
648731
SubdiagnosticKind::MultipartSuggestion { .. } => {
649732
write!(f, "multipart_suggestion_with_style")
650733
}

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

+38
Original file line numberDiff line numberDiff line change
@@ -758,3 +758,41 @@ struct WithDocComment {
758758
#[primary_span]
759759
span: Span,
760760
}
761+
762+
#[derive(Diagnostic)]
763+
#[diag(compiletest_example)]
764+
struct SuggestionsGood {
765+
#[suggestion(code("foo", "bar"))]
766+
sub: Span,
767+
}
768+
769+
#[derive(Diagnostic)]
770+
#[diag(compiletest_example)]
771+
struct SuggestionsSingleItem {
772+
#[suggestion(code("foo"))]
773+
sub: Span,
774+
}
775+
776+
#[derive(Diagnostic)]
777+
#[diag(compiletest_example)]
778+
struct SuggestionsNoItem {
779+
#[suggestion(code())]
780+
//~^ ERROR expected at least one string literal for `code(...)`
781+
sub: Span,
782+
}
783+
784+
#[derive(Diagnostic)]
785+
#[diag(compiletest_example)]
786+
struct SuggestionsInvalidItem {
787+
#[suggestion(code(foo))]
788+
//~^ ERROR `code(...)` must contain only string literals
789+
sub: Span,
790+
}
791+
792+
#[derive(Diagnostic)]
793+
#[diag(compiletest_example)]
794+
struct SuggestionsInvalidLiteral {
795+
#[suggestion(code = 3)]
796+
//~^ ERROR `code = "..."`/`code(...)` must contain only string literals
797+
sub: Span,
798+
}

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:779:18
578+
|
579+
LL | #[suggestion(code())]
580+
| ^^^^^^
581+
582+
error: `code(...)` must contain only string literals
583+
--> $DIR/diagnostic-derive.rs:787:23
584+
|
585+
LL | #[suggestion(code(foo))]
586+
| ^^^
587+
588+
error: `code = "..."`/`code(...)` must contain only string literals
589+
--> $DIR/diagnostic-derive.rs:795: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
@@ -661,3 +661,48 @@ enum BL {
661661
span: Span,
662662
}
663663
}
664+
665+
#[derive(Subdiagnostic)]
666+
#[multipart_suggestion(parser_add_paren)]
667+
struct BM {
668+
#[suggestion_part(code("foo"))]
669+
//~^ ERROR expected exactly one string literal for `code = ...`
670+
span: Span,
671+
r#type: String,
672+
}
673+
674+
#[derive(Subdiagnostic)]
675+
#[multipart_suggestion(parser_add_paren)]
676+
struct BN {
677+
#[suggestion_part(code("foo", "bar"))]
678+
//~^ ERROR expected exactly one string literal for `code = ...`
679+
span: Span,
680+
r#type: String,
681+
}
682+
683+
#[derive(Subdiagnostic)]
684+
#[multipart_suggestion(parser_add_paren)]
685+
struct BO {
686+
#[suggestion_part(code(3))]
687+
//~^ ERROR expected exactly one string literal for `code = ...`
688+
span: Span,
689+
r#type: String,
690+
}
691+
692+
#[derive(Subdiagnostic)]
693+
#[multipart_suggestion(parser_add_paren)]
694+
struct BP {
695+
#[suggestion_part(code())]
696+
//~^ ERROR expected exactly one string literal for `code = ...`
697+
span: Span,
698+
r#type: String,
699+
}
700+
701+
#[derive(Subdiagnostic)]
702+
#[multipart_suggestion(parser_add_paren)]
703+
struct BQ {
704+
#[suggestion_part(code = 3)]
705+
//~^ ERROR `code = "..."`/`code(...)` must contain only string literals
706+
span: Span,
707+
r#type: String,
708+
}

0 commit comments

Comments
 (0)