diff options
Diffstat (limited to 'compiler/rustc_macros/src/diagnostics')
-rw-r--r-- | compiler/rustc_macros/src/diagnostics/diagnostic.rs | 197 | ||||
-rw-r--r-- | compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs | 743 | ||||
-rw-r--r-- | compiler/rustc_macros/src/diagnostics/fluent.rs | 65 | ||||
-rw-r--r-- | compiler/rustc_macros/src/diagnostics/mod.rs | 36 | ||||
-rw-r--r-- | compiler/rustc_macros/src/diagnostics/subdiagnostic.rs | 492 | ||||
-rw-r--r-- | compiler/rustc_macros/src/diagnostics/utils.rs | 421 |
6 files changed, 1057 insertions, 897 deletions
diff --git a/compiler/rustc_macros/src/diagnostics/diagnostic.rs b/compiler/rustc_macros/src/diagnostics/diagnostic.rs index cf1c59455..ef1985b96 100644 --- a/compiler/rustc_macros/src/diagnostics/diagnostic.rs +++ b/compiler/rustc_macros/src/diagnostics/diagnostic.rs @@ -2,94 +2,77 @@ use crate::diagnostics::diagnostic_builder::{DiagnosticDeriveBuilder, DiagnosticDeriveKind}; use crate::diagnostics::error::{span_err, DiagnosticDeriveError}; -use crate::diagnostics::utils::{build_field_mapping, SetOnce}; +use crate::diagnostics::utils::SetOnce; use proc_macro2::TokenStream; use quote::quote; -use syn::spanned::Spanned; use synstructure::Structure; /// The central struct for constructing the `into_diagnostic` method from an annotated struct. -pub(crate) struct SessionDiagnosticDerive<'a> { +pub(crate) struct DiagnosticDerive<'a> { structure: Structure<'a>, - sess: syn::Ident, builder: DiagnosticDeriveBuilder, } -impl<'a> SessionDiagnosticDerive<'a> { - pub(crate) fn new(diag: syn::Ident, sess: syn::Ident, structure: Structure<'a>) -> Self { +impl<'a> DiagnosticDerive<'a> { + pub(crate) fn new(diag: syn::Ident, handler: syn::Ident, structure: Structure<'a>) -> Self { Self { builder: DiagnosticDeriveBuilder { diag, - fields: build_field_mapping(&structure), - kind: DiagnosticDeriveKind::SessionDiagnostic, - code: None, - slug: None, + kind: DiagnosticDeriveKind::Diagnostic { handler }, }, - sess, structure, } } pub(crate) fn into_tokens(self) -> TokenStream { - let SessionDiagnosticDerive { mut structure, sess, mut builder } = self; - - let ast = structure.ast(); - let implementation = { - if let syn::Data::Struct(..) = ast.data { - let preamble = builder.preamble(&structure); - let (attrs, args) = builder.body(&mut structure); - - let span = ast.span().unwrap(); - let diag = &builder.diag; - let init = match builder.slug.value() { - None => { - span_err(span, "diagnostic slug not specified") - .help(&format!( - "specify the slug as the first argument to the `#[diag(...)]` attribute, \ - such as `#[diag(typeck::example_error)]`", - )) - .emit(); - return DiagnosticDeriveError::ErrorHandled.to_compile_error(); - } - Some(slug) => { - quote! { - let mut #diag = #sess.struct_diagnostic(rustc_errors::fluent::#slug); - } - } - }; - - quote! { - #init - #preamble - match self { - #attrs - } - match self { - #args + let DiagnosticDerive { mut structure, mut builder } = self; + + let implementation = builder.each_variant(&mut structure, |mut builder, variant| { + let preamble = builder.preamble(&variant); + let body = builder.body(&variant); + + let diag = &builder.parent.diag; + let DiagnosticDeriveKind::Diagnostic { handler } = &builder.parent.kind else { + unreachable!() + }; + let init = match builder.slug.value_ref() { + None => { + span_err(builder.span, "diagnostic slug not specified") + .help(&format!( + "specify the slug as the first argument to the `#[diag(...)]` \ + attribute, such as `#[diag(hir_analysis_example_error)]`", + )) + .emit(); + return DiagnosticDeriveError::ErrorHandled.to_compile_error(); + } + Some(slug) => { + quote! { + let mut #diag = #handler.struct_diagnostic(rustc_errors::fluent::#slug); } - #diag } - } else { - span_err( - ast.span().unwrap(), - "`#[derive(SessionDiagnostic)]` can only be used on structs", - ) - .emit(); - - DiagnosticDeriveError::ErrorHandled.to_compile_error() + }; + + let formatting_init = &builder.formatting_init; + quote! { + #init + #formatting_init + #preamble + #body + #diag } - }; + }); + let DiagnosticDeriveKind::Diagnostic { handler } = &builder.kind else { unreachable!() }; structure.gen_impl(quote! { - gen impl<'__session_diagnostic_sess, G> - rustc_session::SessionDiagnostic<'__session_diagnostic_sess, G> + gen impl<'__diagnostic_handler_sess, G> + rustc_errors::IntoDiagnostic<'__diagnostic_handler_sess, G> for @Self where G: rustc_errors::EmissionGuarantee { fn into_diagnostic( self, - #sess: &'__session_diagnostic_sess rustc_errors::Handler - ) -> rustc_errors::DiagnosticBuilder<'__session_diagnostic_sess, G> { + #handler: &'__diagnostic_handler_sess rustc_errors::Handler + ) -> rustc_errors::DiagnosticBuilder<'__diagnostic_handler_sess, G> { use rustc_errors::IntoDiagnosticArg; #implementation } @@ -107,13 +90,7 @@ pub(crate) struct LintDiagnosticDerive<'a> { impl<'a> LintDiagnosticDerive<'a> { pub(crate) fn new(diag: syn::Ident, structure: Structure<'a>) -> Self { Self { - builder: DiagnosticDeriveBuilder { - diag, - fields: build_field_mapping(&structure), - kind: DiagnosticDeriveKind::LintDiagnostic, - code: None, - slug: None, - }, + builder: DiagnosticDeriveBuilder { diag, kind: DiagnosticDeriveKind::LintDiagnostic }, structure, } } @@ -121,62 +98,52 @@ impl<'a> LintDiagnosticDerive<'a> { pub(crate) fn into_tokens(self) -> TokenStream { let LintDiagnosticDerive { mut structure, mut builder } = self; - let ast = structure.ast(); - let implementation = { - if let syn::Data::Struct(..) = ast.data { - let preamble = builder.preamble(&structure); - let (attrs, args) = builder.body(&mut structure); - - let diag = &builder.diag; - let span = ast.span().unwrap(); - let init = match builder.slug.value() { - None => { - span_err(span, "diagnostic slug not specified") - .help(&format!( - "specify the slug as the first argument to the attribute, such as \ - `#[diag(typeck::example_error)]`", - )) - .emit(); - return DiagnosticDeriveError::ErrorHandled.to_compile_error(); - } - Some(slug) => { - quote! { - let mut #diag = #diag.build(rustc_errors::fluent::#slug); - } - } - }; - - let implementation = quote! { - #init - #preamble - match self { - #attrs - } - match self { - #args - } - #diag.emit(); - }; - - implementation - } else { - span_err( - ast.span().unwrap(), - "`#[derive(LintDiagnostic)]` can only be used on structs", - ) - .emit(); - - DiagnosticDeriveError::ErrorHandled.to_compile_error() + let implementation = builder.each_variant(&mut structure, |mut builder, variant| { + let preamble = builder.preamble(&variant); + let body = builder.body(&variant); + + let diag = &builder.parent.diag; + let formatting_init = &builder.formatting_init; + quote! { + #preamble + #formatting_init + #body + #diag } - }; + }); + + let msg = builder.each_variant(&mut structure, |mut builder, variant| { + // Collect the slug by generating the preamble. + let _ = builder.preamble(&variant); + + match builder.slug.value_ref() { + None => { + span_err(builder.span, "diagnostic slug not specified") + .help(&format!( + "specify the slug as the first argument to the attribute, such as \ + `#[diag(compiletest_example)]`", + )) + .emit(); + return DiagnosticDeriveError::ErrorHandled.to_compile_error(); + } + Some(slug) => quote! { rustc_errors::fluent::#slug.into() }, + } + }); let diag = &builder.diag; structure.gen_impl(quote! { gen impl<'__a> rustc_errors::DecorateLint<'__a, ()> for @Self { - fn decorate_lint(self, #diag: rustc_errors::LintDiagnosticBuilder<'__a, ()>) { + fn decorate_lint<'__b>( + self, + #diag: &'__b mut rustc_errors::DiagnosticBuilder<'__a, ()> + ) -> &'__b mut rustc_errors::DiagnosticBuilder<'__a, ()> { use rustc_errors::IntoDiagnosticArg; #implementation } + + fn msg(&self) -> rustc_errors::DiagnosticMessage { + #msg + } } }) } diff --git a/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs b/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs index 2a4fe48a8..3ea83fd09 100644 --- a/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs +++ b/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs @@ -5,56 +5,124 @@ use crate::diagnostics::error::{ DiagnosticDeriveError, }; use crate::diagnostics::utils::{ - report_error_if_not_applied_to_span, report_type_error, type_is_unit, type_matches_path, - Applicability, FieldInfo, FieldInnerTy, HasFieldMap, SetOnce, + build_field_mapping, is_doc_comment, report_error_if_not_applied_to_span, report_type_error, + should_generate_set_arg, type_is_unit, type_matches_path, FieldInfo, FieldInnerTy, FieldMap, + HasFieldMap, SetOnce, SpannedOption, SubdiagnosticKind, }; use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote}; -use std::collections::HashMap; -use std::str::FromStr; use syn::{ - parse_quote, spanned::Spanned, Attribute, Field, Meta, MetaList, MetaNameValue, NestedMeta, - Path, Type, + parse_quote, spanned::Spanned, Attribute, Meta, MetaList, MetaNameValue, NestedMeta, Path, Type, }; -use synstructure::{BindingInfo, Structure}; +use synstructure::{BindingInfo, Structure, VariantInfo}; /// What kind of diagnostic is being derived - a fatal/error/warning or a lint? -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] pub(crate) enum DiagnosticDeriveKind { - SessionDiagnostic, + Diagnostic { handler: syn::Ident }, LintDiagnostic, } -/// Tracks persistent information required for building up individual calls to diagnostic methods -/// for generated diagnostic derives - both `SessionDiagnostic` for fatal/errors/warnings and -/// `LintDiagnostic` for lints. +/// Tracks persistent information required for the entire type when building up individual calls to +/// diagnostic methods for generated diagnostic derives - both `Diagnostic` for +/// fatal/errors/warnings and `LintDiagnostic` for lints. pub(crate) struct DiagnosticDeriveBuilder { /// The identifier to use for the generated `DiagnosticBuilder` instance. pub diag: syn::Ident, + /// Kind of diagnostic that should be derived. + pub kind: DiagnosticDeriveKind, +} + +/// Tracks persistent information required for a specific variant when building up individual calls +/// to diagnostic methods for generated diagnostic derives - both `Diagnostic` for +/// fatal/errors/warnings and `LintDiagnostic` for lints. +pub(crate) struct DiagnosticDeriveVariantBuilder<'parent> { + /// The parent builder for the entire type. + pub parent: &'parent DiagnosticDeriveBuilder, + + /// Initialization of format strings for code suggestions. + pub formatting_init: TokenStream, + + /// Span of the struct or the enum variant. + pub span: proc_macro::Span, /// Store a map of field name to its corresponding field. This is built on construction of the /// derive builder. - pub fields: HashMap<String, TokenStream>, + pub field_map: FieldMap, - /// Kind of diagnostic that should be derived. - pub kind: DiagnosticDeriveKind, /// Slug is a mandatory part of the struct attribute as corresponds to the Fluent message that /// has the actual diagnostic message. - pub slug: Option<(Path, proc_macro::Span)>, + pub slug: SpannedOption<Path>, /// Error codes are a optional part of the struct attribute - this is only set to detect /// multiple specifications. - pub code: Option<(String, proc_macro::Span)>, + pub code: SpannedOption<()>, } -impl HasFieldMap for DiagnosticDeriveBuilder { +impl<'a> HasFieldMap for DiagnosticDeriveVariantBuilder<'a> { fn get_field_binding(&self, field: &String) -> Option<&TokenStream> { - self.fields.get(field) + self.field_map.get(field) } } impl DiagnosticDeriveBuilder { - pub fn preamble<'s>(&mut self, structure: &Structure<'s>) -> TokenStream { + /// Call `f` for the struct or for each variant of the enum, returning a `TokenStream` with the + /// tokens from `f` wrapped in an `match` expression. Emits errors for use of derive on unions + /// or attributes on the type itself when input is an enum. + pub fn each_variant<'s, F>(&mut self, structure: &mut Structure<'s>, f: F) -> TokenStream + where + F: for<'a, 'v> Fn(DiagnosticDeriveVariantBuilder<'a>, &VariantInfo<'v>) -> TokenStream, + { let ast = structure.ast(); + let span = ast.span().unwrap(); + match ast.data { + syn::Data::Struct(..) | syn::Data::Enum(..) => (), + syn::Data::Union(..) => { + span_err(span, "diagnostic derives 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 diagnostic derive enum", + ) + .emit(); + } + } + + structure.bind_with(|_| synstructure::BindStyle::Move); + let variants = structure.each_variant(|variant| { + let span = match structure.ast().data { + syn::Data::Struct(..) => span, + // There isn't a good way to get the span of the variant, so the variant's + // name will need to do. + _ => variant.ast().ident.span().unwrap(), + }; + let builder = DiagnosticDeriveVariantBuilder { + parent: &self, + span, + field_map: build_field_mapping(variant), + formatting_init: TokenStream::new(), + slug: None, + code: None, + }; + f(builder, variant) + }); + + quote! { + match self { + #variants + } + } + } +} + +impl<'a> DiagnosticDeriveVariantBuilder<'a> { + /// Generates calls to `code` and similar functions based on the attributes on the type or + /// variant. + pub fn preamble<'s>(&mut self, variant: &VariantInfo<'s>) -> TokenStream { + let ast = variant.ast(); let attrs = &ast.attrs; let preamble = attrs.iter().map(|attr| { self.generate_structure_code_for_attr(attr).unwrap_or_else(|v| v.to_compile_error()) @@ -65,66 +133,48 @@ impl DiagnosticDeriveBuilder { } } - pub fn body<'s>(&mut self, structure: &mut Structure<'s>) -> (TokenStream, TokenStream) { - // Keep track of which fields need to be handled with a by-move binding. - let mut needs_moved = std::collections::HashSet::new(); - - // Generates calls to `span_label` and similar functions based on the attributes - // on fields. Code for suggestions uses formatting machinery and the value of - // other fields - because any given field can be referenced multiple times, it - // should be accessed through a borrow. When passing fields to `add_subdiagnostic` - // or `set_arg` (which happens below) for Fluent, we want to move the data, so that - // has to happen in a separate pass over the fields. - let attrs = structure - .clone() - .filter(|field_binding| { - let ast = &field_binding.ast(); - !self.needs_move(ast) || { - needs_moved.insert(field_binding.binding.clone()); - false - } - }) - .each(|field_binding| self.generate_field_attrs_code(field_binding)); - - structure.bind_with(|_| synstructure::BindStyle::Move); - // When a field has attributes like `#[label]` or `#[note]` then it doesn't - // need to be passed as an argument to the diagnostic. But when a field has no - // attributes or a `#[subdiagnostic]` attribute then it must be passed as an - // argument to the diagnostic so that it can be referred to by Fluent messages. - let args = structure - .filter(|field_binding| needs_moved.contains(&field_binding.binding)) - .each(|field_binding| self.generate_field_attrs_code(field_binding)); - - (attrs, args) + /// Generates calls to `span_label` and similar functions based on the attributes on fields or + /// calls to `set_arg` when no attributes are present. + pub fn body<'s>(&mut self, variant: &VariantInfo<'s>) -> TokenStream { + let mut body = quote! {}; + // Generate `set_arg` calls first.. + for binding in variant.bindings().iter().filter(|bi| should_generate_set_arg(bi.ast())) { + body.extend(self.generate_field_code(binding)); + } + // ..and then subdiagnostic additions. + for binding in variant.bindings().iter().filter(|bi| !should_generate_set_arg(bi.ast())) { + body.extend(self.generate_field_attrs_code(binding)); + } + body } - /// Returns `true` if `field` should generate a `set_arg` call rather than any other diagnostic - /// call (like `span_label`). - fn should_generate_set_arg(&self, field: &Field) -> bool { - field.attrs.is_empty() - } + /// Parse a `SubdiagnosticKind` from an `Attribute`. + fn parse_subdiag_attribute( + &self, + attr: &Attribute, + ) -> Result<Option<(SubdiagnosticKind, Path)>, DiagnosticDeriveError> { + let Some((subdiag, slug)) = SubdiagnosticKind::from_attr(attr, self)? else { + // Some attributes aren't errors - like documentation comments - but also aren't + // subdiagnostics. + return Ok(None); + }; - /// Returns `true` if `field` needs to have code generated in the by-move branch of the - /// generated derive rather than the by-ref branch. - fn needs_move(&self, field: &Field) -> bool { - let generates_set_arg = self.should_generate_set_arg(field); - let is_multispan = type_matches_path(&field.ty, &["rustc_errors", "MultiSpan"]); - // FIXME(davidtwco): better support for one field needing to be in the by-move and - // by-ref branches. - let is_subdiagnostic = field - .attrs - .iter() - .map(|attr| attr.path.segments.last().unwrap().ident.to_string()) - .any(|attr| attr == "subdiagnostic"); - - // `set_arg` calls take their argument by-move.. - generates_set_arg - // If this is a `MultiSpan` field then it needs to be moved to be used by any - // attribute.. - || is_multispan - // If this a `#[subdiagnostic]` then it needs to be moved as the other diagnostic is - // unlikely to be `Copy`.. - || is_subdiagnostic + if let SubdiagnosticKind::MultipartSuggestion { .. } = subdiag { + let meta = attr.parse_meta()?; + throw_invalid_attr!(attr, &meta, |diag| diag + .help("consider creating a `Subdiagnostic` instead")); + } + + let slug = slug.unwrap_or_else(|| match subdiag { + SubdiagnosticKind::Label => parse_quote! { _subdiag::label }, + SubdiagnosticKind::Note => parse_quote! { _subdiag::note }, + SubdiagnosticKind::Help => parse_quote! { _subdiag::help }, + SubdiagnosticKind::Warn => parse_quote! { _subdiag::warn }, + SubdiagnosticKind::Suggestion { .. } => parse_quote! { _subdiag::suggestion }, + SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(), + }); + + Ok(Some((subdiag, slug))) } /// Establishes state in the `DiagnosticDeriveBuilder` resulting from the struct @@ -134,99 +184,70 @@ impl DiagnosticDeriveBuilder { &mut self, attr: &Attribute, ) -> Result<TokenStream, DiagnosticDeriveError> { - let diag = &self.diag; - let span = attr.span().unwrap(); + let diag = &self.parent.diag; + + // Always allow documentation comments. + if is_doc_comment(attr) { + return Ok(quote! {}); + } let name = attr.path.segments.last().unwrap().ident.to_string(); let name = name.as_str(); let meta = attr.parse_meta()?; - let is_diag = name == "diag"; - - let nested = match meta { - // Most attributes are lists, like `#[diag(..)]` for most cases or - // `#[help(..)]`/`#[note(..)]` when the user is specifying a alternative slug. - Meta::List(MetaList { ref nested, .. }) => nested, - // Subdiagnostics without spans can be applied to the type too, and these are just - // paths: `#[help]`, `#[note]` and `#[warning]` - Meta::Path(_) if !is_diag => { - let fn_name = if name == "warning" { - Ident::new("warn", attr.span()) - } else { - Ident::new(name, attr.span()) - }; - return Ok(quote! { #diag.#fn_name(rustc_errors::fluent::_subdiag::#fn_name); }); - } - _ => throw_invalid_attr!(attr, &meta), - }; - - // Check the kind before doing any further processing so that there aren't misleading - // "no kind specified" errors if there are failures later. - match name { - "error" | "lint" => throw_invalid_attr!(attr, &meta, |diag| { - diag.help("`error` and `lint` have been replaced by `diag`") - }), - "warn_" => throw_invalid_attr!(attr, &meta, |diag| { - diag.help("`warn_` have been replaced by `warning`") - }), - "diag" | "help" | "note" | "warning" => (), - _ => throw_invalid_attr!(attr, &meta, |diag| { - diag.help("only `diag`, `help`, `note` and `warning` are valid attributes") - }), - } + if name == "diag" { + let Meta::List(MetaList { ref nested, .. }) = meta else { + throw_invalid_attr!( + attr, + &meta + ); + }; - // First nested element should always be the path, e.g. `#[diag(typeck::invalid)]` or - // `#[help(typeck::another_help)]`. - let mut nested_iter = nested.into_iter(); - if let Some(nested_attr) = nested_iter.next() { - // Report an error if there are any other list items after the path. - if !is_diag && nested_iter.next().is_some() { - throw_invalid_nested_attr!(attr, &nested_attr, |diag| { - diag.help( - "`help`, `note` and `warning` struct attributes can only have one argument", - ) - }); - } + let mut nested_iter = nested.into_iter().peekable(); - match nested_attr { - NestedMeta::Meta(Meta::Path(path)) => { - if is_diag { - self.slug.set_once((path.clone(), span)); - } else { - let fn_name = proc_macro2::Ident::new(name, attr.span()); - return Ok(quote! { #diag.#fn_name(rustc_errors::fluent::#path); }); - } - } - NestedMeta::Meta(meta @ Meta::NameValue(_)) - if is_diag && meta.path().segments.last().unwrap().ident == "code" => - { - // don't error for valid follow-up attributes + match nested_iter.peek() { + Some(NestedMeta::Meta(Meta::Path(slug))) => { + self.slug.set_once(slug.clone(), slug.span().unwrap()); + nested_iter.next(); } - nested_attr => throw_invalid_nested_attr!(attr, &nested_attr, |diag| { - diag.help("first argument of the attribute should be the diagnostic slug") - }), + Some(NestedMeta::Meta(Meta::NameValue { .. })) => {} + Some(nested_attr) => throw_invalid_nested_attr!(attr, &nested_attr, |diag| diag + .help("a diagnostic slug is required as the first argument")), + None => throw_invalid_attr!(attr, &meta, |diag| diag + .help("a diagnostic slug is required as the first argument")), }; - } - // Remaining attributes are optional, only `code = ".."` at the moment. - let mut tokens = Vec::new(); - for nested_attr in nested_iter { - let meta = match nested_attr { - syn::NestedMeta::Meta(meta) => meta, - _ => throw_invalid_nested_attr!(attr, &nested_attr), - }; + // Remaining attributes are optional, only `code = ".."` at the moment. + let mut tokens = TokenStream::new(); + for nested_attr in nested_iter { + let (value, path) = match nested_attr { + NestedMeta::Meta(Meta::NameValue(MetaNameValue { + lit: syn::Lit::Str(value), + path, + .. + })) => (value, path), + NestedMeta::Meta(Meta::Path(_)) => { + invalid_nested_attr(attr, &nested_attr) + .help("diagnostic slug must be the first argument") + .emit(); + continue; + } + _ => { + invalid_nested_attr(attr, &nested_attr).emit(); + continue; + } + }; - let path = meta.path(); - let nested_name = path.segments.last().unwrap().ident.to_string(); - // Struct attributes are only allowed to be applied once, and the diagnostic - // changes will be set in the initialisation code. - if let Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(s), .. }) = &meta { - let span = s.span().unwrap(); + let nested_name = path.segments.last().unwrap().ident.to_string(); + // Struct attributes are only allowed to be applied once, and the diagnostic + // changes will be set in the initialisation code. + let span = value.span().unwrap(); match nested_name.as_str() { "code" => { - self.code.set_once((s.value(), span)); - let code = &self.code.as_ref().map(|(v, _)| v); - tokens.push(quote! { + self.code.set_once((), span); + + let code = value.value(); + tokens.extend(quote! { #diag.code(rustc_errors::DiagnosticId::Error(#code.to_string())); }); } @@ -234,46 +255,68 @@ impl DiagnosticDeriveBuilder { .help("only `code` is a valid nested attributes following the slug") .emit(), } - } else { - invalid_nested_attr(attr, &nested_attr).emit() } + return Ok(tokens); } - Ok(tokens.into_iter().collect()) + let Some((subdiag, slug)) = self.parse_subdiag_attribute(attr)? else { + // Some attributes aren't errors - like documentation comments - but also aren't + // subdiagnostics. + return Ok(quote! {}); + }; + let fn_ident = format_ident!("{}", subdiag); + match subdiag { + SubdiagnosticKind::Note | SubdiagnosticKind::Help | SubdiagnosticKind::Warn => { + Ok(self.add_subdiagnostic(&fn_ident, slug)) + } + SubdiagnosticKind::Label | SubdiagnosticKind::Suggestion { .. } => { + throw_invalid_attr!(attr, &meta, |diag| diag + .help("`#[label]` and `#[suggestion]` can only be applied to fields")); + } + SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(), + } } - fn generate_field_attrs_code(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream { + fn generate_field_code(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream { + let diag = &self.parent.diag; + let field = binding_info.ast(); let field_binding = &binding_info.binding; - if self.should_generate_set_arg(&field) { - let diag = &self.diag; - let ident = field.ident.as_ref().unwrap(); - return quote! { - #diag.set_arg( - stringify!(#ident), - #field_binding - ); - }; + let ident = field.ident.as_ref().unwrap(); + let ident = format_ident!("{}", ident); // strip `r#` prefix, if present + + quote! { + #diag.set_arg( + stringify!(#ident), + #field_binding + ); } + } + + fn generate_field_attrs_code(&mut self, binding_info: &BindingInfo<'_>) -> TokenStream { + let field = binding_info.ast(); + let field_binding = &binding_info.binding; - let needs_move = self.needs_move(&field); let inner_ty = FieldInnerTy::from_type(&field.ty); field .attrs .iter() .map(move |attr| { + // Always allow documentation comments. + if is_doc_comment(attr) { + return quote! {}; + } + let name = attr.path.segments.last().unwrap().ident.to_string(); let needs_clone = name == "primary_span" && matches!(inner_ty, FieldInnerTy::Vec(_)); let (binding, needs_destructure) = if needs_clone { // `primary_span` can accept a `Vec<Span>` so don't destructure that. (quote! { #field_binding.clone() }, false) - } else if needs_move { - (quote! { #field_binding }, true) } else { - (quote! { *#field_binding }, true) + (quote! { #field_binding }, true) }; let generated_code = self @@ -303,42 +346,23 @@ impl DiagnosticDeriveBuilder { info: FieldInfo<'_>, binding: TokenStream, ) -> Result<TokenStream, DiagnosticDeriveError> { - let meta = attr.parse_meta()?; - match meta { - Meta::Path(_) => self.generate_inner_field_code_path(attr, info, binding), - Meta::List(MetaList { .. }) => self.generate_inner_field_code_list(attr, info, binding), - _ => throw_invalid_attr!(attr, &meta), - } - } - - fn generate_inner_field_code_path( - &mut self, - attr: &Attribute, - info: FieldInfo<'_>, - binding: TokenStream, - ) -> Result<TokenStream, DiagnosticDeriveError> { - assert!(matches!(attr.parse_meta()?, Meta::Path(_))); - let diag = &self.diag; - + let diag = &self.parent.diag; let meta = attr.parse_meta()?; let ident = &attr.path.segments.last().unwrap().ident; let name = ident.to_string(); - let name = name.as_str(); - match name { - "skip_arg" => { - // Don't need to do anything - by virtue of the attribute existing, the - // `set_arg` call will not be generated. - Ok(quote! {}) - } - "primary_span" => { - match self.kind { - DiagnosticDeriveKind::SessionDiagnostic => { + match (&meta, name.as_str()) { + // Don't need to do anything - by virtue of the attribute existing, the + // `set_arg` call will not be generated. + (Meta::Path(_), "skip_arg") => return Ok(quote! {}), + (Meta::Path(_), "primary_span") => { + match self.parent.kind { + DiagnosticDeriveKind::Diagnostic { .. } => { report_error_if_not_applied_to_span(attr, &info)?; - Ok(quote! { + return Ok(quote! { #diag.set_span(#binding); - }) + }); } DiagnosticDeriveKind::LintDiagnostic => { throw_invalid_attr!(attr, &meta, |diag| { @@ -347,188 +371,100 @@ impl DiagnosticDeriveBuilder { } } } - "label" => { - report_error_if_not_applied_to_span(attr, &info)?; - Ok(self.add_spanned_subdiagnostic(binding, ident, parse_quote! { _subdiag::label })) + (Meta::Path(_), "subdiagnostic") => { + return Ok(quote! { #diag.subdiagnostic(#binding); }); } - "note" | "help" | "warning" => { - let warn_ident = Ident::new("warn", Span::call_site()); - let (ident, path) = match name { - "note" => (ident, parse_quote! { _subdiag::note }), - "help" => (ident, parse_quote! { _subdiag::help }), - "warning" => (&warn_ident, parse_quote! { _subdiag::warn }), - _ => unreachable!(), - }; - if type_matches_path(&info.ty, &["rustc_span", "Span"]) { - Ok(self.add_spanned_subdiagnostic(binding, ident, path)) - } else if type_is_unit(&info.ty) { - Ok(self.add_subdiagnostic(ident, path)) - } else { - report_type_error(attr, "`Span` or `()`")? - } + (Meta::NameValue(_), "subdiagnostic") => { + throw_invalid_attr!(attr, &meta, |diag| { + diag.help("`eager` is the only supported nested attribute for `subdiagnostic`") + }) } - "subdiagnostic" => Ok(quote! { #diag.subdiagnostic(#binding); }), - _ => throw_invalid_attr!(attr, &meta, |diag| { - diag.help( - "only `skip_arg`, `primary_span`, `label`, `note`, `help` and `subdiagnostic` \ - are valid field attributes", - ) - }), - } - } + (Meta::List(MetaList { ref nested, .. }), "subdiagnostic") => { + if nested.len() != 1 { + throw_invalid_attr!(attr, &meta, |diag| { + diag.help( + "`eager` is the only supported nested attribute for `subdiagnostic`", + ) + }) + } - fn generate_inner_field_code_list( - &mut self, - attr: &Attribute, - info: FieldInfo<'_>, - binding: TokenStream, - ) -> Result<TokenStream, DiagnosticDeriveError> { - let meta = attr.parse_meta()?; - let Meta::List(MetaList { ref path, ref nested, .. }) = meta else { unreachable!() }; + let handler = match &self.parent.kind { + DiagnosticDeriveKind::Diagnostic { handler } => handler, + DiagnosticDeriveKind::LintDiagnostic => { + throw_invalid_attr!(attr, &meta, |diag| { + diag.help("eager subdiagnostics are not supported on lints") + }) + } + }; - let ident = &attr.path.segments.last().unwrap().ident; - let name = path.segments.last().unwrap().ident.to_string(); - let name = name.as_ref(); - match name { - "suggestion" | "suggestion_short" | "suggestion_hidden" | "suggestion_verbose" => { - return self.generate_inner_field_code_suggestion(attr, info); + let nested_attr = nested.first().expect("pop failed for single element list"); + match nested_attr { + NestedMeta::Meta(meta @ Meta::Path(_)) + if meta.path().segments.last().unwrap().ident.to_string().as_str() + == "eager" => + { + return Ok(quote! { #diag.eager_subdiagnostic(#handler, #binding); }); + } + _ => { + throw_invalid_nested_attr!(attr, nested_attr, |diag| { + diag.help("`eager` is the only supported nested attribute for `subdiagnostic`") + }) + } + } } - "label" | "help" | "note" | "warning" => (), - _ => throw_invalid_attr!(attr, &meta, |diag| { - diag.help( - "only `label`, `help`, `note`, `warn` or `suggestion{,_short,_hidden,_verbose}` are \ - valid field attributes", - ) - }), + _ => (), } - // For `#[label(..)]`, `#[note(..)]` and `#[help(..)]`, the first nested element must be a - // path, e.g. `#[label(typeck::label)]`. - let mut nested_iter = nested.into_iter(); - let msg = match nested_iter.next() { - Some(NestedMeta::Meta(Meta::Path(path))) => path.clone(), - Some(nested_attr) => throw_invalid_nested_attr!(attr, &nested_attr), - None => throw_invalid_attr!(attr, &meta), + let Some((subdiag, slug)) = self.parse_subdiag_attribute(attr)? else { + // Some attributes aren't errors - like documentation comments - but also aren't + // subdiagnostics. + return Ok(quote! {}); }; - - // None of these attributes should have anything following the slug. - if nested_iter.next().is_some() { - throw_invalid_attr!(attr, &meta); - } - - match name { - "label" => { + let fn_ident = format_ident!("{}", subdiag); + match subdiag { + SubdiagnosticKind::Label => { report_error_if_not_applied_to_span(attr, &info)?; - Ok(self.add_spanned_subdiagnostic(binding, ident, msg)) + Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug)) } - "note" | "help" if type_matches_path(&info.ty, &["rustc_span", "Span"]) => { - Ok(self.add_spanned_subdiagnostic(binding, ident, msg)) - } - "note" | "help" if type_is_unit(&info.ty) => Ok(self.add_subdiagnostic(ident, msg)), - // `warning` must be special-cased because the attribute `warn` already has meaning and - // so isn't used, despite the diagnostic API being named `warn`. - "warning" if type_matches_path(&info.ty, &["rustc_span", "Span"]) => Ok(self - .add_spanned_subdiagnostic(binding, &Ident::new("warn", Span::call_site()), msg)), - "warning" if type_is_unit(&info.ty) => { - Ok(self.add_subdiagnostic(&Ident::new("warn", Span::call_site()), msg)) - } - "note" | "help" | "warning" => report_type_error(attr, "`Span` or `()`")?, - _ => unreachable!(), - } - } - - fn generate_inner_field_code_suggestion( - &mut self, - attr: &Attribute, - info: FieldInfo<'_>, - ) -> Result<TokenStream, DiagnosticDeriveError> { - let diag = &self.diag; - - let mut meta = attr.parse_meta()?; - let Meta::List(MetaList { ref path, ref mut nested, .. }) = meta else { unreachable!() }; - - let (span_field, mut applicability) = self.span_and_applicability_of_ty(info)?; - - let mut msg = None; - let mut code = None; - - let mut nested_iter = nested.into_iter().peekable(); - if let Some(nested_attr) = nested_iter.peek() { - if let NestedMeta::Meta(Meta::Path(path)) = nested_attr { - msg = Some(path.clone()); + SubdiagnosticKind::Note | SubdiagnosticKind::Help | SubdiagnosticKind::Warn => { + if type_matches_path(&info.ty, &["rustc_span", "Span"]) { + Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug)) + } else if type_is_unit(&info.ty) { + Ok(self.add_subdiagnostic(&fn_ident, slug)) + } else { + report_type_error(attr, "`Span` or `()`")? + } } - }; - // Move the iterator forward if a path was found (don't otherwise so that - // code/applicability can be found or an error emitted). - if msg.is_some() { - let _ = nested_iter.next(); - } - - for nested_attr in nested_iter { - let meta = match nested_attr { - syn::NestedMeta::Meta(ref meta) => meta, - syn::NestedMeta::Lit(_) => throw_invalid_nested_attr!(attr, &nested_attr), - }; - - 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), .. }) => { - let span = meta.span().unwrap(); - match nested_name { - "code" => { - let formatted_str = self.build_format(&s.value(), s.span()); - code = Some(formatted_str); - } - "applicability" => { - applicability = match applicability { - Some(v) => { - span_err( - span, - "applicability cannot be set in both the field and \ - attribute", - ) - .emit(); - Some(v) - } - None => match Applicability::from_str(&s.value()) { - Ok(v) => Some(quote! { #v }), - Err(()) => { - span_err(span, "invalid applicability").emit(); - None - } - }, - } - } - _ => throw_invalid_nested_attr!(attr, &nested_attr, |diag| { - diag.help( - "only `message`, `code` and `applicability` are valid field \ - attributes", - ) - }), - } + SubdiagnosticKind::Suggestion { + suggestion_kind, + applicability: static_applicability, + code_field, + code_init, + } => { + let (span_field, mut applicability) = self.span_and_applicability_of_ty(info)?; + + if let Some((static_applicability, span)) = static_applicability { + applicability.set_once(quote! { #static_applicability }, span); } - _ => 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 applicability = applicability + .value() + .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified }); + let style = suggestion_kind.to_suggestion_style(); + + self.formatting_init.extend(code_init); + Ok(quote! { + #diag.span_suggestions_with_style( + #span_field, + rustc_errors::fluent::#slug, + #code_field, + #applicability, + #style + ); + }) } + SubdiagnosticKind::MultipartSuggestion { .. } => unreachable!(), } - - let applicability = - applicability.unwrap_or_else(|| quote!(rustc_errors::Applicability::Unspecified)); - - let name = path.segments.last().unwrap().ident.to_string(); - let method = format_ident!("span_{}", name); - - let msg = msg.unwrap_or_else(|| parse_quote! { _subdiag::suggestion }); - let msg = quote! { rustc_errors::fluent::#msg }; - let code = code.unwrap_or_else(|| quote! { String::new() }); - - Ok(quote! { #diag.#method(#span_field, #msg, #code, #applicability); }) } /// Adds a spanned subdiagnostic by generating a `diag.span_$kind` call with the current slug @@ -539,7 +475,7 @@ impl DiagnosticDeriveBuilder { kind: &Ident, fluent_attr_identifier: Path, ) -> TokenStream { - let diag = &self.diag; + let diag = &self.parent.diag; let fn_name = format_ident!("span_{}", kind); quote! { #diag.#fn_name( @@ -552,7 +488,7 @@ impl DiagnosticDeriveBuilder { /// Adds a subdiagnostic by generating a `diag.span_$kind` call with the current slug /// and `fluent_attr_identifier`. fn add_subdiagnostic(&self, kind: &Ident, fluent_attr_identifier: Path) -> TokenStream { - let diag = &self.diag; + let diag = &self.parent.diag; quote! { #diag.#kind(rustc_errors::fluent::#fluent_attr_identifier); } @@ -561,58 +497,49 @@ impl DiagnosticDeriveBuilder { fn span_and_applicability_of_ty( &self, info: FieldInfo<'_>, - ) -> Result<(TokenStream, Option<TokenStream>), DiagnosticDeriveError> { + ) -> Result<(TokenStream, SpannedOption<TokenStream>), DiagnosticDeriveError> { match &info.ty { // If `ty` is `Span` w/out applicability, then use `Applicability::Unspecified`. ty @ Type::Path(..) if type_matches_path(ty, &["rustc_span", "Span"]) => { let binding = &info.binding.binding; - Ok((quote!(*#binding), None)) + Ok((quote!(#binding), None)) } // If `ty` is `(Span, Applicability)` then return tokens accessing those. Type::Tuple(tup) => { let mut span_idx = None; let mut applicability_idx = None; + fn type_err(span: &Span) -> Result<!, DiagnosticDeriveError> { + span_err(span.unwrap(), "wrong types for suggestion") + .help( + "`#[suggestion(...)]` on a tuple field must be applied to fields \ + of type `(Span, Applicability)`", + ) + .emit(); + Err(DiagnosticDeriveError::ErrorHandled) + } + for (idx, elem) in tup.elems.iter().enumerate() { if type_matches_path(elem, &["rustc_span", "Span"]) { - if span_idx.is_none() { - span_idx = Some(syn::Index::from(idx)); - } else { - throw_span_err!( - info.span.unwrap(), - "type of field annotated with `#[suggestion(...)]` contains more \ - than one `Span`" - ); - } + span_idx.set_once(syn::Index::from(idx), elem.span().unwrap()); } else if type_matches_path(elem, &["rustc_errors", "Applicability"]) { - if applicability_idx.is_none() { - applicability_idx = Some(syn::Index::from(idx)); - } else { - throw_span_err!( - info.span.unwrap(), - "type of field annotated with `#[suggestion(...)]` contains more \ - than one Applicability" - ); - } + applicability_idx.set_once(syn::Index::from(idx), elem.span().unwrap()); + } else { + type_err(&elem.span())?; } } - if let Some(span_idx) = span_idx { - let binding = &info.binding.binding; - let span = quote!(#binding.#span_idx); - let applicability = applicability_idx - .map(|applicability_idx| quote!(#binding.#applicability_idx)) - .unwrap_or_else(|| quote!(rustc_errors::Applicability::Unspecified)); - - return Ok((span, Some(applicability))); - } + let Some((span_idx, _)) = span_idx else { + type_err(&tup.span())?; + }; + let Some((applicability_idx, applicability_span)) = applicability_idx else { + type_err(&tup.span())?; + }; + let binding = &info.binding.binding; + let span = quote!(#binding.#span_idx); + let applicability = quote!(#binding.#applicability_idx); - throw_span_err!(info.span.unwrap(), "wrong types for suggestion", |diag| { - diag.help( - "`#[suggestion(...)]` on a tuple field must be applied to fields of type \ - `(Span, Applicability)`", - ) - }); + Ok((span, Some((applicability, applicability_span)))) } // If `ty` isn't a `Span` or `(Span, Applicability)` then emit an error. _ => throw_span_err!(info.span.unwrap(), "wrong field type for suggestion", |diag| { diff --git a/compiler/rustc_macros/src/diagnostics/fluent.rs b/compiler/rustc_macros/src/diagnostics/fluent.rs index f7d8b494e..3e447c94e 100644 --- a/compiler/rustc_macros/src/diagnostics/fluent.rs +++ b/compiler/rustc_macros/src/diagnostics/fluent.rs @@ -25,18 +25,18 @@ use syn::{ use unic_langid::langid; struct Resource { - ident: Ident, + krate: Ident, #[allow(dead_code)] fat_arrow_token: token::FatArrow, - resource: LitStr, + resource_path: LitStr, } impl Parse for Resource { fn parse(input: ParseStream<'_>) -> Result<Self> { Ok(Resource { - ident: input.parse()?, + krate: input.parse()?, fat_arrow_token: input.parse()?, - resource: input.parse()?, + resource_path: input.parse()?, }) } } @@ -94,19 +94,20 @@ pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::Tok // diagnostics. let mut previous_defns = HashMap::new(); + // Set of Fluent attribute names already output, to avoid duplicate type errors - any given + // constant created for a given attribute is the same. + let mut previous_attrs = HashSet::new(); + let mut includes = TokenStream::new(); let mut generated = TokenStream::new(); - for res in resources.0 { - let ident_span = res.ident.span().unwrap(); - let path_span = res.resource.span().unwrap(); - // Set of Fluent attribute names already output, to avoid duplicate type errors - any given - // constant created for a given attribute is the same. - let mut previous_attrs = HashSet::new(); + for res in resources.0 { + let krate_span = res.krate.span().unwrap(); + let path_span = res.resource_path.span().unwrap(); - let relative_ftl_path = res.resource.value(); + let relative_ftl_path = res.resource_path.value(); let absolute_ftl_path = - invocation_relative_path_to_absolute(ident_span, &relative_ftl_path); + invocation_relative_path_to_absolute(krate_span, &relative_ftl_path); // As this macro also outputs an `include_str!` for this file, the macro will always be // re-executed when the file changes. let mut resource_file = match File::open(absolute_ftl_path) { @@ -185,7 +186,7 @@ pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::Tok let mut constants = TokenStream::new(); for entry in resource.entries() { - let span = res.ident.span(); + let span = res.krate.span(); if let Entry::Message(Message { id: Identifier { name }, attributes, .. }) = entry { let _ = previous_defns.entry(name.to_string()).or_insert(path_span); @@ -199,29 +200,30 @@ pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::Tok .emit(); } - // `typeck_foo_bar` => `foo_bar` (in `typeck.ftl`) - // `const_eval_baz` => `baz` (in `const_eval.ftl`) + // Require that the message name starts with the crate name + // `hir_typeck_foo_bar` (in `hir_typeck.ftl`) + // `const_eval_baz` (in `const_eval.ftl`) // `const-eval-hyphen-having` => `hyphen_having` (in `const_eval.ftl`) // The last case we error about above, but we want to fall back gracefully // so that only the error is being emitted and not also one about the macro // failing. - let crate_prefix = format!("{}_", res.ident); + let crate_prefix = format!("{}_", res.krate); let snake_name = name.replace('-', "_"); - let snake_name = match snake_name.strip_prefix(&crate_prefix) { - Some(rest) => Ident::new(rest, span), - None => { - Diagnostic::spanned( - path_span, - Level::Error, - format!("name `{name}` does not start with the crate name"), - ) - .help(format!("prepend `{crate_prefix}` to the slug name: `{crate_prefix}{snake_name}`")) - .emit(); - Ident::new(&snake_name, span) - } + if !snake_name.starts_with(&crate_prefix) { + Diagnostic::spanned( + path_span, + Level::Error, + format!("name `{name}` does not start with the crate name"), + ) + .help(format!( + "prepend `{crate_prefix}` to the slug name: `{crate_prefix}{snake_name}`" + )) + .emit(); }; + let snake_name = Ident::new(&snake_name, span); + constants.extend(quote! { pub const #snake_name: crate::DiagnosticMessage = crate::DiagnosticMessage::FluentIdentifier( @@ -275,12 +277,7 @@ pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::Tok includes.extend(quote! { include_str!(#relative_ftl_path), }); - let ident = res.ident; - generated.extend(quote! { - pub mod #ident { - #constants - } - }); + generated.extend(constants); } quote! { diff --git a/compiler/rustc_macros/src/diagnostics/mod.rs b/compiler/rustc_macros/src/diagnostics/mod.rs index 2ff21e18f..860340b43 100644 --- a/compiler/rustc_macros/src/diagnostics/mod.rs +++ b/compiler/rustc_macros/src/diagnostics/mod.rs @@ -5,14 +5,14 @@ mod fluent; mod subdiagnostic; mod utils; -use diagnostic::{LintDiagnosticDerive, SessionDiagnosticDerive}; +use diagnostic::{DiagnosticDerive, LintDiagnosticDerive}; pub(crate) use fluent::fluent_messages; use proc_macro2::TokenStream; use quote::format_ident; -use subdiagnostic::SessionSubdiagnosticDerive; +use subdiagnostic::SubdiagnosticDeriveBuilder; use synstructure::Structure; -/// Implements `#[derive(SessionDiagnostic)]`, which allows for errors to be specified as a struct, +/// Implements `#[derive(Diagnostic)]`, which allows for errors to be specified as a struct, /// independent from the actual diagnostics emitting code. /// /// ```ignore (rust) @@ -22,15 +22,15 @@ use synstructure::Structure; /// # use rustc_span::{symbol::Ident, Span}; /// # extern crate rust_middle; /// # use rustc_middle::ty::Ty; -/// #[derive(SessionDiagnostic)] -/// #[diag(borrowck::move_out_of_borrow, code = "E0505")] +/// #[derive(Diagnostic)] +/// #[diag(borrowck_move_out_of_borrow, code = "E0505")] /// pub struct MoveOutOfBorrowError<'tcx> { /// pub name: Ident, /// pub ty: Ty<'tcx>, /// #[primary_span] /// #[label] /// pub span: Span, -/// #[label(borrowck::first_borrow_label)] +/// #[label(first_borrow_label)] /// pub first_borrow_span: Span, /// #[suggestion(code = "{name}.clone()")] /// pub clone_sugg: Option<(Span, Applicability)> @@ -56,10 +56,10 @@ use synstructure::Structure; /// }); /// ``` /// -/// See rustc dev guide for more examples on using the `#[derive(SessionDiagnostic)]`: +/// See rustc dev guide for more examples on using the `#[derive(Diagnostic)]`: /// <https://rustc-dev-guide.rust-lang.org/diagnostics/diagnostic-structs.html> pub fn session_diagnostic_derive(s: Structure<'_>) -> TokenStream { - SessionDiagnosticDerive::new(format_ident!("diag"), format_ident!("sess"), s).into_tokens() + DiagnosticDerive::new(format_ident!("diag"), format_ident!("handler"), s).into_tokens() } /// Implements `#[derive(LintDiagnostic)]`, which allows for lints to be specified as a struct, @@ -67,14 +67,14 @@ pub fn session_diagnostic_derive(s: Structure<'_>) -> TokenStream { /// /// ```ignore (rust) /// #[derive(LintDiagnostic)] -/// #[diag(lint::atomic_ordering_invalid_fail_success)] +/// #[diag(lint_atomic_ordering_invalid_fail_success)] /// pub struct AtomicOrderingInvalidLint { /// method: Symbol, /// success_ordering: Symbol, /// fail_ordering: Symbol, -/// #[label(lint::fail_label)] +/// #[label(fail_label)] /// fail_order_arg_span: Span, -/// #[label(lint::success_label)] +/// #[label(success_label)] /// #[suggestion( /// code = "std::sync::atomic::Ordering::{success_suggestion}", /// applicability = "maybe-incorrect" @@ -103,24 +103,24 @@ pub fn session_diagnostic_derive(s: Structure<'_>) -> TokenStream { /// ``` /// /// See rustc dev guide for more examples on using the `#[derive(LintDiagnostic)]`: -/// <https://rustc-dev-guide.rust-lang.org/diagnostics/sessiondiagnostic.html> +/// <https://rustc-dev-guide.rust-lang.org/diagnostics/diagnostic-structs.html#reference> pub fn lint_diagnostic_derive(s: Structure<'_>) -> TokenStream { LintDiagnosticDerive::new(format_ident!("diag"), s).into_tokens() } -/// Implements `#[derive(SessionSubdiagnostic)]`, which allows for labels, notes, helps and +/// Implements `#[derive(Subdiagnostic)]`, which allows for labels, notes, helps and /// suggestions to be specified as a structs or enums, independent from the actual diagnostics /// emitting code or diagnostic derives. /// /// ```ignore (rust) -/// #[derive(SessionSubdiagnostic)] +/// #[derive(Subdiagnostic)] /// pub enum ExpectedIdentifierLabel<'tcx> { -/// #[label(parser::expected_identifier)] +/// #[label(expected_identifier)] /// WithoutFound { /// #[primary_span] /// span: Span, /// } -/// #[label(parser::expected_identifier_found)] +/// #[label(expected_identifier_found)] /// WithFound { /// #[primary_span] /// span: Span, @@ -128,7 +128,7 @@ pub fn lint_diagnostic_derive(s: Structure<'_>) -> TokenStream { /// } /// } /// -/// #[derive(SessionSubdiagnostic)] +/// #[derive(Subdiagnostic)] /// #[suggestion_verbose(parser::raw_identifier)] /// pub struct RawIdentifierSuggestion<'tcx> { /// #[primary_span] @@ -155,5 +155,5 @@ pub fn lint_diagnostic_derive(s: Structure<'_>) -> TokenStream { /// diag.subdiagnostic(RawIdentifierSuggestion { span, applicability, ident }); /// ``` pub fn session_subdiagnostic_derive(s: Structure<'_>) -> TokenStream { - SessionSubdiagnosticDerive::new(s).into_tokens() + SubdiagnosticDeriveBuilder::new().into_tokens(s) } diff --git a/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs b/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs index dce5d3cfb..fa0ca5a52 100644 --- a/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs +++ b/compiler/rustc_macros/src/diagnostics/subdiagnostic.rs @@ -1,116 +1,35 @@ #![deny(unused_must_use)] use crate::diagnostics::error::{ - span_err, throw_invalid_attr, throw_invalid_nested_attr, throw_span_err, DiagnosticDeriveError, + invalid_attr, 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, + build_field_mapping, is_doc_comment, new_code_ident, + report_error_if_not_applied_to_applicability, report_error_if_not_applied_to_span, FieldInfo, + FieldInnerTy, FieldMap, HasFieldMap, SetOnce, SpannedOption, SubdiagnosticKind, }; use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use std::collections::HashMap; -use std::fmt; -use std::str::FromStr; -use syn::{spanned::Spanned, Attribute, Meta, MetaList, MetaNameValue, NestedMeta, Path}; +use syn::{spanned::Spanned, Attribute, Meta, MetaList, 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, -} - -impl FromStr for SubdiagnosticSuggestionKind { - type Err = (); - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s { - "" => Ok(SubdiagnosticSuggestionKind::Normal), - "_short" => Ok(SubdiagnosticSuggestionKind::Short), - "_hidden" => Ok(SubdiagnosticSuggestionKind::Hidden), - "_verbose" => Ok(SubdiagnosticSuggestionKind::Verbose), - _ => Err(()), - } - } -} - -impl SubdiagnosticSuggestionKind { - pub fn to_suggestion_style(&self) -> TokenStream { - match self { - SubdiagnosticSuggestionKind::Normal => { - quote! { rustc_errors::SuggestionStyle::ShowCode } - } - SubdiagnosticSuggestionKind::Short => { - quote! { rustc_errors::SuggestionStyle::HideCodeInline } - } - SubdiagnosticSuggestionKind::Hidden => { - quote! { rustc_errors::SuggestionStyle::HideCodeAlways } - } - SubdiagnosticSuggestionKind::Verbose => { - quote! { rustc_errors::SuggestionStyle::ShowAlways } - } - } - } -} - -/// Which kind of subdiagnostic is being created from a variant? -#[derive(Clone)] -enum SubdiagnosticKind { - /// `#[label(...)]` - Label, - /// `#[note(...)]` - Note, - /// `#[help(...)]` - Help, - /// `#[warning(...)]` - Warn, - /// `#[suggestion{,_short,_hidden,_verbose}]` - Suggestion { suggestion_kind: SubdiagnosticSuggestionKind, code: TokenStream }, - /// `#[multipart_suggestion{,_short,_hidden,_verbose}]` - MultipartSuggestion { suggestion_kind: SubdiagnosticSuggestionKind }, -} - -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 { .. } => write!(f, "suggestion_with_style"), - SubdiagnosticKind::MultipartSuggestion { .. } => { - write!(f, "multipart_suggestion_with_style") - } - } - } - - fn span(&self) -> Option<proc_macro2::Span> { - None - } -} +use super::utils::{build_suggestion_code, AllowMultipleAlternatives}; /// The central struct for constructing the `add_to_diagnostic` method from an annotated struct. -pub(crate) struct SessionSubdiagnosticDerive<'a> { - structure: Structure<'a>, +pub(crate) struct SubdiagnosticDeriveBuilder { diag: syn::Ident, + f: syn::Ident, } -impl<'a> SessionSubdiagnosticDerive<'a> { - pub(crate) fn new(structure: Structure<'a>) -> Self { +impl SubdiagnosticDeriveBuilder { + pub(crate) fn new() -> Self { let diag = format_ident!("diag"); - Self { structure, diag } + let f = format_ident!("f"); + Self { diag, f } } - pub(crate) fn into_tokens(self) -> TokenStream { - let SessionSubdiagnosticDerive { mut structure, diag } = self; + pub(crate) fn into_tokens<'a>(self, mut structure: Structure<'a>) -> TokenStream { let implementation = { let ast = structure.ast(); let span = ast.span().unwrap(); @@ -119,13 +38,19 @@ impl<'a> SessionSubdiagnosticDerive<'a> { syn::Data::Union(..) => { span_err( span, - "`#[derive(SessionSubdiagnostic)]` can only be used on structs and enums", + "`#[derive(Subdiagnostic)]` can only be used on structs and enums", ); } } - if matches!(ast.data, syn::Data::Enum(..)) { + let is_enum = matches!(ast.data, syn::Data::Enum(..)); + if is_enum { for attr in &ast.attrs { + // Always allow documentation comments. + if is_doc_comment(attr) { + continue; + } + span_err( attr.span().unwrap(), "unsupported type attribute for subdiagnostic enum", @@ -136,24 +61,16 @@ impl<'a> SessionSubdiagnosticDerive<'a> { 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, + let mut builder = SubdiagnosticDeriveVariantBuilder { + parent: &self, variant, span, - fields: fields_map, + formatting_init: TokenStream::new(), + fields: build_field_mapping(variant), span_field: None, applicability: None, has_suggestion_parts: false, + is_enum, }; builder.into_tokens().unwrap_or_else(|v| v.to_compile_error()) }); @@ -165,9 +82,17 @@ impl<'a> SessionSubdiagnosticDerive<'a> { } }; + let diag = &self.diag; + let f = &self.f; let ret = structure.gen_impl(quote! { - gen impl rustc_errors::AddSubdiagnostic for @Self { - fn add_to_diagnostic(self, #diag: &mut rustc_errors::Diagnostic) { + gen impl rustc_errors::AddToDiagnostic for @Self { + fn add_to_diagnostic_with<__F>(self, #diag: &mut rustc_errors::Diagnostic, #f: __F) + where + __F: core::ops::Fn( + &mut rustc_errors::Diagnostic, + rustc_errors::SubdiagnosticMessage + ) -> rustc_errors::SubdiagnosticMessage, + { use rustc_errors::{Applicability, IntoDiagnosticArg}; #implementation } @@ -178,34 +103,40 @@ impl<'a> SessionSubdiagnosticDerive<'a> { } /// 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` +/// for the final generated method. This is a separate struct to `SubdiagnosticDerive` /// 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> { +struct SubdiagnosticDeriveVariantBuilder<'parent, 'a> { /// The identifier to use for the generated `DiagnosticBuilder` instance. - diag: &'a syn::Ident, + parent: &'parent SubdiagnosticDeriveBuilder, /// 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, + /// Initialization of format strings for code suggestions. + formatting_init: TokenStream, + /// Store a map of field name to its corresponding field. This is built on construction of the /// derive builder. - fields: HashMap<String, TokenStream>, + fields: FieldMap, /// 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)>, + span_field: SpannedOption<proc_macro2::Ident>, + + /// The binding to the `#[applicability]` field, if present. + applicability: SpannedOption<TokenStream>, /// Set to true when a `#[suggestion_part]` field is encountered, used to generate an error /// during finalization if still `false`. has_suggestion_parts: bool, + + /// Set to true when this variant is an enum variant rather than just the body of a struct. + is_enum: bool, } -impl<'a> HasFieldMap for SessionSubdiagnosticDeriveBuilder<'a> { +impl<'parent, 'a> HasFieldMap for SubdiagnosticDeriveVariantBuilder<'parent, 'a> { fn get_field_binding(&self, field: &String) -> Option<&TokenStream> { self.fields.get(field) } @@ -217,6 +148,7 @@ struct KindsStatistics { has_multipart_suggestion: bool, all_multipart_suggestions: bool, has_normal_suggestion: bool, + all_applicabilities_static: bool, } impl<'a> FromIterator<&'a SubdiagnosticKind> for KindsStatistics { @@ -225,8 +157,15 @@ impl<'a> FromIterator<&'a SubdiagnosticKind> for KindsStatistics { has_multipart_suggestion: false, all_multipart_suggestions: true, has_normal_suggestion: false, + all_applicabilities_static: true, }; + for kind in kinds { + if let SubdiagnosticKind::MultipartSuggestion { applicability: None, .. } + | SubdiagnosticKind::Suggestion { applicability: None, .. } = kind + { + ret.all_applicabilities_static = false; + } if let SubdiagnosticKind::MultipartSuggestion { .. } = kind { ret.has_multipart_suggestion = true; } else { @@ -241,134 +180,23 @@ impl<'a> FromIterator<&'a SubdiagnosticKind> for KindsStatistics { } } -impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { +impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> { fn identify_kind(&mut self) -> Result<Vec<(SubdiagnosticKind, Path)>, DiagnosticDeriveError> { let mut kind_slugs = vec![]; 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 Meta::List(MetaList { ref nested, .. }) = meta else { - throw_invalid_attr!(attr, &meta); + let Some((kind, slug)) = SubdiagnosticKind::from_attr(attr, self)? else { + // Some attributes aren't errors - like documentation comments - but also aren't + // subdiagnostics. + continue; }; - let mut kind = match name { - "label" => SubdiagnosticKind::Label, - "note" => SubdiagnosticKind::Note, - "help" => SubdiagnosticKind::Help, - "warning" => SubdiagnosticKind::Warn, - _ => { - if let Some(suggestion_kind) = - name.strip_prefix("suggestion").and_then(|s| s.parse().ok()) - { - SubdiagnosticKind::Suggestion { suggestion_kind, code: TokenStream::new() } - } else if let Some(suggestion_kind) = - name.strip_prefix("multipart_suggestion").and_then(|s| s.parse().ok()) - { - SubdiagnosticKind::MultipartSuggestion { suggestion_kind } - } else { - throw_invalid_attr!(attr, &meta); - } - } - }; - - let mut slug = None; - let mut code = None; - - let mut nested_iter = nested.into_iter(); - if let Some(nested_attr) = nested_iter.next() { - match nested_attr { - NestedMeta::Meta(Meta::Path(path)) => { - 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(); - - let value = match meta { - Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(value), .. }) => value, - Meta::Path(_) => throw_invalid_nested_attr!(attr, &nested_attr, |diag| { - diag.help("a diagnostic slug must be the first argument to the attribute") - }), - _ => throw_invalid_nested_attr!(attr, &nested_attr), - }; - - match nested_name { - "code" => { - if matches!(kind, SubdiagnosticKind::Suggestion { .. }) { - let formatted_str = self.build_format(&value.value(), value.span()); - code.set_once((formatted_str, span)); - } else { - span_err( - span, - &format!( - "`code` is not a valid nested attribute of a `{}` attribute", - name - ), - ) - .emit(); - } - } - "applicability" => { - if matches!( - kind, - SubdiagnosticKind::Suggestion { .. } - | SubdiagnosticKind::MultipartSuggestion { .. } - ) { - let value = - Applicability::from_str(&value.value()).unwrap_or_else(|()| { - span_err(span, "invalid applicability").emit(); - Applicability::Unspecified - }); - self.applicability.set_once((quote! { #value }, span)); - } else { - span_err( - span, - &format!( - "`applicability` is not a valid nested attribute of a `{}` attribute", - name - ) - ).emit(); - } - } - _ => throw_invalid_nested_attr!(attr, &nested_attr, |diag| { - diag.help("only `code` and `applicability` are valid nested attributes") - }), - } - } + let Some(slug) = slug else { + let name = attr.path.segments.last().unwrap().ident.to_string(); + let name = name.as_str(); - let Some((slug, _)) = slug else { throw_span_err!( - span, + attr.span().unwrap(), &format!( "diagnostic slug must be first argument of a `#[{}(...)]` attribute", name @@ -376,21 +204,7 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { ); }; - match kind { - SubdiagnosticKind::Suggestion { code: ref mut code_field, .. } => { - let Some((code, _)) = code else { - throw_span_err!(span, "suggestion without `code = \"...\"`"); - }; - *code_field = code; - } - SubdiagnosticKind::Label - | SubdiagnosticKind::Note - | SubdiagnosticKind::Help - | SubdiagnosticKind::Warn - | SubdiagnosticKind::MultipartSuggestion { .. } => {} - } - - kind_slugs.push((kind, slug)) + kind_slugs.push((kind, slug)); } Ok(kind_slugs) @@ -401,8 +215,11 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { let ast = binding.ast(); assert_eq!(ast.attrs.len(), 0, "field with attribute used as diagnostic arg"); - let diag = &self.diag; + let diag = &self.parent.diag; let ident = ast.ident.as_ref().unwrap(); + // strip `r#` prefix, if present + let ident = format_ident!("{}", ident); + quote! { #diag.set_arg( stringify!(#ident), @@ -426,6 +243,11 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { ast.attrs .iter() .map(|attr| { + // Always allow documentation comments. + if is_doc_comment(attr) { + return quote! {}; + } + let info = FieldInfo { binding, ty: inner_ty.inner_type().unwrap_or(&ast.ty), @@ -433,7 +255,7 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { }; let generated = self - .generate_field_code_inner(kind_stats, attr, info) + .generate_field_code_inner(kind_stats, attr, info, inner_ty.will_iterate()) .unwrap_or_else(|v| v.to_compile_error()); inner_ty.with(binding, generated) @@ -446,13 +268,18 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { kind_stats: KindsStatistics, attr: &Attribute, info: FieldInfo<'_>, + clone_suggestion_code: bool, ) -> Result<TokenStream, DiagnosticDeriveError> { let meta = attr.parse_meta()?; match meta { Meta::Path(path) => self.generate_field_code_inner_path(kind_stats, attr, info, path), - Meta::List(list @ MetaList { .. }) => { - self.generate_field_code_inner_list(kind_stats, attr, info, list) - } + Meta::List(list @ MetaList { .. }) => self.generate_field_code_inner_list( + kind_stats, + attr, + info, + list, + clone_suggestion_code, + ), _ => throw_invalid_attr!(attr, &meta), } } @@ -474,18 +301,20 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { "skip_arg" => Ok(quote! {}), "primary_span" => { if kind_stats.has_multipart_suggestion { - throw_invalid_attr!(attr, &Meta::Path(path), |diag| { - diag.help( + invalid_attr(attr, &Meta::Path(path)) + .help( "multipart suggestions use one or more `#[suggestion_part]`s rather \ than one `#[primary_span]`", ) - }) - } - - report_error_if_not_applied_to_span(attr, &info)?; + .emit(); + } else { + report_error_if_not_applied_to_span(attr, &info)?; - let binding = info.binding.binding.clone(); - self.span_field.set_once((binding, span)); + let binding = info.binding.binding.clone(); + // FIXME(#100717): support `Option<Span>` on `primary_span` like in the + // diagnostic derive + self.span_field.set_once(binding, span); + } Ok(quote! {}) } @@ -495,28 +324,39 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { if kind_stats.has_multipart_suggestion { span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`") .emit(); - Ok(quote! {}) } else { - throw_invalid_attr!(attr, &Meta::Path(path), |diag| { - diag.help( - "`#[suggestion_part(...)]` is only valid in multipart suggestions, use `#[primary_span]` instead", - ) - }); + invalid_attr(attr, &Meta::Path(path)) + .help( + "`#[suggestion_part(...)]` is only valid in multipart suggestions, \ + use `#[primary_span]` instead", + ) + .emit(); } + + Ok(quote! {}) } "applicability" => { if kind_stats.has_multipart_suggestion || kind_stats.has_normal_suggestion { report_error_if_not_applied_to_applicability(attr, &info)?; + if kind_stats.all_applicabilities_static { + span_err( + span, + "`#[applicability]` has no effect if all `#[suggestion]`/\ + `#[multipart_suggestion]` attributes have a static \ + `applicability = \"...\"`", + ) + .emit(); + } let binding = info.binding.binding.clone(); - self.applicability.set_once((quote! { #binding }, span)); + self.applicability.set_once(quote! { #binding }, span); } else { span_err(span, "`#[applicability]` is only valid on suggestions").emit(); } Ok(quote! {}) } - _ => throw_invalid_attr!(attr, &Meta::Path(path), |diag| { + _ => { let mut span_attrs = vec![]; if kind_stats.has_multipart_suggestion { span_attrs.push("suggestion_part"); @@ -524,11 +364,16 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { if !kind_stats.all_multipart_suggestions { span_attrs.push("primary_span") } - diag.help(format!( - "only `{}`, `applicability` and `skip_arg` are valid field attributes", - span_attrs.join(", ") - )) - }), + + invalid_attr(attr, &Meta::Path(path)) + .help(format!( + "only `{}`, `applicability` and `skip_arg` are valid field attributes", + span_attrs.join(", ") + )) + .emit(); + + Ok(quote! {}) + } } } @@ -540,6 +385,7 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { attr: &Attribute, info: FieldInfo<'_>, list: MetaList, + clone_suggestion_code: bool, ) -> Result<TokenStream, DiagnosticDeriveError> { let span = attr.span().unwrap(); let ident = &list.path.segments.last().unwrap().ident; @@ -570,14 +416,16 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'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()); - code.set_once((formatted_str, span)); + let code_field = new_code_ident(); + 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") @@ -585,14 +433,20 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { } } - let Some((code, _)) = code 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; - Ok(quote! { suggestions.push((#binding, #code)); }) + self.formatting_init.extend(formatting_init); + let code_field = if clone_suggestion_code { + quote! { #code_field.clone() } + } else { + quote! { #code_field } + }; + Ok(quote! { suggestions.push((#binding, #code_field)); }) } _ => throw_invalid_attr!(attr, &Meta::List(list), |diag| { let mut span_attrs = vec![]; @@ -613,10 +467,16 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { pub fn into_tokens(&mut self) -> Result<TokenStream, DiagnosticDeriveError> { let kind_slugs = self.identify_kind()?; if kind_slugs.is_empty() { - throw_span_err!( - self.variant.ast().ident.span().unwrap(), - "subdiagnostic kind not specified" - ); + if self.is_enum { + // It's okay for a variant to not be a subdiagnostic at all.. + return Ok(quote! {}); + } else { + // ..but structs should always be _something_. + throw_span_err!( + self.variant.ast().ident.span().unwrap(), + "subdiagnostic kind not specified" + ); + } }; let kind_stats: KindsStatistics = kind_slugs.iter().map(|(kind, _slug)| kind).collect(); @@ -635,29 +495,46 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { .map(|binding| self.generate_field_attr_code(binding, kind_stats)) .collect(); - let span_field = self.span_field.as_ref().map(|(span, _)| span); - let applicability = self.applicability.take().map_or_else( - || quote! { rustc_errors::Applicability::Unspecified }, - |(applicability, _)| applicability, - ); + let span_field = self.span_field.value_ref(); - let diag = &self.diag; + let diag = &self.parent.diag; + let f = &self.parent.f; let mut calls = TokenStream::new(); for (kind, slug) in kind_slugs { + let message = format_ident!("__message"); + calls.extend(quote! { let #message = #f(#diag, rustc_errors::fluent::#slug.into()); }); + let name = format_ident!("{}{}", if span_field.is_some() { "span_" } else { "" }, kind); - let message = quote! { rustc_errors::fluent::#slug }; let call = match kind { - SubdiagnosticKind::Suggestion { suggestion_kind, code } => { + SubdiagnosticKind::Suggestion { + suggestion_kind, + applicability, + code_init, + code_field, + } => { + self.formatting_init.extend(code_init); + + let applicability = applicability + .value() + .map(|a| quote! { #a }) + .or_else(|| self.applicability.take().value()) + .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified }); + if let Some(span) = span_field { let style = suggestion_kind.to_suggestion_style(); - - quote! { #diag.#name(#span, #message, #code, #applicability, #style); } + quote! { #diag.#name(#span, #message, #code_field, #applicability, #style); } } else { span_err(self.span, "suggestion without `#[primary_span]` field").emit(); quote! { unreachable!(); } } } - SubdiagnosticKind::MultipartSuggestion { suggestion_kind } => { + SubdiagnosticKind::MultipartSuggestion { suggestion_kind, applicability } => { + let applicability = applicability + .value() + .map(|a| quote! { #a }) + .or_else(|| self.applicability.take().value()) + .unwrap_or_else(|| quote! { rustc_errors::Applicability::Unspecified }); + if !self.has_suggestion_parts { span_err( self.span, @@ -686,6 +563,7 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { } } }; + calls.extend(call); } @@ -697,11 +575,13 @@ impl<'a> SessionSubdiagnosticDeriveBuilder<'a> { .map(|binding| self.generate_field_set_arg(binding)) .collect(); + let formatting_init = &self.formatting_init; Ok(quote! { #init + #formatting_init #attr_args - #calls #plain_args + #calls }) } } diff --git a/compiler/rustc_macros/src/diagnostics/utils.rs b/compiler/rustc_macros/src/diagnostics/utils.rs index ad9ecd39b..374c795d0 100644 --- a/compiler/rustc_macros/src/diagnostics/utils.rs +++ b/compiler/rustc_macros/src/diagnostics/utils.rs @@ -1,11 +1,31 @@ -use crate::diagnostics::error::{span_err, throw_span_err, DiagnosticDeriveError}; +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}; +use std::fmt; use std::str::FromStr; -use syn::{spanned::Spanned, Attribute, Meta, Type, TypeTuple}; -use synstructure::{BindingInfo, Structure}; +use syn::{spanned::Spanned, Attribute, Field, Meta, Type, TypeTuple}; +use syn::{MetaList, MetaNameValue, NestedMeta, Path}; +use synstructure::{BindingInfo, VariantInfo}; + +use super::error::invalid_nested_attr; + +thread_local! { + pub static CODE_IDENT_COUNT: RefCell<u32> = RefCell::new(0); +} + +/// Returns an ident of the form `__code_N` where `N` is incremented once with every call. +pub(crate) fn new_code_ident() -> syn::Ident { + CODE_IDENT_COUNT.with(|count| { + let ident = format_ident!("__code_{}", *count.borrow()); + *count.borrow_mut() += 1; + ident + }) +} /// Checks whether the type name of `ty` matches `name`. /// @@ -135,6 +155,15 @@ impl<'ty> FieldInnerTy<'ty> { unreachable!(); } + /// Returns `true` if `FieldInnerTy::with` will result in iteration for this inner type (i.e. + /// that cloning might be required for values moved in the loop body). + pub(crate) fn will_iterate(&self) -> bool { + match self { + FieldInnerTy::Vec(..) => true, + FieldInnerTy::Option(..) | FieldInnerTy::None => false, + } + } + /// Returns `Option` containing inner type if there is one. pub(crate) fn inner_type(&self) -> Option<&'ty Type> { match self { @@ -172,13 +201,17 @@ pub(crate) struct FieldInfo<'a> { /// Small helper trait for abstracting over `Option` fields that contain a value and a `Span` /// for error reporting if they are set more than once. pub(crate) trait SetOnce<T> { - fn set_once(&mut self, _: (T, Span)); + fn set_once(&mut self, value: T, span: Span); fn value(self) -> Option<T>; + fn value_ref(&self) -> Option<&T>; } -impl<T> SetOnce<T> for Option<(T, Span)> { - fn set_once(&mut self, (value, span): (T, Span)) { +/// An [`Option<T>`] that keeps track of the span that caused it to be set; used with [`SetOnce`]. +pub(super) type SpannedOption<T> = Option<(T, Span)>; + +impl<T> SetOnce<T> for SpannedOption<T> { + fn set_once(&mut self, value: T, span: Span) { match self { None => { *self = Some((value, span)); @@ -194,8 +227,14 @@ impl<T> SetOnce<T> for Option<(T, Span)> { fn value(self) -> Option<T> { self.map(|(v, _)| v) } + + fn value_ref(&self) -> Option<&T> { + self.as_ref().map(|(v, _)| v) + } } +pub(super) type FieldMap = HashMap<String, TokenStream>; + pub(crate) trait HasFieldMap { /// Returns the binding for the field with the given name, if it exists on the type. fn get_field_binding(&self, field: &String) -> Option<&TokenStream>; @@ -303,6 +342,7 @@ pub(crate) trait HasFieldMap { /// `Applicability` of a suggestion - mirrors `rustc_errors::Applicability` - and used to represent /// the user's selection of applicability if specified in an attribute. +#[derive(Clone, Copy)] pub(crate) enum Applicability { MachineApplicable, MaybeIncorrect, @@ -345,17 +385,366 @@ impl quote::ToTokens for Applicability { /// Build the mapping of field names to fields. This allows attributes to peek values from /// other fields. -pub(crate) fn build_field_mapping<'a>(structure: &Structure<'a>) -> HashMap<String, TokenStream> { - let mut fields_map = HashMap::new(); - - let ast = structure.ast(); - if let syn::Data::Struct(syn::DataStruct { fields, .. }) = &ast.data { - for field in fields.iter() { - if let Some(ident) = &field.ident { - fields_map.insert(ident.to_string(), quote! { &self.#ident }); +pub(super) fn build_field_mapping<'v>(variant: &VariantInfo<'v>) -> HashMap<String, TokenStream> { + let mut fields_map = FieldMap::new(); + for binding in variant.bindings() { + if let Some(ident) = &binding.ast().ident { + fields_map.insert(ident.to_string(), quote! { #binding }); + } + } + 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(); } } +} - fields_map +/// Possible styles for suggestion subdiagnostics. +#[derive(Clone, Copy)] +pub(super) enum SuggestionKind { + /// `#[suggestion]` + Normal, + /// `#[suggestion_short]` + Short, + /// `#[suggestion_hidden]` + Hidden, + /// `#[suggestion_verbose]` + Verbose, +} + +impl FromStr for SuggestionKind { + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "" => Ok(SuggestionKind::Normal), + "_short" => Ok(SuggestionKind::Short), + "_hidden" => Ok(SuggestionKind::Hidden), + "_verbose" => Ok(SuggestionKind::Verbose), + _ => Err(()), + } + } +} + +impl SuggestionKind { + pub fn to_suggestion_style(&self) -> TokenStream { + match self { + SuggestionKind::Normal => { + quote! { rustc_errors::SuggestionStyle::ShowCode } + } + SuggestionKind::Short => { + quote! { rustc_errors::SuggestionStyle::HideCodeInline } + } + SuggestionKind::Hidden => { + quote! { rustc_errors::SuggestionStyle::HideCodeAlways } + } + SuggestionKind::Verbose => { + quote! { rustc_errors::SuggestionStyle::ShowAlways } + } + } + } +} + +/// Types of subdiagnostics that can be created using attributes +#[derive(Clone)] +pub(super) enum SubdiagnosticKind { + /// `#[label(...)]` + Label, + /// `#[note(...)]` + Note, + /// `#[help(...)]` + Help, + /// `#[warning(...)]` + Warn, + /// `#[suggestion{,_short,_hidden,_verbose}]` + Suggestion { + suggestion_kind: SuggestionKind, + applicability: SpannedOption<Applicability>, + /// Identifier for variable used for formatted code, e.g. `___code_0`. Enables separation + /// of formatting and diagnostic emission so that `set_arg` calls can happen in-between.. + code_field: syn::Ident, + /// Initialization logic for `code_field`'s variable, e.g. + /// `let __formatted_code = /* whatever */;` + code_init: TokenStream, + }, + /// `#[multipart_suggestion{,_short,_hidden,_verbose}]` + MultipartSuggestion { + suggestion_kind: SuggestionKind, + applicability: SpannedOption<Applicability>, + }, +} + +impl SubdiagnosticKind { + /// Constructs a `SubdiagnosticKind` from a field or type attribute such as `#[note]`, + /// `#[error(parser::add_paren)]` or `#[suggestion(code = "...")]`. Returns the + /// `SubdiagnosticKind` and the diagnostic slug, if specified. + pub(super) fn from_attr( + attr: &Attribute, + fields: &impl HasFieldMap, + ) -> Result<Option<(SubdiagnosticKind, Option<Path>)>, DiagnosticDeriveError> { + // Always allow documentation comments. + if is_doc_comment(attr) { + return Ok(None); + } + + 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 mut kind = match name { + "label" => SubdiagnosticKind::Label, + "note" => SubdiagnosticKind::Note, + "help" => SubdiagnosticKind::Help, + "warning" => SubdiagnosticKind::Warn, + _ => { + if let Some(suggestion_kind) = + name.strip_prefix("suggestion").and_then(|s| s.parse().ok()) + { + SubdiagnosticKind::Suggestion { + suggestion_kind, + applicability: None, + code_field: new_code_ident(), + code_init: TokenStream::new(), + } + } else if let Some(suggestion_kind) = + name.strip_prefix("multipart_suggestion").and_then(|s| s.parse().ok()) + { + SubdiagnosticKind::MultipartSuggestion { suggestion_kind, applicability: None } + } else { + throw_invalid_attr!(attr, &meta); + } + } + }; + + let nested = match meta { + Meta::List(MetaList { ref nested, .. }) => { + // An attribute with properties, such as `#[suggestion(code = "...")]` or + // `#[error(some::slug)]` + nested + } + Meta::Path(_) => { + // An attribute without a slug or other properties, such as `#[note]` - return + // without further processing. + // + // Only allow this if there are no mandatory properties, such as `code = "..."` in + // `#[suggestion(...)]` + match kind { + SubdiagnosticKind::Label + | SubdiagnosticKind::Note + | SubdiagnosticKind::Help + | SubdiagnosticKind::Warn + | SubdiagnosticKind::MultipartSuggestion { .. } => { + return Ok(Some((kind, None))); + } + SubdiagnosticKind::Suggestion { .. } => { + throw_span_err!(span, "suggestion without `code = \"...\"`") + } + } + } + _ => { + throw_invalid_attr!(attr, &meta) + } + }; + + let mut code = None; + + let mut nested_iter = nested.into_iter().peekable(); + + // Peek at the first nested attribute: if it's a slug path, consume it. + let slug = if let Some(NestedMeta::Meta(Meta::Path(path))) = nested_iter.peek() { + let path = path.clone(); + // Advance the iterator. + nested_iter.next(); + Some(path) + } else { + None + }; + + for nested_attr in nested_iter { + let meta = match nested_attr { + NestedMeta::Meta(ref meta) => meta, + NestedMeta::Lit(_) => { + invalid_nested_attr(attr, &nested_attr).emit(); + continue; + } + }; + + let span = meta.span().unwrap(); + let nested_name = meta.path().segments.last().unwrap().ident.to_string(); + let nested_name = nested_name.as_str(); + + 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") + }), + _ => None, + }; + + match (nested_name, &mut kind) { + ("code", SubdiagnosticKind::Suggestion { code_field, .. }) => { + 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 + }); + applicability.set_once(value, span); + } + + // Invalid nested attribute + (_, SubdiagnosticKind::Suggestion { .. }) => { + invalid_nested_attr(attr, &nested_attr) + .help("only `code` and `applicability` are valid nested attributes") + .emit(); + } + (_, SubdiagnosticKind::MultipartSuggestion { .. }) => { + invalid_nested_attr(attr, &nested_attr) + .help("only `applicability` is a valid nested attributes") + .emit() + } + _ => { + invalid_nested_attr(attr, &nested_attr).emit(); + } + } + } + + match kind { + SubdiagnosticKind::Suggestion { ref code_field, ref mut code_init, .. } => { + *code_init = if let Some(init) = code.value() { + init + } else { + span_err(span, "suggestion without `code = \"...\"`").emit(); + quote! { let #code_field = std::iter::empty(); } + }; + } + SubdiagnosticKind::Label + | SubdiagnosticKind::Note + | SubdiagnosticKind::Help + | SubdiagnosticKind::Warn + | SubdiagnosticKind::MultipartSuggestion { .. } => {} + } + + Ok(Some((kind, slug))) + } +} + +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 { .. } => write!(f, "suggestions_with_style"), + SubdiagnosticKind::MultipartSuggestion { .. } => { + write!(f, "multipart_suggestion_with_style") + } + } + } + + fn span(&self) -> Option<proc_macro2::Span> { + None + } +} + +/// Returns `true` if `field` should generate a `set_arg` call rather than any other diagnostic +/// call (like `span_label`). +pub(super) fn should_generate_set_arg(field: &Field) -> bool { + field.attrs.is_empty() +} + +pub(super) fn is_doc_comment(attr: &Attribute) -> bool { + attr.path.segments.last().unwrap().ident.to_string() == "doc" } |