Skip to main content

cinrs_core/
diag.rs

1//! Diagnostics: collection and emission as `compile_error!` invocations.
2//!
3//! Every diagnostic carries a [`SourceRange`] into the captured C text. At
4//! emission time the range is resolved through the [`SourceMap`] into a
5//! `proc_macro2::Span` and *every* token of the generated
6//! `::core::compile_error! { "…" }` is stamped with it, so `rustc` renders the
7//! error with its caret on the offending C token.
8//!
9//! Several diagnostics simply become several `compile_error!` items; `rustc`
10//! reports them all.
11//!
12//! Warnings are kept in the model but are not emitted: there is no stable way
13//! for a procedural macro to raise a warning, and turning warnings into hard
14//! errors would be worse than staying quiet.
15
16use proc_macro2::{Literal, Span, TokenStream};
17use quote::quote_spanned;
18
19use crate::capture::{SourceMap, SourceRange};
20
21/// Severity of a [`Diagnostic`].
22#[derive(Clone, Copy, PartialEq, Eq, Debug)]
23pub enum Level {
24    /// Stops compilation.
25    Error,
26    /// Advisory only; currently not emitted (see the [module docs](self)).
27    Warning,
28}
29
30/// An extra remark attached to a [`Diagnostic`].
31///
32/// A procedural macro has one span per diagnostic and no way to add a second
33/// one, so a note that refers to *another* place — the `#define` a macro came
34/// from, the first of two conflicting declarations — cannot point there. Its
35/// `range` is rendered into the message instead, as ` at line 5` for the
36/// macro's own text or ` in include/foo.h:12` for an `#include`d file; see
37/// [`Diagnostics::render`]. Messages are therefore phrased to be completed by
38/// that phrase ("macro 'MAX' defined", not "macro 'MAX' defined here").
39#[derive(Clone, PartialEq, Eq, Debug)]
40pub struct Note {
41    /// The text of the note.
42    pub message: String,
43    /// The place the note is about, if any.
44    pub range: Option<SourceRange>,
45}
46
47/// One problem found in the C source.
48#[derive(Clone, PartialEq, Eq, Debug)]
49pub struct Diagnostic {
50    /// Severity.
51    pub level: Level,
52    /// The primary message, phrased like a C compiler would phrase it.
53    pub message: String,
54    /// What the message is about.
55    pub range: SourceRange,
56    /// Additional remarks.
57    pub notes: Vec<Note>,
58    /// Whether the *text* is what is wrong — the spelling of a preprocessing
59    /// token, or the comments before it — rather than the token itself;
60    /// something translation phase 3 decided as the token was being formed.
61    ///
62    /// The lexer's findings are normally held on the token and reported only
63    /// if it survives into the preprocessor's output, because C99 6.4p3 makes
64    /// any character that fits nothing else a preprocessing token of its own:
65    /// a stray `\` or `$` handed to a macro that drops its argument is not an
66    /// error at all. A constraint on the text is different — a universal
67    /// character name that names a character 6.4.3p2 forbids is ill-formed
68    /// where it is *written*, and throwing the token away does not make it
69    /// well-formed; nor does an unterminated comment, or a `//` one in C89,
70    /// stop being wrong because the token after it opened a directive — so
71    /// these are reported as soon as the token is read.
72    ///
73    /// "As soon as it is read" is still not inside a group `#if 0` skips: that
74    /// text is never read at all.
75    pub lexical: bool,
76}
77
78impl Diagnostic {
79    /// Creates an error.
80    pub fn error(range: SourceRange, message: impl Into<String>) -> Self {
81        Self {
82            level: Level::Error,
83            message: message.into(),
84            range,
85            notes: Vec::new(),
86            lexical: false,
87        }
88    }
89
90    /// Creates a warning.
91    pub fn warning(range: SourceRange, message: impl Into<String>) -> Self {
92        Self {
93            level: Level::Warning,
94            message: message.into(),
95            range,
96            notes: Vec::new(),
97            lexical: false,
98        }
99    }
100
101    /// Marks this as a problem with a token's spelling; see
102    /// [`Diagnostic::lexical`].
103    pub fn at_lexing(mut self) -> Self {
104        self.lexical = true;
105        self
106    }
107
108    /// Attaches a note.
109    pub fn with_note(mut self, message: impl Into<String>) -> Self {
110        self.notes.push(Note {
111            message: message.into(),
112            range: None,
113        });
114        self
115    }
116
117    /// Attaches a note that points somewhere.
118    pub fn with_note_at(mut self, range: SourceRange, message: impl Into<String>) -> Self {
119        self.notes.push(Note {
120            message: message.into(),
121            range: Some(range),
122        });
123        self
124    }
125}
126
127/// A sink collecting every problem found while processing one macro
128/// invocation.
129#[derive(Default)]
130pub struct Diagnostics {
131    items: Vec<Diagnostic>,
132    /// Set when the input could not be processed at all; reported at the call
133    /// site because there is no meaningful C position to point at.
134    fatal: Option<String>,
135}
136
137impl Diagnostics {
138    /// Creates an empty sink.
139    pub fn new() -> Self {
140        Self::default()
141    }
142
143    /// Records a diagnostic.
144    pub fn push(&mut self, diag: Diagnostic) {
145        self.items.push(diag);
146    }
147
148    /// Records an error at `range`.
149    pub fn error(&mut self, range: SourceRange, message: impl Into<String>) {
150        self.push(Diagnostic::error(range, message));
151    }
152
153    /// Records a warning at `range`.
154    pub fn warning(&mut self, range: SourceRange, message: impl Into<String>) {
155        self.push(Diagnostic::warning(range, message));
156    }
157
158    /// Merges another sink's contents into this one.
159    pub fn extend(&mut self, other: Diagnostics) {
160        self.items.extend(other.items);
161        if self.fatal.is_none() {
162            self.fatal = other.fatal;
163        }
164    }
165
166    /// Records an unrecoverable problem with the macro input itself.
167    pub fn fatal(&mut self, message: impl Into<String>) {
168        if self.fatal.is_none() {
169            self.fatal = Some(message.into());
170        }
171    }
172
173    /// All recorded diagnostics, in the order they were found.
174    pub fn items(&self) -> &[Diagnostic] {
175        &self.items
176    }
177
178    /// All recorded diagnostics, for a pass that annotates them after the
179    /// fact.
180    ///
181    /// [`Expansions::annotate`](crate::pp::Expansions::annotate) is the reason
182    /// this exists: whether a diagnostic happened inside a macro expansion is
183    /// only known to the preprocessor, and only after the pass that produced
184    /// the diagnostic has finished.
185    pub fn items_mut(&mut self) -> &mut [Diagnostic] {
186        &mut self.items
187    }
188
189    /// Whether anything would stop compilation.
190    pub fn has_errors(&self) -> bool {
191        self.fatal.is_some() || self.items.iter().any(|d| d.level == Level::Error)
192    }
193
194    /// Number of errors recorded.
195    pub fn error_count(&self) -> usize {
196        self.items
197            .iter()
198            .filter(|d| d.level == Level::Error)
199            .count()
200    }
201
202    /// The diagnostics in source order.
203    ///
204    /// The lexer runs to completion before the parser starts, so the raw
205    /// insertion order interleaves badly; reporting top to bottom is what a
206    /// reader expects.
207    pub fn sorted(&self) -> Vec<&Diagnostic> {
208        let mut out: Vec<&Diagnostic> = self.items.iter().collect();
209        out.sort_by_key(|d| d.range.start);
210        out
211    }
212
213    /// Renders every error as a `compile_error!` invocation spanned at the C
214    /// token it refers to.
215    pub fn to_token_stream(&self, map: &SourceMap) -> TokenStream {
216        let mut out = TokenStream::new();
217        if let Some(msg) = &self.fatal {
218            out.extend(compile_error_at(Span::call_site(), msg));
219        }
220        for diag in self.sorted() {
221            if diag.level != Level::Error {
222                continue;
223            }
224            let span = map.span(diag.range);
225            out.extend(compile_error_at(span, &self.render(map, diag)));
226        }
227        out
228    }
229
230    /// The full message text of `diag`, including position information for
231    /// sources whose spans cannot point at an exact location.
232    ///
233    /// Three things can be added to the text the pass wrote:
234    ///
235    /// * A `header.h:12:5: ` prefix, when the problem is inside an `#include`d
236    ///   file. The caret is on the `#include` that pulled the header in, which
237    ///   says nothing about *where* in it the problem is.
238    /// * A ` (at line L, column C of the C source)` suffix, when the input was
239    ///   a string literal, which stable Rust cannot span into.
240    /// * A location for every [`Note`] that has one; see there.
241    pub fn render(&self, map: &SourceMap, diag: &Diagnostic) -> String {
242        let mut msg = position_prefix(map, diag.range);
243        msg.push_str(&diag.message);
244        msg.push_str(&position_suffix(map, diag.range));
245        for note in &diag.notes {
246            msg.push_str("\nnote: ");
247            msg.push_str(&note.message);
248            if let Some(range) = note.range {
249                msg.push_str(&note_location(map, range));
250            }
251        }
252        msg
253    }
254}
255
256/// `header.h:12:5: `, for a problem found inside an `#include`d file.
257fn position_prefix(map: &SourceMap, range: SourceRange) -> String {
258    match map.header_position(range.start) {
259        Some((name, line, column)) => format!("{name}:{line}:{column}: "),
260        None => String::new(),
261    }
262}
263
264/// ` (at line L, column C of the C source)`, but only where a span cannot
265/// point at the position by itself (i.e. string-literal mode, unless the
266/// caller supplied a [`Subspan`](crate::Subspan) hook that answered).
267///
268/// An `#include`d file is not precise either, but it carries its position in
269/// the [prefix](position_prefix) instead, which names the header as well.
270fn position_suffix(map: &SourceMap, range: SourceRange) -> String {
271    if map.is_precise_at(range) || map.header_position(range.start).is_some() {
272        return String::new();
273    }
274    let (line, column) = map.line_col(range.start);
275    format!(" (at line {line}, column {column} of the C source)")
276}
277
278/// ` at line 5` or ` in include/foo.h:12` — where a [`Note`] is pointing.
279///
280/// The line is counted exactly as `__LINE__` counts it: a line of the `.rs`
281/// file the invocation is written in for the macro's own text, and a line of
282/// the header itself for an `#include`d file, which is also named.
283fn note_location(map: &SourceMap, range: SourceRange) -> String {
284    match map.header_position(range.start) {
285        Some((name, line, _)) => format!(" in {name}:{line}"),
286        None => format!(" at line {}", map.source_line(range.start)),
287    }
288}
289
290/// Builds `::core::compile_error! { "message" }` with every token spanned at
291/// `span`.
292pub fn compile_error_at(span: Span, message: &str) -> TokenStream {
293    let mut lit = Literal::string(message);
294    lit.set_span(span);
295    // Braces (rather than parentheses) so that the expansion is valid both in
296    // item position and in statement position.
297    quote_spanned! { span => ::core::compile_error! { #lit } }
298}