Skip to main content

zerodds_idl_parse/
diagnostic.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Diagnostics. Spec §7.
4
5use alloc::string::String;
6use alloc::vec::Vec;
7use core::fmt;
8use zerodds_idl_cst::Span;
9
10/// How serious a diagnostic is.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
12pub enum Severity {
13    /// The file is not valid IDL. Parsing continues anyway.
14    Error,
15    /// Accepted, but worth saying.
16    Warning,
17}
18
19/// A stable identifier for a diagnostic.
20///
21/// Stable across releases on purpose: a build that greps for a code has
22/// to keep working. Codes are appended, never renumbered or reused.
23/// Spec §7.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
25#[non_exhaustive]
26pub enum DiagnosticCode {
27    /// A token appeared where the grammar did not allow it.
28    UnexpectedToken,
29    /// A token the grammar requires was not there.
30    MissingToken,
31    /// A declaration ran out before it was complete.
32    UnterminatedDeclaration,
33    /// The lexer could not make sense of a range.
34    InvalidToken,
35    /// A construct only a vendor dialect admits, under the strict
36    /// dialect.
37    VendorExtension,
38    /// A construct the grammar has but this parser does not implement
39    /// yet. Reported rather than skipped, so a gap cannot pass for
40    /// acceptance.
41    Unsupported,
42    /// An annotation was written with an argument the specification does
43    /// not define for it.
44    InvalidAnnotation,
45    /// A name is declared twice in one scope, case-insensitively.
46    DuplicateName,
47    /// A scoped name resolves to more than one declaration.
48    AmbiguousName,
49    /// A name resolves to nothing.
50    UnresolvedName,
51    /// A union discriminator is not of a type IDL allows.
52    InvalidDiscriminator,
53    /// A case label repeats, or lies outside the discriminator's range.
54    InvalidCaseLabel,
55    /// Two annotations on one node contradict each other.
56    ConflictingAnnotations,
57    /// A bound, dimension or precision is outside its legal range.
58    InvalidBound,
59    /// A forward declaration was never defined.
60    UndefinedForward,
61    /// Two members of one aggregate resolve to the same id.
62    DuplicateMemberId,
63    /// An identifier differs from a keyword only in case, which
64    /// OMG IDL 4.2 §7.2.4 makes illegal.
65    KeywordCollision,
66    /// A choice the specification permits and something downstream pays
67    /// for.
68    ///
69    /// Never produced by the parser or by lowering — both answer "is
70    /// this IDL", and the answer here is always yes. It is the code
71    /// `zerodds-idlc lint` reports under, so a rule about a *choice* is
72    /// distinguishable from a rule about the language, by code and not
73    /// by which tool printed it.
74    PolicyAdvice,
75    /// A value is not of the type it was declared with.
76    ///
77    /// IDL 4.2 §7.4.1.4.3 requires a constant's value expression to be
78    /// "consistent with the type declared for the constant", and
79    /// §7.2.6.3 states the wide/narrow string pair as a "compile-time
80    /// diagnostic" in so many words.
81    TypeMismatch,
82    /// A `#error` directive was reached.
83    ///
84    /// ISO/IEC 14882:2003 §16.5, which IDL 4.2 §7.3 adopts: "#error
85    /// causes the implementation to produce a diagnostic message that
86    /// includes the specified sequence of preprocessing tokens." Its own
87    /// code because it is the one diagnostic the *source* asked for.
88    ErrorDirective,
89    /// A derived type breaks a rule inheritance places on it.
90    ///
91    /// DDS-XTypes 1.3 §7.2.2.4.5: a derived structure has its base's
92    /// extensibility or none of its own, and defines no key fields — its
93    /// keys are its root base's. Duplicate names and member ids across the
94    /// hierarchy keep their own codes, [`Self::DuplicateName`] and
95    /// [`Self::DuplicateMemberId`].
96    InvalidInheritance,
97    /// A rule of the preprocessor ISO/IEC 14882:2003 §16 describes, which
98    /// IDL 4.2 §7.3 adopts: a macro invoked with the wrong number of
99    /// arguments, a `#` not followed by a parameter, a `##` at either
100    /// end of a replacement list, a paste that does not form one token,
101    /// an `#if` expression that does not evaluate. As a warning: an
102    /// identifier in an `#if` that names no macro, which §16.1 evaluates
103    /// as 0.
104    Preprocessing,
105}
106
107impl DiagnosticCode {
108    /// The greppable form, `IDL0001` and so on.
109    #[must_use]
110    pub const fn as_str(self) -> &'static str {
111        match self {
112            Self::UnexpectedToken => "IDL0001",
113            Self::MissingToken => "IDL0002",
114            Self::UnterminatedDeclaration => "IDL0003",
115            Self::InvalidToken => "IDL0004",
116            Self::VendorExtension => "IDL0005",
117            Self::Unsupported => "IDL0006",
118            Self::InvalidAnnotation => "IDL0007",
119            Self::DuplicateName => "IDL0008",
120            Self::AmbiguousName => "IDL0009",
121            Self::UnresolvedName => "IDL0010",
122            Self::InvalidDiscriminator => "IDL0011",
123            Self::InvalidCaseLabel => "IDL0012",
124            Self::ConflictingAnnotations => "IDL0013",
125            Self::InvalidBound => "IDL0014",
126            Self::UndefinedForward => "IDL0015",
127            Self::DuplicateMemberId => "IDL0016",
128            Self::KeywordCollision => "IDL0017",
129            Self::PolicyAdvice => "IDL0018",
130            Self::TypeMismatch => "IDL0019",
131            Self::ErrorDirective => "IDL0020",
132            Self::InvalidInheritance => "IDL0021",
133            Self::Preprocessing => "IDL0022",
134        }
135    }
136
137    /// Every code, so a test can assert the set is collision-free.
138    pub const ALL: [Self; 22] = [
139        Self::UnexpectedToken,
140        Self::MissingToken,
141        Self::UnterminatedDeclaration,
142        Self::InvalidToken,
143        Self::VendorExtension,
144        Self::Unsupported,
145        Self::InvalidAnnotation,
146        Self::DuplicateName,
147        Self::AmbiguousName,
148        Self::UnresolvedName,
149        Self::InvalidDiscriminator,
150        Self::InvalidCaseLabel,
151        Self::ConflictingAnnotations,
152        Self::InvalidBound,
153        Self::UndefinedForward,
154        Self::DuplicateMemberId,
155        Self::KeywordCollision,
156        Self::PolicyAdvice,
157        Self::TypeMismatch,
158        Self::ErrorDirective,
159        Self::InvalidInheritance,
160        Self::Preprocessing,
161    ];
162}
163
164/// One thing wrong with the input.
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct Diagnostic {
167    /// How serious.
168    pub severity: Severity,
169    /// The stable code.
170    pub code: DiagnosticCode,
171    /// What is wrong, in a sentence.
172    pub message: String,
173    /// Where it is.
174    pub primary: Span,
175    /// Further places worth pointing at, with a note each.
176    pub labels: Vec<(Span, String)>,
177    /// The file `primary` and `labels` point into, when that is not the
178    /// unit itself — an included file. `None` is the unit.
179    ///
180    /// The spans index the file's own text once a caller has mapped
181    /// them out of the preprocessed stream; see
182    /// `Preprocessed::locate`.
183    pub file: Option<String>,
184    /// The paragraph the rule comes from.
185    ///
186    /// A diagnostic without one is ZeroDDS policy and says so in the
187    /// string rather than leaving it empty. Spec §7.
188    pub spec_reference: &'static str,
189}
190
191impl Diagnostic {
192    /// An error at `primary`.
193    #[must_use]
194    pub fn error(
195        code: DiagnosticCode,
196        primary: Span,
197        message: String,
198        spec_reference: &'static str,
199    ) -> Self {
200        Self {
201            severity: Severity::Error,
202            code,
203            message,
204            primary,
205            labels: Vec::new(),
206            file: None,
207            spec_reference,
208        }
209    }
210
211    /// Whether the rule is a clause of a specification rather than
212    /// ZeroDDS policy.
213    ///
214    /// A clause reference carries the section sign and names the
215    /// specification (`OMG IDL 4.2 §7.5.2`, `DDS-XTypes 1.3 §7.2.2.4.5`);
216    /// a policy diagnostic names the ZeroDDS document that states the
217    /// policy (`zerodds-ast-1.0 §8.1`), as [`Diagnostic::spec_reference`]
218    /// asks of it.
219    #[must_use]
220    pub fn cites_a_clause(&self) -> bool {
221        self.spec_reference.contains('§') && !self.spec_reference.starts_with("zerodds")
222    }
223
224    /// A warning at `primary`.
225    ///
226    /// For what is accepted and is still worth a caller's attention: a
227    /// construct admitted only because the dialect was asked to admit
228    /// it, and which no other vendor's compiler will read.
229    #[must_use]
230    pub fn warning(
231        code: DiagnosticCode,
232        primary: Span,
233        message: String,
234        spec_reference: &'static str,
235    ) -> Self {
236        Self {
237            severity: Severity::Warning,
238            code,
239            message,
240            primary,
241            labels: Vec::new(),
242            file: None,
243            spec_reference,
244        }
245    }
246
247    /// Add a secondary place worth pointing at.
248    #[must_use]
249    pub fn with_label(mut self, span: Span, note: String) -> Self {
250        self.labels.push((span, note));
251        self
252    }
253
254    /// Whether this is an error rather than a warning.
255    #[must_use]
256    pub const fn is_error(&self) -> bool {
257        matches!(self.severity, Severity::Error)
258    }
259}
260
261impl fmt::Display for Diagnostic {
262    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263        write!(
264            f,
265            "{} [{}] at {}: {} ({})",
266            match self.severity {
267                Severity::Error => "error",
268                Severity::Warning => "warning",
269            },
270            self.code.as_str(),
271            self.primary,
272            self.message,
273            self.spec_reference
274        )
275    }
276}
277
278impl core::error::Error for Diagnostic {}
279
280/// Whether `diagnostics` are the specification refusing the input: at
281/// least one error, and every error cites a clause
282/// ([`Diagnostic::cites_a_clause`]). Warnings count neither way.
283///
284/// What the probe generators answer with: a refusal by a clause is the
285/// verdict `refused` (exit 1), and anything else stopping the front end
286/// is a gap of this tree (exit 6), `docs/specs/zerodds-verdicts-1.0.md`.
287#[must_use]
288pub fn refused_by_a_clause(diagnostics: &[Diagnostic]) -> bool {
289    let mut errors = diagnostics
290        .iter()
291        .filter(|d| d.severity == Severity::Error)
292        .peekable();
293    errors.peek().is_some() && errors.all(Diagnostic::cites_a_clause)
294}
295
296#[cfg(test)]
297mod clause_tests {
298    use super::*;
299
300    fn at(severity: Severity, spec_reference: &'static str) -> Diagnostic {
301        Diagnostic {
302            severity,
303            code: DiagnosticCode::InvalidInheritance,
304            message: String::new(),
305            primary: Span::new(0, 0),
306            labels: Vec::new(),
307            file: None,
308            spec_reference,
309        }
310    }
311
312    #[test]
313    fn a_clause_of_an_omg_specification_refuses_and_zerodds_policy_does_not() {
314        let clause = at(Severity::Error, "DDS-XTypes 1.3 §7.2.2.4.5");
315        let idl = at(Severity::Error, "OMG IDL 4.2 §7.5.2");
316        let policy = at(Severity::Error, "zerodds-ast-1.0 §8.1");
317        let warning = at(Severity::Warning, "zerodds-idl-frontend-1.0 §3");
318        assert!(clause.cites_a_clause() && idl.cites_a_clause());
319        assert!(!policy.cites_a_clause());
320        assert!(refused_by_a_clause(&[clause.clone(), idl.clone()]));
321        assert!(refused_by_a_clause(&[clause.clone(), warning.clone()]));
322        assert!(!refused_by_a_clause(&[clause, policy]));
323        assert!(!refused_by_a_clause(&[warning]));
324        assert!(!refused_by_a_clause(&[]));
325    }
326}