summaryrefslogtreecommitdiffstats
path: root/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs
diff options
context:
space:
mode:
Diffstat (limited to 'compiler/rustc_macros/src/diagnostics/subdiagnostic.rs')
-rw-r--r--compiler/rustc_macros/src/diagnostics/subdiagnostic.rs491
1 files changed, 491 insertions, 0 deletions
diff --git a/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs b/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs
new file mode 100644
index 000000000..edf4dbed9
--- /dev/null
+++ b/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs
@@ -0,0 +1,491 @@
+#![deny(unused_must_use)]
+
+use crate::diagnostics::error::{
+ span_err, throw_invalid_attr, throw_invalid_nested_attr, throw_span_err, DiagnosticDeriveError,
+};
+use crate::diagnostics::utils::{
+ report_error_if_not_applied_to_applicability, report_error_if_not_applied_to_span,
+ Applicability, FieldInfo, FieldInnerTy, HasFieldMap, SetOnce,
+};
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
+use std::collections::HashMap;
+use std::fmt;
+use std::str::FromStr;
+use syn::{parse_quote, spanned::Spanned, Meta, MetaList, MetaNameValue, NestedMeta, Path};
+use synstructure::{BindingInfo, Structure, VariantInfo};
+
+/// Which kind of suggestion is being created?
+#[derive(Clone, Copy)]
+enum SubdiagnosticSuggestionKind {
+ /// `#[suggestion]`
+ Normal,
+ /// `#[suggestion_short]`
+ Short,
+ /// `#[suggestion_hidden]`
+ Hidden,
+ /// `#[suggestion_verbose]`
+ Verbose,
+}
+
+/// Which kind of subdiagnostic is being created from a variant?
+#[derive(Clone, Copy)]
+enum SubdiagnosticKind {
+ /// `#[label(...)]`
+ Label,
+ /// `#[note(...)]`
+ Note,
+ /// `#[help(...)]`
+ Help,
+ /// `#[warn_(...)]`
+ Warn,
+ /// `#[suggestion{,_short,_hidden,_verbose}]`
+ Suggestion(SubdiagnosticSuggestionKind),
+}
+
+impl FromStr for SubdiagnosticKind {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "label" => Ok(SubdiagnosticKind::Label),
+ "note" => Ok(SubdiagnosticKind::Note),
+ "help" => Ok(SubdiagnosticKind::Help),
+ "warn_" => Ok(SubdiagnosticKind::Warn),
+ "suggestion" => Ok(SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Normal)),
+ "suggestion_short" => {
+ Ok(SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Short))
+ }
+ "suggestion_hidden" => {
+ Ok(SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Hidden))
+ }
+ "suggestion_verbose" => {
+ Ok(SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Verbose))
+ }
+ _ => Err(()),
+ }
+ }
+}
+
+impl quote::IdentFragment for SubdiagnosticKind {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ SubdiagnosticKind::Label => write!(f, "label"),
+ SubdiagnosticKind::Note => write!(f, "note"),
+ SubdiagnosticKind::Help => write!(f, "help"),
+ SubdiagnosticKind::Warn => write!(f, "warn"),
+ SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Normal) => {
+ write!(f, "suggestion")
+ }
+ SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Short) => {
+ write!(f, "suggestion_short")
+ }
+ SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Hidden) => {
+ write!(f, "suggestion_hidden")
+ }
+ SubdiagnosticKind::Suggestion(SubdiagnosticSuggestionKind::Verbose) => {
+ write!(f, "suggestion_verbose")
+ }
+ }
+ }
+
+ fn span(&self) -> Option<proc_macro2::Span> {
+ None
+ }
+}
+
+/// The central struct for constructing the `add_to_diagnostic` method from an annotated struct.
+pub(crate) struct SessionSubdiagnosticDerive<'a> {
+ structure: Structure<'a>,
+ diag: syn::Ident,
+}
+
+impl<'a> SessionSubdiagnosticDerive<'a> {
+ pub(crate) fn new(structure: Structure<'a>) -> Self {
+ let diag = format_ident!("diag");
+ Self { structure, diag }
+ }
+
+ pub(crate) fn into_tokens(self) -> TokenStream {
+ let SessionSubdiagnosticDerive { mut structure, diag } = self;
+ let implementation = {
+ let ast = structure.ast();
+ let span = ast.span().unwrap();
+ match ast.data {
+ syn::Data::Struct(..) | syn::Data::Enum(..) => (),
+ syn::Data::Union(..) => {
+ span_err(
+ span,
+ "`#[derive(SessionSubdiagnostic)]` can only be used on structs and enums",
+ );
+ }
+ }
+
+ if matches!(ast.data, syn::Data::Enum(..)) {
+ for attr in &ast.attrs {
+ span_err(
+ attr.span().unwrap(),
+ "unsupported type attribute for subdiagnostic enum",
+ )
+ .emit();
+ }
+ }
+
+ structure.bind_with(|_| synstructure::BindStyle::Move);
+ let variants_ = structure.each_variant(|variant| {
+ // Build the mapping of field names to fields. This allows attributes to peek
+ // values from other fields.
+ let mut fields_map = HashMap::new();
+ for binding in variant.bindings() {
+ let field = binding.ast();
+ if let Some(ident) = &field.ident {
+ fields_map.insert(ident.to_string(), quote! { #binding });
+ }
+ }
+
+ let mut builder = SessionSubdiagnosticDeriveBuilder {
+ diag: &diag,
+ variant,
+ span,
+ fields: fields_map,
+ kind: None,
+ slug: None,
+ code: None,
+ span_field: None,
+ applicability: None,
+ };
+ builder.into_tokens().unwrap_or_else(|v| v.to_compile_error())
+ });
+
+ quote! {
+ match self {
+ #variants_
+ }
+ }
+ };
+
+ let ret = structure.gen_impl(quote! {
+ gen impl rustc_errors::AddSubdiagnostic for @Self {
+ fn add_to_diagnostic(self, #diag: &mut rustc_errors::Diagnostic) {
+ use rustc_errors::{Applicability, IntoDiagnosticArg};
+ #implementation
+ }
+ }
+ });
+ ret
+ }
+}
+
+/// Tracks persistent information required for building up the call to add to the diagnostic
+/// for the final generated method. This is a separate struct to `SessionSubdiagnosticDerive`
+/// only to be able to destructure and split `self.builder` and the `self.structure` up to avoid a
+/// double mut borrow later on.
+struct SessionSubdiagnosticDeriveBuilder<'a> {
+ /// The identifier to use for the generated `DiagnosticBuilder` instance.
+ diag: &'a syn::Ident,
+
+ /// Info for the current variant (or the type if not an enum).
+ variant: &'a VariantInfo<'a>,
+ /// Span for the entire type.
+ span: proc_macro::Span,
+
+ /// Store a map of field name to its corresponding field. This is built on construction of the
+ /// derive builder.
+ fields: HashMap<String, TokenStream>,
+
+ /// Subdiagnostic kind of the type/variant.
+ kind: Option<(SubdiagnosticKind, proc_macro::Span)>,
+
+ /// Slug of the subdiagnostic - corresponds to the Fluent identifier for the message - from the
+ /// `#[kind(slug)]` attribute on the type or variant.
+ slug: Option<(Path, proc_macro::Span)>,
+ /// If a suggestion, the code to suggest as a replacement - from the `#[kind(code = "...")]`
+ /// attribute on the type or variant.
+ code: Option<(TokenStream, proc_macro::Span)>,
+
+ /// Identifier for the binding to the `#[primary_span]` field.
+ span_field: Option<(proc_macro2::Ident, proc_macro::Span)>,
+ /// If a suggestion, the identifier for the binding to the `#[applicability]` field or a
+ /// `rustc_errors::Applicability::*` variant directly.
+ applicability: Option<(TokenStream, proc_macro::Span)>,
+}
+
+impl<'a> HasFieldMap for SessionSubdiagnosticDeriveBuilder<'a> {
+ fn get_field_binding(&self, field: &String) -> Option<&TokenStream> {
+ self.fields.get(field)
+ }
+}
+
+impl<'a> SessionSubdiagnosticDeriveBuilder<'a> {
+ fn identify_kind(&mut self) -> Result<(), DiagnosticDeriveError> {
+ for attr in self.variant.ast().attrs {
+ let span = attr.span().unwrap();
+
+ let name = attr.path.segments.last().unwrap().ident.to_string();
+ let name = name.as_str();
+
+ let meta = attr.parse_meta()?;
+ let kind = match meta {
+ Meta::List(MetaList { ref nested, .. }) => {
+ let mut nested_iter = nested.into_iter();
+ if let Some(nested_attr) = nested_iter.next() {
+ match nested_attr {
+ NestedMeta::Meta(Meta::Path(path)) => {
+ self.slug.set_once((path.clone(), span));
+ }
+ NestedMeta::Meta(meta @ Meta::NameValue(_))
+ if matches!(
+ meta.path().segments.last().unwrap().ident.to_string().as_str(),
+ "code" | "applicability"
+ ) =>
+ {
+ // don't error for valid follow-up attributes
+ }
+ nested_attr => {
+ throw_invalid_nested_attr!(attr, &nested_attr, |diag| {
+ diag.help(
+ "first argument of the attribute should be the diagnostic \
+ slug",
+ )
+ })
+ }
+ };
+ }
+
+ for nested_attr in nested_iter {
+ let meta = match nested_attr {
+ NestedMeta::Meta(ref meta) => meta,
+ _ => throw_invalid_nested_attr!(attr, &nested_attr),
+ };
+
+ let span = meta.span().unwrap();
+ let nested_name = meta.path().segments.last().unwrap().ident.to_string();
+ let nested_name = nested_name.as_str();
+
+ match meta {
+ Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(s), .. }) => {
+ match nested_name {
+ "code" => {
+ let formatted_str = self.build_format(&s.value(), s.span());
+ self.code.set_once((formatted_str, span));
+ }
+ "applicability" => {
+ let value = match Applicability::from_str(&s.value()) {
+ Ok(v) => v,
+ Err(()) => {
+ span_err(span, "invalid applicability").emit();
+ Applicability::Unspecified
+ }
+ };
+ self.applicability.set_once((quote! { #value }, span));
+ }
+ _ => throw_invalid_nested_attr!(attr, &nested_attr, |diag| {
+ diag.help(
+ "only `code` and `applicability` are valid nested \
+ attributes",
+ )
+ }),
+ }
+ }
+ _ => throw_invalid_nested_attr!(attr, &nested_attr, |diag| {
+ if matches!(meta, Meta::Path(_)) {
+ diag.help(
+ "a diagnostic slug must be the first argument to the \
+ attribute",
+ )
+ } else {
+ diag
+ }
+ }),
+ }
+ }
+
+ let Ok(kind) = SubdiagnosticKind::from_str(name) else {
+ throw_invalid_attr!(attr, &meta)
+ };
+
+ kind
+ }
+ _ => throw_invalid_attr!(attr, &meta),
+ };
+
+ if matches!(
+ kind,
+ SubdiagnosticKind::Label | SubdiagnosticKind::Help | SubdiagnosticKind::Note
+ ) && self.code.is_some()
+ {
+ throw_span_err!(
+ span,
+ &format!("`code` is not a valid nested attribute of a `{}` attribute", name)
+ );
+ }
+
+ if matches!(
+ kind,
+ SubdiagnosticKind::Label | SubdiagnosticKind::Help | SubdiagnosticKind::Note
+ ) && self.applicability.is_some()
+ {
+ throw_span_err!(
+ span,
+ &format!(
+ "`applicability` is not a valid nested attribute of a `{}` attribute",
+ name
+ )
+ );
+ }
+
+ if self.slug.is_none() {
+ throw_span_err!(
+ span,
+ &format!(
+ "diagnostic slug must be first argument of a `#[{}(...)]` attribute",
+ name
+ )
+ );
+ }
+
+ self.kind.set_once((kind, span));
+ }
+
+ Ok(())
+ }
+
+ fn generate_field_code(
+ &mut self,
+ binding: &BindingInfo<'_>,
+ is_suggestion: bool,
+ ) -> Result<TokenStream, DiagnosticDeriveError> {
+ let ast = binding.ast();
+
+ let inner_ty = FieldInnerTy::from_type(&ast.ty);
+ let info = FieldInfo {
+ binding: binding,
+ ty: inner_ty.inner_type().unwrap_or(&ast.ty),
+ span: &ast.span(),
+ };
+
+ for attr in &ast.attrs {
+ let name = attr.path.segments.last().unwrap().ident.to_string();
+ let name = name.as_str();
+ let span = attr.span().unwrap();
+
+ let meta = attr.parse_meta()?;
+ match meta {
+ Meta::Path(_) => match name {
+ "primary_span" => {
+ report_error_if_not_applied_to_span(attr, &info)?;
+ self.span_field.set_once((binding.binding.clone(), span));
+ return Ok(quote! {});
+ }
+ "applicability" if is_suggestion => {
+ report_error_if_not_applied_to_applicability(attr, &info)?;
+ let binding = binding.binding.clone();
+ self.applicability.set_once((quote! { #binding }, span));
+ return Ok(quote! {});
+ }
+ "applicability" => {
+ span_err(span, "`#[applicability]` is only valid on suggestions").emit();
+ return Ok(quote! {});
+ }
+ "skip_arg" => {
+ return Ok(quote! {});
+ }
+ _ => throw_invalid_attr!(attr, &meta, |diag| {
+ diag.help(
+ "only `primary_span`, `applicability` and `skip_arg` are valid field \
+ attributes",
+ )
+ }),
+ },
+ _ => throw_invalid_attr!(attr, &meta),
+ }
+ }
+
+ let ident = ast.ident.as_ref().unwrap();
+
+ let diag = &self.diag;
+ let generated = quote! {
+ #diag.set_arg(
+ stringify!(#ident),
+ #binding
+ );
+ };
+
+ Ok(inner_ty.with(binding, generated))
+ }
+
+ fn into_tokens(&mut self) -> Result<TokenStream, DiagnosticDeriveError> {
+ self.identify_kind()?;
+ let Some(kind) = self.kind.map(|(kind, _)| kind) else {
+ throw_span_err!(
+ self.variant.ast().ident.span().unwrap(),
+ "subdiagnostic kind not specified"
+ );
+ };
+
+ let is_suggestion = matches!(kind, SubdiagnosticKind::Suggestion(_));
+
+ let mut args = TokenStream::new();
+ for binding in self.variant.bindings() {
+ let arg = self
+ .generate_field_code(binding, is_suggestion)
+ .unwrap_or_else(|v| v.to_compile_error());
+ args.extend(arg);
+ }
+
+ // Missing slug errors will already have been reported.
+ let slug = self
+ .slug
+ .as_ref()
+ .map(|(slug, _)| slug.clone())
+ .unwrap_or_else(|| parse_quote! { you::need::to::specify::a::slug });
+ let code = match self.code.as_ref() {
+ Some((code, _)) => Some(quote! { #code }),
+ None if is_suggestion => {
+ span_err(self.span, "suggestion without `code = \"...\"`").emit();
+ Some(quote! { /* macro error */ "..." })
+ }
+ None => None,
+ };
+
+ let span_field = self.span_field.as_ref().map(|(span, _)| span);
+ let applicability = match self.applicability.clone() {
+ Some((applicability, _)) => Some(applicability),
+ None if is_suggestion => {
+ span_err(self.span, "suggestion without `applicability`").emit();
+ Some(quote! { rustc_errors::Applicability::Unspecified })
+ }
+ None => None,
+ };
+
+ let diag = &self.diag;
+ let name = format_ident!("{}{}", if span_field.is_some() { "span_" } else { "" }, kind);
+ let message = quote! { rustc_errors::fluent::#slug };
+ let call = if matches!(kind, SubdiagnosticKind::Suggestion(..)) {
+ if let Some(span) = span_field {
+ quote! { #diag.#name(#span, #message, #code, #applicability); }
+ } else {
+ span_err(self.span, "suggestion without `#[primary_span]` field").emit();
+ quote! { unreachable!(); }
+ }
+ } else if matches!(kind, SubdiagnosticKind::Label) {
+ if let Some(span) = span_field {
+ quote! { #diag.#name(#span, #message); }
+ } else {
+ span_err(self.span, "label without `#[primary_span]` field").emit();
+ quote! { unreachable!(); }
+ }
+ } else {
+ if let Some(span) = span_field {
+ quote! { #diag.#name(#span, #message); }
+ } else {
+ quote! { #diag.#name(#message); }
+ }
+ };
+
+ Ok(quote! {
+ #call
+ #args
+ })
+ }
+}