Skip to main content

oo_ide/log_matcher/
message.rs

1//! Compile-time diagnostic message types, modelled on rustc-style output.
2//!
3//! Used exclusively during the compilation of log matchers from YAML into
4//! [`crate::log_matcher::types::CompiledMatcher`]. Never used at runtime.
5
6use std::fmt;
7
8// ---------------------------------------------------------------------------
9// Reference
10// ---------------------------------------------------------------------------
11
12/// A location pointer within a YAML source.
13///
14/// Since serde does not expose line numbers, `filename` typically encodes a
15/// field path like `"extension.yaml:matchers[0].start.match"`.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Reference {
18    /// Field path or filename, e.g. `"extension.yaml:matchers[0].id"`.
19    pub filename: String,
20    pub line: Option<u32>,
21    pub column: Option<u32>,
22}
23
24impl Reference {
25    pub fn new(filename: impl Into<String>) -> Self {
26        Self {
27            filename: filename.into(),
28            line: None,
29            column: None,
30        }
31    }
32}
33
34impl fmt::Display for Reference {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        write!(f, "{}", self.filename)?;
37        if let Some(l) = self.line {
38            write!(f, ":{}", l)?;
39            if let Some(c) = self.column {
40                write!(f, ":{}", c)?;
41            }
42        }
43        Ok(())
44    }
45}
46
47// ---------------------------------------------------------------------------
48// AnnotatedRef
49// ---------------------------------------------------------------------------
50
51/// A reference with a label, used for secondary "see also" locations.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct AnnotatedRef {
54    pub reference: Reference,
55    /// Short label, e.g. `"first defined here"`.
56    pub label: String,
57}
58
59// ---------------------------------------------------------------------------
60// MessageLevel
61// ---------------------------------------------------------------------------
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
64pub enum MessageLevel {
65    Info,
66    Warning,
67    Error,
68}
69
70impl fmt::Display for MessageLevel {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match self {
73            Self::Error => write!(f, "error"),
74            Self::Warning => write!(f, "warning"),
75            Self::Info => write!(f, "info"),
76        }
77    }
78}
79
80// ---------------------------------------------------------------------------
81// Message
82// ---------------------------------------------------------------------------
83
84/// A compile-time diagnostic, modelled on rustc-style output.
85///
86/// ```text
87/// error: duplicate matcher id: rust.cargo.error
88///   --> extension.yaml:matchers[2].id
89///   = note: first defined here: extension.yaml:matchers[0].id
90/// ```
91#[derive(Debug, Clone)]
92pub struct Message {
93    pub level: MessageLevel,
94    pub text: String,
95    /// Primary location where the issue was found.
96    pub reference: Option<Reference>,
97    /// Secondary annotated locations (e.g. "first defined here").
98    pub related: Vec<AnnotatedRef>,
99}
100
101impl Message {
102    pub fn error(text: impl Into<String>) -> Self {
103        Self {
104            level: MessageLevel::Error,
105            text: text.into(),
106            reference: None,
107            related: vec![],
108        }
109    }
110
111    pub fn error_at(text: impl Into<String>, reference: impl Into<String>) -> Self {
112        Self {
113            level: MessageLevel::Error,
114            text: text.into(),
115            reference: Some(Reference::new(reference)),
116            related: vec![],
117        }
118    }
119
120    pub fn warning_at(text: impl Into<String>, reference: impl Into<String>) -> Self {
121        Self {
122            level: MessageLevel::Warning,
123            text: text.into(),
124            reference: Some(Reference::new(reference)),
125            related: vec![],
126        }
127    }
128
129    /// Attach a secondary annotated reference to this message.
130    pub fn with_related(mut self, reference: impl Into<String>, label: impl Into<String>) -> Self {
131        self.related.push(AnnotatedRef {
132            reference: Reference::new(reference),
133            label: label.into(),
134        });
135        self
136    }
137}
138
139impl fmt::Display for Message {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        write!(f, "{}: {}", self.level, self.text)?;
142        if let Some(r) = &self.reference {
143            write!(f, "\n  --> {}", r)?;
144        }
145        for ann in &self.related {
146            write!(f, "\n  = note: {}: {}", ann.label, ann.reference)?;
147        }
148        Ok(())
149    }
150}
151
152// ---------------------------------------------------------------------------
153// Helpers
154// ---------------------------------------------------------------------------
155
156/// Returns `true` if any message in `msgs` has level [`MessageLevel::Error`].
157pub fn has_errors(msgs: &[Message]) -> bool {
158    msgs.iter().any(|m| m.level == MessageLevel::Error)
159}
160
161// ---------------------------------------------------------------------------
162// Tests
163// ---------------------------------------------------------------------------
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn message_display_error_with_reference() {
171        let msg = Message::error_at("something went wrong", "file.yaml:matchers[0].id");
172        let s = msg.to_string();
173        assert!(s.contains("error: something went wrong"));
174        assert!(s.contains("file.yaml:matchers[0].id"));
175    }
176
177    #[test]
178    fn message_display_with_related() {
179        let msg = Message::error_at("duplicate id: foo", "file.yaml:matchers[1].id")
180            .with_related("file.yaml:matchers[0].id", "first defined here");
181        let s = msg.to_string();
182        assert!(s.contains("first defined here"));
183        assert!(s.contains("file.yaml:matchers[0].id"));
184    }
185
186    #[test]
187    fn has_errors_detects_error_level() {
188        let msgs = vec![
189            Message::warning_at("a warning", "somewhere"),
190            Message::error_at("an error", "somewhere"),
191        ];
192        assert!(has_errors(&msgs));
193    }
194
195    #[test]
196    fn has_errors_false_for_warnings_only() {
197        let msgs = vec![Message::warning_at("just a warning", "somewhere")];
198        assert!(!has_errors(&msgs));
199    }
200
201    #[test]
202    fn has_errors_false_for_empty() {
203        assert!(!has_errors(&[]));
204    }
205}