Skip to main content

mk_rs_core/
error.rs

1//! Centralized error types for mk-core.
2//!
3//! Every fallible function in mk-core returns `Result<T, MkError>`.
4//! No panics in library code.
5
6use thiserror::Error;
7
8// ── Main error type ────────────────────────────────────────────────────────
9
10/// Top-level error for all mk-core operations.
11#[derive(Debug, Error)]
12pub enum MkError {
13    #[error("lex error: {0}")]
14    Lex(#[from] LexError),
15
16    #[error("parse error: {0}")]
17    Parse(#[from] ParseError),
18
19    #[error("variable error: {0}")]
20    Var(#[from] VarError),
21
22    #[error("graph error: {0}")]
23    Graph(#[from] GraphError),
24
25    #[error("I/O error: {0}")]
26    Io(#[from] std::io::Error),
27
28    #[error("shell error: {0}")]
29    Shell(#[from] ShellError),
30
31    #[error("recipe error: {0}")]
32    Recipe(#[from] RecipeError),
33
34    #[error("scheduler error: {0}")]
35    Sched(#[from] SchedError),
36
37    #[error("include error: {0}")]
38    Include(#[from] IncludeError),
39}
40
41// ── Sub-error types ────────────────────────────────────────────────────────
42
43#[derive(Debug, Error)]
44pub enum LexError {
45    #[error("unterminated quote at position {pos}")]
46    UnterminatedQuote { pos: usize },
47
48    #[error("unterminated backtick at position {pos}")]
49    UnterminatedBacktick { pos: usize },
50}
51
52#[derive(Debug, Error)]
53pub enum ParseError {
54    #[error("expected colon at line {line}")]
55    ExpectedColon { line: usize },
56
57    #[error("ambiguous recipe for target {target} at line {line}")]
58    AmbiguousRecipe { target: String, line: usize },
59
60    #[error("unknown attribute {attr} at line {line}")]
61    UnknownAttr { attr: char, line: usize },
62
63    #[error("unexpected token at line {line}: expected {expected}, got {got}")]
64    UnexpectedToken {
65        expected: String,
66        got: String,
67        line: usize,
68    },
69
70    #[error("empty target name at line {line}")]
71    EmptyTarget { line: usize },
72}
73
74#[derive(Debug, Error)]
75pub enum VarError {
76    #[error("undefined variable: ${name}")]
77    UndefinedVar { name: String },
78
79    #[error("invalid variable reference: {ref_}")]
80    InvalidRef { ref_: String },
81
82    #[error("invalid substitution pattern: {pattern}")]
83    InvalidPattern { pattern: String },
84
85    #[error("recursive variable expansion: ${name}")]
86    RecursiveExpansion { name: String },
87}
88
89#[derive(Debug, Error)]
90pub enum GraphError {
91    #[error("cyclic dependency detected: {chain}")]
92    Cycle { chain: String },
93
94    #[error("ambiguous rules for target {target}")]
95    AmbiguousTarget { target: String },
96
97    #[error("no rule to make target {target}")]
98    NoRule { target: String },
99
100    #[error("target {target} is up to date")]
101    UpToDate { target: String },
102}
103
104#[derive(Debug, Error)]
105pub enum ShellError {
106    #[error("shell not found: {name}")]
107    ShellNotFound { name: String },
108
109    #[error("recipe execution failed with exit code {code}: {stderr}")]
110    CommandFailed { code: i32, stderr: String },
111
112    #[error("shell I/O error: {0}")]
113    Io(#[from] std::io::Error),
114}
115
116#[derive(Debug, Error)]
117pub enum RecipeError {
118    #[error("recipe command failed with exit code {code}: {stderr}")]
119    CommandFailed { code: i32, stderr: String },
120
121    #[error("recipe target {target} deleted after error")]
122    TargetDeleted { target: String },
123
124    #[error("recipe I/O error: {0}")]
125    Io(#[from] std::io::Error),
126}
127
128#[derive(Debug, Error)]
129pub enum SchedError {
130    #[error("build aborted due to errors")]
131    BuildFailed,
132
133    #[error("no targets specified")]
134    NoTargets,
135}
136
137#[derive(Debug, Error)]
138pub enum IncludeError {
139    #[error("circular include detected: {chain}")]
140    CircularInclude { chain: String },
141
142    #[error("included file not found: {path}")]
143    FileNotFound { path: String },
144
145    #[error("include command failed: {command}")]
146    CommandFailed { command: String },
147
148    #[error("include I/O error: {0}")]
149    Io(#[from] std::io::Error),
150}
151
152// ── Convenience alias ──────────────────────────────────────────────────────
153
154/// Standard Result type for mk-core operations.
155pub type MkResult<T> = Result<T, MkError>;
156
157// ── Tests ──────────────────────────────────────────────────────────────────
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn mk_error_from_lex_error() {
165        let err: MkError = LexError::UnterminatedQuote { pos: 42 }.into();
166        assert!(matches!(err, MkError::Lex(_)));
167    }
168
169    #[test]
170    fn mk_error_from_parse_error() {
171        let err: MkError = ParseError::ExpectedColon { line: 10 }.into();
172        assert!(matches!(err, MkError::Parse(_)));
173    }
174
175    #[test]
176    fn mk_error_from_var_error() {
177        let err: MkError = VarError::UndefinedVar { name: "FOO".into() }.into();
178        assert!(matches!(err, MkError::Var(_)));
179    }
180
181    #[test]
182    fn mk_error_from_graph_error() {
183        let err: MkError = GraphError::Cycle {
184            chain: "a -> b -> a".into(),
185        }
186        .into();
187        assert!(matches!(err, MkError::Graph(_)));
188    }
189
190    #[test]
191    fn mk_error_from_io_error() {
192        let io = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
193        let err: MkError = io.into();
194        assert!(matches!(err, MkError::Io(_)));
195    }
196
197    #[test]
198    fn shell_error_display() {
199        let err = ShellError::CommandFailed {
200            code: 1,
201            stderr: "gcc: fatal error".into(),
202        };
203        let s = err.to_string();
204        assert!(s.contains("exit code 1"));
205        assert!(s.contains("gcc: fatal error"));
206    }
207
208    #[test]
209    fn graph_cycle_display() {
210        let err = GraphError::Cycle {
211            chain: "a -> b -> c -> a".into(),
212        };
213        assert!(err.to_string().contains("cyclic"));
214        assert!(err.to_string().contains("a -> b -> c -> a"));
215    }
216
217    #[test]
218    fn var_invalid_ref_display() {
219        let err = VarError::InvalidRef {
220            ref_: "$missing}".into(),
221        };
222        assert!(err.to_string().contains("$missing}"));
223    }
224
225    #[test]
226    fn parse_unexpected_token_format() {
227        let err = ParseError::UnexpectedToken {
228            expected: "colon".into(),
229            got: "xyz".into(),
230            line: 5,
231        };
232        assert!(err.to_string().contains("expected colon"));
233        assert!(err.to_string().contains("got xyz"));
234    }
235
236    #[test]
237    fn include_circular_display() {
238        let err = IncludeError::CircularInclude {
239            chain: "a.mk -> b.mk -> a.mk".into(),
240        };
241        assert!(err.to_string().contains("circular"));
242    }
243
244    #[test]
245    fn error_sizes_are_reasonable() {
246        use std::mem::size_of;
247        assert!(size_of::<LexError>() <= 16);
248        assert!(size_of::<ParseError>() <= 56);
249        assert!(size_of::<GraphError>() <= 32);
250        assert!(size_of::<MkError>() <= 56);
251    }
252}
253// pre-commit hook test 1781659076