flux_macros/diagnostics/
diagnostic.rs

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