#![deny(unused_must_use)] use std::cell::RefCell; use proc_macro2::TokenStream; use quote::quote; use syn::spanned::Spanned; use synstructure::Structure; use crate::diagnostics::diagnostic_builder::DiagnosticDeriveKind; use crate::diagnostics::error::{DiagnosticDeriveError, span_err}; use crate::diagnostics::utils::SetOnce; /// The central struct for constructing the `into_diag` method from an annotated struct. pub(crate) struct DiagnosticDerive<'a> { structure: Structure<'a>, } impl<'a> DiagnosticDerive<'a> { pub(crate) fn new(structure: Structure<'a>) -> Self { Self { structure } } pub(crate) fn into_tokens(self) -> TokenStream { let DiagnosticDerive { mut structure } = self; let kind = DiagnosticDeriveKind::Diagnostic; let slugs = RefCell::new(Vec::new()); let implementation = kind.each_variant(&mut structure, |mut builder, variant| { let preamble = builder.preamble(variant); let body = builder.body(variant); let init = match builder.slug.value_ref() { None => { span_err(builder.span, "diagnostic slug not specified") .help( "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) if let Some(Mismatch { slug_name, crate_name, slug_prefix }) = Mismatch::check(slug) => { span_err(slug.span().unwrap(), "diagnostic slug and crate name do not match") .note(format!("slug is `{slug_name}` but the crate name is `{crate_name}`")) .help(format!("expected a slug starting with `{slug_prefix}_...`")) .emit(); return DiagnosticDeriveError::ErrorHandled.to_compile_error(); } Some(slug) => { slugs.borrow_mut().push(slug.clone()); quote! { let mut diag = rustc_errors::Diag::new( dcx, level, crate::fluent_generated::#slug ); } } }; let formatting_init = &builder.formatting_init; quote! { #init #formatting_init #preamble #body diag } }); // A lifetime of `'a` causes conflicts, but `_sess` is fine. // FIXME(edition_2024): Fix the `keyword_idents_2024` lint to not trigger here? #[allow(keyword_idents_2024)] let mut imp = structure.gen_impl(quote! { gen impl<'_sess, G> rustc_errors::Diagnostic<'_sess, G> for @Self where G: rustc_errors::EmissionGuarantee { #[track_caller] fn into_diag( self, dcx: rustc_errors::DiagCtxtHandle<'_sess>, level: rustc_errors::Level ) -> rustc_errors::Diag<'_sess, G> { #implementation } } }); for test in slugs.borrow().iter().map(|s| generate_test(s, &structure)) { imp.extend(test); } imp } } /// The central struct for constructing the `decorate_lint` method from an annotated struct. pub(crate) struct LintDiagnosticDerive<'a> { structure: Structure<'a>, } impl<'a> LintDiagnosticDerive<'a> { pub(crate) fn new(structure: Structure<'a>) -> Self { Self { structure } } pub(crate) fn into_tokens(self) -> TokenStream { let LintDiagnosticDerive { mut structure } = self; let kind = DiagnosticDeriveKind::LintDiagnostic; let slugs = RefCell::new(Vec::new()); let implementation = kind.each_variant(&mut structure, |mut builder, variant| { let preamble = builder.preamble(variant); let body = builder.body(variant); let primary_message = match builder.slug.value_ref() { None => { span_err(builder.span, "diagnostic slug not specified") .help( "specify the slug as the first argument to the attribute, such as \ `#[diag(compiletest_example)]`", ) .emit(); DiagnosticDeriveError::ErrorHandled.to_compile_error() } Some(slug) if let Some(Mismatch { slug_name, crate_name, slug_prefix }) = Mismatch::check(slug) => { span_err(slug.span().unwrap(), "diagnostic slug and crate name do not match") .note(format!("slug is `{slug_name}` but the crate name is `{crate_name}`")) .help(format!("expected a slug starting with `{slug_prefix}_...`")) .emit(); DiagnosticDeriveError::ErrorHandled.to_compile_error() } Some(slug) => { slugs.borrow_mut().push(slug.clone()); quote! { diag.primary_message(crate::fluent_generated::#slug); } } }; let formatting_init = &builder.formatting_init; quote! { #primary_message #preamble #formatting_init #body diag } }); // FIXME(edition_2024): Fix the `keyword_idents_2024` lint to not trigger here? #[allow(keyword_idents_2024)] let mut imp = structure.gen_impl(quote! { gen impl<'__a> rustc_errors::LintDiagnostic<'__a, ()> for @Self { #[track_caller] fn decorate_lint<'__b>( self, diag: &'__b mut rustc_errors::Diag<'__a, ()> ) { #implementation; } } }); for test in slugs.borrow().iter().map(|s| generate_test(s, &structure)) { imp.extend(test); } imp } } struct Mismatch { slug_name: String, crate_name: String, slug_prefix: String, } impl Mismatch { /// Checks whether the slug starts with the crate name it's in. fn check(slug: &syn::Path) -> Option { // If this is missing we're probably in a test, so bail. let crate_name = std::env::var("CARGO_CRATE_NAME").ok()?; // If we're not in a "rustc_" crate, bail. let Some(("rustc", slug_prefix)) = crate_name.split_once('_') else { return None }; let slug_name = slug.segments.first()?.ident.to_string(); if !slug_name.starts_with(slug_prefix) { Some(Mismatch { slug_name, slug_prefix: slug_prefix.to_string(), crate_name }) } else { None } } } /// Generates a `#[test]` that verifies that all referenced variables /// exist on this structure. fn generate_test(slug: &syn::Path, structure: &Structure<'_>) -> TokenStream { // FIXME: We can't identify variables in a subdiagnostic for field in structure.variants().iter().flat_map(|v| v.ast().fields.iter()) { for attr_name in field.attrs.iter().filter_map(|at| at.path().get_ident()) { if attr_name == "subdiagnostic" { return quote!(); } } } use std::sync::atomic::{AtomicUsize, Ordering}; // We need to make sure that the same diagnostic slug can be used multiple times without // causing an error, so just have a global counter here. static COUNTER: AtomicUsize = AtomicUsize::new(0); let slug = slug.get_ident().unwrap(); let ident = quote::format_ident!("verify_{slug}_{}", COUNTER.fetch_add(1, Ordering::Relaxed)); let ref_slug = quote::format_ident!("{slug}_refs"); let struct_name = &structure.ast().ident; let variables: Vec<_> = structure .variants() .iter() .flat_map(|v| v.ast().fields.iter().filter_map(|f| f.ident.as_ref().map(|i| i.to_string()))) .collect(); // tidy errors on `#[test]` outside of test files, so we use `#[test ]` to work around this quote! { #[cfg(test)] #[test ] fn #ident() { let variables = [#(#variables),*]; for vref in crate::fluent_generated::#ref_slug { assert!(variables.contains(vref), "{}: variable `{vref}` not found ({})", stringify!(#struct_name), stringify!(#slug)); } } } }