From 8bc43f99e91a94868fe08bb72b7ce66d7656d0b5 Mon Sep 17 00:00:00 2001
From: Xiretza <xiretza@xiretza.xyz>
Date: Mon, 17 Oct 2022 19:41:49 +0200
Subject: [PATCH] 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.
---
 compiler/rustc_errors/src/diagnostic.rs       |  26 ++++-
 .../src/diagnostics/diagnostic_builder.rs     |   2 +-
 .../src/diagnostics/subdiagnostic.rs          |  21 ++--
 .../rustc_macros/src/diagnostics/utils.rs     | 105 ++++++++++++++++--
 .../session-diagnostic/diagnostic-derive.rs   |  38 +++++++
 .../diagnostic-derive.stderr                  |  20 +++-
 .../subdiagnostic-derive.rs                   |  45 ++++++++
 .../subdiagnostic-derive.stderr               |  32 +++++-
 8 files changed, 263 insertions(+), 26 deletions(-)

diff --git a/compiler/rustc_errors/src/diagnostic.rs b/compiler/rustc_errors/src/diagnostic.rs
index a63fc0ca285da..23f29a24fe79f 100644
--- a/compiler/rustc_errors/src/diagnostic.rs
+++ b/compiler/rustc_errors/src/diagnostic.rs
@@ -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();
@@ -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>,
@@ -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.
     ///
diff --git a/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs b/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs
index 9f7d2661a3e8b..3ea83fd09c794 100644
--- a/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs
+++ b/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs
@@ -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,
diff --git a/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs b/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs
index d1acb71384220..fa0ca5a52423a 100644
--- a/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs
+++ b/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs
@@ -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,
@@ -414,15 +416,16 @@ 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")
@@ -430,14 +433,14 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
                     }
                 }
 
-                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 {
diff --git a/compiler/rustc_macros/src/diagnostics/utils.rs b/compiler/rustc_macros/src/diagnostics/utils.rs
index 61d5007fc30f0..374c795d0a638 100644
--- a/compiler/rustc_macros/src/diagnostics/utils.rs
+++ b/compiler/rustc_macros/src/diagnostics/utils.rs
@@ -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};
@@ -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 {
@@ -571,21 +647,23 @@ 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);
                 }
                 (
@@ -593,6 +671,11 @@ impl SubdiagnosticKind {
                     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
@@ -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
@@ -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")
             }
diff --git a/src/test/ui-fulldeps/session-diagnostic/diagnostic-derive.rs b/src/test/ui-fulldeps/session-diagnostic/diagnostic-derive.rs
index 46164d573b0bd..ca77e483d6ff8 100644
--- a/src/test/ui-fulldeps/session-diagnostic/diagnostic-derive.rs
+++ b/src/test/ui-fulldeps/session-diagnostic/diagnostic-derive.rs
@@ -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,
+}
diff --git a/src/test/ui-fulldeps/session-diagnostic/diagnostic-derive.stderr b/src/test/ui-fulldeps/session-diagnostic/diagnostic-derive.stderr
index 0a1c4bddb06a0..859c272b6ba9c 100644
--- a/src/test/ui-fulldeps/session-diagnostic/diagnostic-derive.stderr
+++ b/src/test/ui-fulldeps/session-diagnostic/diagnostic-derive.stderr
@@ -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
    |
@@ -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`.
diff --git a/src/test/ui-fulldeps/session-diagnostic/subdiagnostic-derive.rs b/src/test/ui-fulldeps/session-diagnostic/subdiagnostic-derive.rs
index 9088ca6ce462b..efec85eb52c2e 100644
--- a/src/test/ui-fulldeps/session-diagnostic/subdiagnostic-derive.rs
+++ b/src/test/ui-fulldeps/session-diagnostic/subdiagnostic-derive.rs
@@ -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,
+}
diff --git a/src/test/ui-fulldeps/session-diagnostic/subdiagnostic-derive.stderr b/src/test/ui-fulldeps/session-diagnostic/subdiagnostic-derive.stderr
index b21f9cc94a98c..a85a8711eaca4 100644
--- a/src/test/ui-fulldeps/session-diagnostic/subdiagnostic-derive.stderr
+++ b/src/test/ui-fulldeps/session-diagnostic/subdiagnostic-derive.stderr
@@ -415,6 +415,36 @@ error: `#[applicability]` has no effect if all `#[suggestion]`/`#[multipart_sugg
 LL |     #[applicability]
    |     ^^^^^^^^^^^^^^^^
 
+error: expected exactly one string literal for `code = ...`
+  --> $DIR/subdiagnostic-derive.rs:668:23
+   |
+LL |     #[suggestion_part(code("foo"))]
+   |                       ^^^^^^^^^^^
+
+error: expected exactly one string literal for `code = ...`
+  --> $DIR/subdiagnostic-derive.rs:677:23
+   |
+LL |     #[suggestion_part(code("foo", "bar"))]
+   |                       ^^^^^^^^^^^^^^^^^^
+
+error: expected exactly one string literal for `code = ...`
+  --> $DIR/subdiagnostic-derive.rs:686:23
+   |
+LL |     #[suggestion_part(code(3))]
+   |                       ^^^^^^^
+
+error: expected exactly one string literal for `code = ...`
+  --> $DIR/subdiagnostic-derive.rs:695:23
+   |
+LL |     #[suggestion_part(code())]
+   |                       ^^^^^^
+
+error: `code = "..."`/`code(...)` must contain only string literals
+  --> $DIR/subdiagnostic-derive.rs:704:23
+   |
+LL |     #[suggestion_part(code = 3)]
+   |                       ^^^^^^^^
+
 error: cannot find attribute `foo` in this scope
   --> $DIR/subdiagnostic-derive.rs:63:3
    |
@@ -475,6 +505,6 @@ error[E0425]: cannot find value `slug` in module `rustc_errors::fluent`
 LL | #[label(slug)]
    |         ^^^^ not found in `rustc_errors::fluent`
 
-error: aborting due to 67 previous errors
+error: aborting due to 72 previous errors
 
 For more information about this error, try `rustc --explain E0425`.