Skip to main content

espforge_diagnostics/
lib.rs

1//! One diagnostic model for the whole pipeline.
2//!
3//! A [`Diagnostic`] carries a stable code, a severity, a primary span, any
4//! number of labels, optional help and machine-applicable fixes. The human
5//! renderer lives here too, so the CLI prints and the library never does.
6//!
7//! # Codes
8//!
9//! Codes are five characters: `E` or `W` followed by four digits. The ranges
10//! are reserved by stage so two producers cannot pick the same number:
11//!
12//! | Range | Stage |
13//! |-------|-------|
14//! | `E01xx` | syntax (parser junk, section headers) |
15//! | `E02xx` | `# espforge` core and meta |
16//! | `E03xx` | options and their answers |
17//! | `E04xx` | `$` graph resolution |
18//! | `E05xx` | peripherals |
19//! | `E06xx` | components |
20//! | `E07xx` | ownership and conflicts |
21//! | `E08xx` | services and driver routing |
22//!
23//! Every diagnostic carries a code from the moment it is constructed. There is
24//! no way to build one without it, and no title-to-code table.
25
26use std::ops::Range;
27
28pub mod codes;
29pub mod render;
30
31pub use render::to_human;
32
33/// A byte range into the original source.
34pub type Span = Range<usize>;
35
36/// The whole set of problems from one stage, in render order.
37pub type Diagnostics = Vec<Diagnostic>;
38
39/// How loudly a diagnostic is reported. Only errors make a command fail.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
41pub enum Severity {
42    Error,
43    Warning,
44    Note,
45}
46
47impl Severity {
48    /// The lowercase spelling used by the machine format.
49    pub fn as_str(self) -> &'static str {
50        match self {
51            Severity::Error => "error",
52            Severity::Warning => "warning",
53            Severity::Note => "note",
54        }
55    }
56}
57
58/// A stable diagnostic code (`E0701`).
59///
60/// The format is `E`/`W` plus four digits; the ranges are documented on the
61/// crate. Codes are a public contract once emitted, so never reuse one for a
62/// different message.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub struct DiagCode(&'static str);
65
66impl DiagCode {
67    /// A code with the `E`/`W` + four-digit spelling.
68    pub const fn new(code: &'static str) -> Self {
69        Self(code)
70    }
71
72    /// The code as it appears in machine output.
73    pub const fn as_str(self) -> &'static str {
74        self.0
75    }
76}
77
78/// One underline and its explanation.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct Label {
81    pub span: Span,
82    pub text: String,
83}
84
85/// One text replacement. Spans are byte offsets into the original source.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct Edit {
88    pub span: Span,
89    pub text: String,
90}
91
92/// A suggested edit, shown as one action (`espforge fix`, an LSP quick-fix).
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct Fix {
95    pub title: String,
96    pub edits: Vec<Edit>,
97}
98
99/// One located problem.
100///
101/// `labels` carries the underline text: the first label whose span equals
102/// [`Diagnostic::primary`] is rendered as the primary annotation, every other
103/// label as context. A diagnostic with no matching label still underlines the
104/// primary span, with no text.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct Diagnostic {
107    /// Stable code for this message (see [`codes`]).
108    pub code: DiagCode,
109    pub severity: Severity,
110    /// Short summary on the `error:` line.
111    pub message: String,
112    /// Where the diagnostic points.
113    pub primary: Span,
114    /// Underline text, primary label included.
115    pub labels: Vec<Label>,
116    pub help: Option<String>,
117    pub fixes: Vec<Fix>,
118}
119
120impl Diagnostic {
121    /// An error with a primary span and one label.
122    pub fn error(
123        code: DiagCode,
124        primary: Span,
125        message: impl Into<String>,
126        label: impl Into<String>,
127    ) -> Self {
128        Self {
129            code,
130            severity: Severity::Error,
131            message: message.into(),
132            labels: vec![Label {
133                span: primary.clone(),
134                text: label.into(),
135            }],
136            primary,
137            help: None,
138            fixes: Vec::new(),
139        }
140    }
141
142    /// A warning with a primary span and one label.
143    pub fn warning(
144        code: DiagCode,
145        primary: Span,
146        message: impl Into<String>,
147        label: impl Into<String>,
148    ) -> Self {
149        Self {
150            severity: Severity::Warning,
151            ..Self::error(code, primary, message, label)
152        }
153    }
154
155    /// Add a context underline.
156    pub fn with_label(mut self, span: Span, text: impl Into<String>) -> Self {
157        self.labels.push(Label {
158            span,
159            text: text.into(),
160        });
161        self
162    }
163
164    /// Add a `help:` line.
165    pub fn with_help(mut self, help: impl Into<String>) -> Self {
166        self.help = Some(help.into());
167        self
168    }
169
170    /// Add a machine-applicable fix.
171    pub fn with_fix(mut self, fix: Fix) -> Self {
172        self.fixes.push(fix);
173        self
174    }
175
176    /// The label rendered as the primary annotation, if one matches the
177    /// primary span exactly.
178    pub fn primary_label(&self) -> Option<&Label> {
179        self.labels.iter().find(|label| label.span == self.primary)
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn a_diagnostic_has_one_primary_label() {
189        let d = Diagnostic::error(codes::CORE_UNKNOWN_ELEMENT, 4..8, "bad", "no such thing")
190            .with_label(20..24, "declared here");
191        assert_eq!(d.severity, Severity::Error);
192        assert_eq!(d.primary_label().unwrap().text, "no such thing");
193        assert_eq!(d.labels.len(), 2);
194    }
195
196    #[test]
197    fn codes_are_data_not_prefixes() {
198        let d = Diagnostic::error(DiagCode::new("E0701"), 0..1, "boom", "here");
199        assert_eq!(d.code.as_str(), "E0701");
200    }
201
202    #[test]
203    fn the_machine_severity_spelling_is_stable() {
204        assert_eq!(Severity::Error.as_str(), "error");
205        assert_eq!(Severity::Warning.as_str(), "warning");
206        assert_eq!(Severity::Note.as_str(), "note");
207    }
208}