Skip to main content

meta_ast/
error.rs

1use std::path::PathBuf;
2
3use crate::model::SourceRange;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
6#[non_exhaustive]
7pub enum Severity {
8    Warning,
9    Error,
10}
11
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13pub struct Diagnostic {
14    pub path: PathBuf,
15    pub severity: Severity,
16    pub message: String,
17    pub source_range: Option<SourceRange>,
18}
19
20impl Diagnostic {
21    /// Total ordering key: path, message, then the source position.
22    ///
23    /// The range keeps two diagnostics with the same path and message in a
24    /// stable order, so a run never reorders them between passes.
25    pub fn sort_key(&self) -> (&std::path::Path, &str, usize) {
26        (
27            self.path.as_path(),
28            self.message.as_str(),
29            self.source_range
30                .as_ref()
31                .map_or(usize::MAX, |range| range.byte_start),
32        )
33    }
34}
35
36#[derive(Debug, thiserror::Error)]
37pub enum Error {
38    #[error("IO: {0}")]
39    Io(#[from] std::io::Error),
40
41    #[error("parse error in {path}: {message}")]
42    Parse { path: PathBuf, message: String },
43
44    #[error("query error ({language}): {message}")]
45    Query {
46        language: crate::language::LangId,
47        message: String,
48    },
49
50    #[error("config: {0}")]
51    Config(String),
52
53    #[error("identifier space exhausted")]
54    IdExhausted,
55
56    #[error("analysis reported {errors} error(s) and {warnings} warning(s)")]
57    Diagnostics { errors: usize, warnings: usize },
58
59    #[error("invalid source URI '{uri}': {message}")]
60    InvalidSourceUri { uri: String, message: String },
61
62    #[error("graph error: {0}")]
63    Graph(String),
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::language::LangId;
70    use std::path::PathBuf;
71
72    #[test]
73    fn io_error_from_std_io() {
74        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
75        let err: Error = io_err.into();
76        assert!(matches!(err, Error::Io(_)));
77        assert!(err.to_string().contains("file not found"));
78    }
79
80    #[test]
81    fn parse_error_display_format() {
82        let err = Error::Parse {
83            path: PathBuf::from("foo.bar"),
84            message: "bad syntax".into(),
85        };
86        let displayed = err.to_string();
87        assert!(displayed.contains("foo.bar"), "{displayed}");
88        assert!(displayed.contains("bad syntax"), "{displayed}");
89        assert!(displayed.starts_with("parse error in"), "{displayed}");
90    }
91
92    #[test]
93    fn query_error_display_format() {
94        let err = Error::Query {
95            language: LangId::Rust,
96            message: "no match".into(),
97        };
98        let displayed = err.to_string();
99        assert!(displayed.contains("rust"), "{displayed}");
100        assert!(displayed.contains("no match"), "{displayed}");
101        assert!(displayed.starts_with("query error"), "{displayed}");
102    }
103
104    #[test]
105    fn config_error_display() {
106        let err = Error::Config("bad setting".into());
107        let displayed = err.to_string();
108        assert!(displayed.starts_with("config:"), "{displayed}");
109        assert!(displayed.contains("bad setting"), "{displayed}");
110    }
111
112    #[test]
113    fn diagnostic_construction() {
114        let sr = SourceRange {
115            byte_start: 0,
116            byte_end: 10,
117            start: crate::model::LineColumn { line: 1, column: 0 },
118            end: crate::model::LineColumn {
119                line: 1,
120                column: 10,
121            },
122        };
123        let d = Diagnostic {
124            path: PathBuf::from("test.rs"),
125            severity: Severity::Error,
126            message: "undefined variable".into(),
127            source_range: Some(sr.clone()),
128        };
129        assert_eq!(d.path, PathBuf::from("test.rs"));
130        assert_eq!(d.severity, Severity::Error);
131        assert_eq!(d.message, "undefined variable");
132        assert!(d.source_range.is_some());
133        let range = d.source_range.unwrap();
134        assert_eq!(range.byte_start, 0);
135        assert_eq!(range.byte_end, 10);
136    }
137
138    #[test]
139    fn severity_variants() {
140        assert_ne!(Severity::Warning, Severity::Error);
141        let v = [Severity::Warning, Severity::Error];
142        assert_eq!(v.len(), 2);
143    }
144
145    #[test]
146    fn graph_error_display_format() {
147        let err = Error::Graph("cycle detected".into());
148        let displayed = err.to_string();
149        assert!(displayed.starts_with("graph error:"), "{displayed}");
150        assert!(displayed.contains("cycle detected"), "{displayed}");
151    }
152
153    #[test]
154    fn graph_error_matches() {
155        let err = Error::Graph("test".into());
156        assert!(matches!(err, Error::Graph(_)));
157    }
158}