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 {
178            name: "FOO".into(),
179        }
180        .into();
181        assert!(matches!(err, MkError::Var(_)));
182    }
183
184    #[test]
185    fn mk_error_from_graph_error() {
186        let err: MkError = GraphError::Cycle {
187            chain: "a -> b -> a".into(),
188        }
189        .into();
190        assert!(matches!(err, MkError::Graph(_)));
191    }
192
193    #[test]
194    fn mk_error_from_io_error() {
195        let io = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
196        let err: MkError = io.into();
197        assert!(matches!(err, MkError::Io(_)));
198    }
199
200    #[test]
201    fn shell_error_display() {
202        let err = ShellError::CommandFailed {
203            code: 1,
204            stderr: "gcc: fatal error".into(),
205        };
206        let s = err.to_string();
207        assert!(s.contains("exit code 1"));
208        assert!(s.contains("gcc: fatal error"));
209    }
210
211    #[test]
212    fn graph_cycle_display() {
213        let err = GraphError::Cycle {
214            chain: "a -> b -> c -> a".into(),
215        };
216        assert!(err.to_string().contains("cyclic"));
217        assert!(err.to_string().contains("a -> b -> c -> a"));
218    }
219
220    #[test]
221    fn var_invalid_ref_display() {
222        let err = VarError::InvalidRef {
223            ref_: "$missing}".into(),
224        };
225        assert!(err.to_string().contains("$missing}"));
226    }
227
228    #[test]
229    fn parse_unexpected_token_format() {
230        let err = ParseError::UnexpectedToken {
231            expected: "colon".into(),
232            got: "xyz".into(),
233            line: 5,
234        };
235        assert!(err.to_string().contains("expected colon"));
236        assert!(err.to_string().contains("got xyz"));
237    }
238
239    #[test]
240    fn include_circular_display() {
241        let err = IncludeError::CircularInclude {
242            chain: "a.mk -> b.mk -> a.mk".into(),
243        };
244        assert!(err.to_string().contains("circular"));
245    }
246
247    #[test]
248    fn error_sizes_are_reasonable() {
249        use std::mem::size_of;
250        assert!(size_of::<LexError>() <= 16);
251        assert!(size_of::<ParseError>() <= 56);
252        assert!(size_of::<GraphError>() <= 32);
253        assert!(size_of::<MkError>() <= 56);
254    }
255}