Skip to main content

alint_core/
error.rs

1use std::path::{Path, PathBuf};
2
3use thiserror::Error;
4
5pub type Result<T, E = Error> = std::result::Result<T, E>;
6
7#[derive(Debug, Error)]
8pub enum Error {
9    #[error("I/O error at {path}: {source}")]
10    Io {
11        path: PathBuf,
12        #[source]
13        source: std::io::Error,
14    },
15
16    #[error("walk error: {0}")]
17    Walk(#[from] ignore::Error),
18
19    #[error("invalid glob {pattern:?}: {source}")]
20    Glob {
21        pattern: String,
22        #[source]
23        source: globset::Error,
24    },
25
26    #[error("YAML parse error: {0}")]
27    Yaml(#[from] serde_yaml_ng::Error),
28
29    #[error("unknown rule kind {0:?}")]
30    UnknownRuleKind(String),
31
32    #[error("rule {rule_id:?}: {message}")]
33    RuleConfig { rule_id: String, message: String },
34
35    #[error(
36        "file not in index: {path} \
37         (excluded by .gitignore / ignore:, or outside the walked tree)"
38    )]
39    FileNotInIndex { path: PathBuf },
40
41    #[error("{0}")]
42    Other(String),
43}
44
45impl Error {
46    pub fn rule_config(rule_id: impl Into<String>, message: impl Into<String>) -> Self {
47        Self::RuleConfig {
48            rule_id: rule_id.into(),
49            message: message.into(),
50        }
51    }
52
53    /// The single-file re-evaluation contract ([`Engine::run_for_file`](crate::Engine::run_for_file))
54    /// returns this when the requested path isn't in the cached index —
55    /// distinct from "rules ran but found nothing." Callers (the LSP
56    /// server) read it as "this file is excluded from linting."
57    pub fn file_not_in_index(path: &Path) -> Self {
58        Self::FileNotInIndex {
59            path: path.to_path_buf(),
60        }
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn rule_config_constructor_accepts_strings_and_str_refs() {
70        let e1 = Error::rule_config("foo", "bad");
71        let e2 = Error::rule_config(String::from("foo"), String::from("bad"));
72        assert_eq!(e1.to_string(), e2.to_string());
73    }
74
75    #[test]
76    fn rule_config_display_includes_rule_id_and_message() {
77        let e = Error::rule_config("my-rule", "missing field");
78        let s = e.to_string();
79        assert!(s.contains("my-rule"), "missing rule id: {s}");
80        assert!(s.contains("missing field"), "missing message: {s}");
81    }
82
83    #[test]
84    fn unknown_rule_kind_display_quotes_the_kind() {
85        let e = Error::UnknownRuleKind("not_a_real_kind".into());
86        assert!(e.to_string().contains("not_a_real_kind"));
87    }
88
89    #[test]
90    fn glob_error_display_includes_pattern() {
91        let bad = globset::Glob::new("[unterminated").unwrap_err();
92        let e = Error::Glob {
93            pattern: "[unterminated".into(),
94            source: bad,
95        };
96        let s = e.to_string();
97        assert!(s.contains("[unterminated"), "missing pattern: {s}");
98    }
99
100    #[test]
101    fn yaml_error_propagates_via_from_impl() {
102        // The `#[from] serde_yaml_ng::Error` derive lets `?` lift
103        // a YAML parse failure into our Error type without
104        // boilerplate. Sanity-check the impl is wired.
105        let parse: std::result::Result<i32, _> = serde_yaml_ng::from_str("not: yaml: [");
106        let yaml_err = parse.unwrap_err();
107        let our_err: Error = yaml_err.into();
108        assert!(matches!(our_err, Error::Yaml(_)));
109    }
110
111    #[test]
112    fn other_variant_carries_arbitrary_text() {
113        let e = Error::Other("something went sideways".into());
114        assert_eq!(e.to_string(), "something went sideways");
115    }
116}