1use thiserror::Error;
7
8#[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#[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
152pub type MkResult<T> = Result<T, MkError>;
156
157#[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}