rustc_macros/diagnostics/
diagnostic.rs

1#![deny(unused_must_use)]
2
3use std::cell::RefCell;
4
5use proc_macro2::TokenStream;
6use quote::quote;
7use syn::spanned::Spanned;
8use synstructure::Structure;
9
10use crate::diagnostics::diagnostic_builder::DiagnosticDeriveKind;
11use crate::diagnostics::error::{DiagnosticDeriveError, span_err};
12use crate::diagnostics::utils::SetOnce;
13
14/// The central struct for constructing the `into_diag` method from an annotated struct.
15pub(crate) struct DiagnosticDerive<'a> {
16    structure: Structure<'a>,
17}
18
19impl<'a> DiagnosticDerive<'a> {
20    pub(crate) fn new(structure: Structure<'a>) -> Self {
21        Self { structure }
22    }
23
24    pub(crate) fn into_tokens(self) -> TokenStream {
25        let DiagnosticDerive { mut structure } = self;
26        let kind = DiagnosticDeriveKind::Diagnostic;
27        let slugs = RefCell::new(Vec::new());
28        let implementation = kind.each_variant(&mut structure, |mut builder, variant| {
29            let preamble = builder.preamble(variant);
30            let body = builder.body(variant);
31
32            let init = match builder.slug.value_ref() {
33                None => {
34                    span_err(builder.span, "diagnostic slug not specified")
35                        .help(
36                            "specify the slug as the first argument to the `#[diag(...)]` \
37                            attribute, such as `#[diag(hir_analysis_example_error)]`",
38                        )
39                        .emit();
40                    return DiagnosticDeriveError::ErrorHandled.to_compile_error();
41                }
42                Some(slug)
43                    if let Some(Mismatch { slug_name, crate_name, slug_prefix }) =
44                        Mismatch::check(slug) =>
45                {
46                    span_err(slug.span().unwrap(), "diagnostic slug and crate name do not match")
47                        .note(format!("slug is `{slug_name}` but the crate name is `{crate_name}`"))
48                        .help(format!("expected a slug starting with `{slug_prefix}_...`"))
49                        .emit();
50                    return DiagnosticDeriveError::ErrorHandled.to_compile_error();
51                }
52                Some(slug) => {
53                    slugs.borrow_mut().push(slug.clone());
54                    quote! {
55                        let mut diag = rustc_errors::Diag::new(
56                            dcx,
57                            level,
58                            crate::fluent_generated::#slug
59                        );
60                    }
61                }
62            };
63
64            let formatting_init = &builder.formatting_init;
65            quote! {
66                #init
67                #formatting_init
68                #preamble
69                #body
70                diag
71            }
72        });
73
74        // A lifetime of `'a` causes conflicts, but `_sess` is fine.
75        // FIXME(edition_2024): Fix the `keyword_idents_2024` lint to not trigger here?
76        #[allow(keyword_idents_2024)]
77        let mut imp = structure.gen_impl(quote! {
78            gen impl<'_sess, G> rustc_errors::Diagnostic<'_sess, G> for @Self
79                where G: rustc_errors::EmissionGuarantee
80            {
81                #[track_caller]
82                fn into_diag(
83                    self,
84                    dcx: rustc_errors::DiagCtxtHandle<'_sess>,
85                    level: rustc_errors::Level
86                ) -> rustc_errors::Diag<'_sess, G> {
87                    #implementation
88                }
89            }
90        });
91        for test in slugs.borrow().iter().map(|s| generate_test(s, &structure)) {
92            imp.extend(test);
93        }
94        imp
95    }
96}
97
98/// The central struct for constructing the `decorate_lint` method from an annotated struct.
99pub(crate) struct LintDiagnosticDerive<'a> {
100    structure: Structure<'a>,
101}
102
103impl<'a> LintDiagnosticDerive<'a> {
104    pub(crate) fn new(structure: Structure<'a>) -> Self {
105        Self { structure }
106    }
107
108    pub(crate) fn into_tokens(self) -> TokenStream {
109        let LintDiagnosticDerive { mut structure } = self;
110        let kind = DiagnosticDeriveKind::LintDiagnostic;
111        let slugs = RefCell::new(Vec::new());
112        let implementation = kind.each_variant(&mut structure, |mut builder, variant| {
113            let preamble = builder.preamble(variant);
114            let body = builder.body(variant);
115
116            let primary_message = match builder.slug.value_ref() {
117                None => {
118                    span_err(builder.span, "diagnostic slug not specified")
119                        .help(
120                            "specify the slug as the first argument to the attribute, such as \
121                            `#[diag(compiletest_example)]`",
122                        )
123                        .emit();
124                    DiagnosticDeriveError::ErrorHandled.to_compile_error()
125                }
126                Some(slug)
127                    if let Some(Mismatch { slug_name, crate_name, slug_prefix }) =
128                        Mismatch::check(slug) =>
129                {
130                    span_err(slug.span().unwrap(), "diagnostic slug and crate name do not match")
131                        .note(format!("slug is `{slug_name}` but the crate name is `{crate_name}`"))
132                        .help(format!("expected a slug starting with `{slug_prefix}_...`"))
133                        .emit();
134                    DiagnosticDeriveError::ErrorHandled.to_compile_error()
135                }
136                Some(slug) => {
137                    slugs.borrow_mut().push(slug.clone());
138                    quote! {
139                        diag.primary_message(crate::fluent_generated::#slug);
140                    }
141                }
142            };
143
144            let formatting_init = &builder.formatting_init;
145            quote! {
146                #primary_message
147                #preamble
148                #formatting_init
149                #body
150                diag
151            }
152        });
153
154        // FIXME(edition_2024): Fix the `keyword_idents_2024` lint to not trigger here?
155        #[allow(keyword_idents_2024)]
156        let mut imp = structure.gen_impl(quote! {
157            gen impl<'__a> rustc_errors::LintDiagnostic<'__a, ()> for @Self {
158                #[track_caller]
159                fn decorate_lint<'__b>(
160                    self,
161                    diag: &'__b mut rustc_errors::Diag<'__a, ()>
162                ) {
163                    #implementation;
164                }
165            }
166        });
167        for test in slugs.borrow().iter().map(|s| generate_test(s, &structure)) {
168            imp.extend(test);
169        }
170
171        imp
172    }
173}
174
175struct Mismatch {
176    slug_name: String,
177    crate_name: String,
178    slug_prefix: String,
179}
180
181impl Mismatch {
182    /// Checks whether the slug starts with the crate name it's in.
183    fn check(slug: &syn::Path) -> Option<Mismatch> {
184        // If this is missing we're probably in a test, so bail.
185        let crate_name = std::env::var("CARGO_CRATE_NAME").ok()?;
186
187        // If we're not in a "rustc_" crate, bail.
188        let Some(("rustc", slug_prefix)) = crate_name.split_once('_') else { return None };
189
190        let slug_name = slug.segments.first()?.ident.to_string();
191        if !slug_name.starts_with(slug_prefix) {
192            Some(Mismatch { slug_name, slug_prefix: slug_prefix.to_string(), crate_name })
193        } else {
194            None
195        }
196    }
197}
198
199/// Generates a `#[test]` that verifies that all referenced variables
200/// exist on this structure.
201fn generate_test(slug: &syn::Path, structure: &Structure<'_>) -> TokenStream {
202    // FIXME: We can't identify variables in a subdiagnostic
203    for field in structure.variants().iter().flat_map(|v| v.ast().fields.iter()) {
204        for attr_name in field.attrs.iter().filter_map(|at| at.path().get_ident()) {
205            if attr_name == "subdiagnostic" {
206                return quote!();
207            }
208        }
209    }
210    use std::sync::atomic::{AtomicUsize, Ordering};
211    // We need to make sure that the same diagnostic slug can be used multiple times without
212    // causing an error, so just have a global counter here.
213    static COUNTER: AtomicUsize = AtomicUsize::new(0);
214    let slug = slug.get_ident().unwrap();
215    let ident = quote::format_ident!("verify_{slug}_{}", COUNTER.fetch_add(1, Ordering::Relaxed));
216    let ref_slug = quote::format_ident!("{slug}_refs");
217    let struct_name = &structure.ast().ident;
218    let variables: Vec<_> = structure
219        .variants()
220        .iter()
221        .flat_map(|v| v.ast().fields.iter().filter_map(|f| f.ident.as_ref().map(|i| i.to_string())))
222        .collect();
223    // tidy errors on `#[test]` outside of test files, so we use `#[test ]` to work around this
224    quote! {
225        #[cfg(test)]
226        #[test ]
227        fn #ident() {
228            let variables = [#(#variables),*];
229            for vref in crate::fluent_generated::#ref_slug {
230                assert!(variables.contains(vref), "{}: variable `{vref}` not found ({})", stringify!(#struct_name), stringify!(#slug));
231            }
232        }
233    }
234}