Skip to main content

sqry_core/graph/
error.rs

1//! Error types used by graph builders.
2//!
3//! The unified graph architecture expects language-specific builders to surface
4//! rich, structured errors instead of raw strings.  This improves debugging,
5//! allows callers to decide whether an error is recoverable, and keeps us in
6//! compliance with the error-handling guidance in `AGENTS.md`.
7
8use std::path::PathBuf;
9
10use super::node::Span;
11use thiserror::Error;
12
13/// Result alias used throughout the graph builder pipeline.
14pub type GraphResult<T> = Result<T, GraphBuilderError>;
15
16/// Errors that can occur while constructing the unified code graph.
17#[derive(Debug, Error)]
18pub enum GraphBuilderError {
19    /// Tree-sitter failed to parse a construct that the builder expected.
20    #[error("Failed to parse AST node at {span:?}: {reason}")]
21    ParseError {
22        /// Source span for the problematic node.
23        span: Span,
24        /// Human readable error message.
25        reason: String,
26    },
27
28    /// The builder encountered a language feature that is currently unsupported.
29    #[error("Unsupported language construct: {construct} at {span:?}")]
30    UnsupportedConstruct {
31        /// Description of the unsupported construct (e.g., "async generator").
32        construct: String,
33        /// Span for the construct.
34        span: Span,
35    },
36
37    /// A symbol reference could not be resolved to a known definition.
38    #[error("Failed to resolve symbol '{symbol}' in {file}")]
39    SymbolResolutionError {
40        /// Node that the builder attempted to resolve.
41        symbol: String,
42        /// File where resolution failed.
43        file: PathBuf,
44    },
45
46    /// Building a cross-language edge failed validation (e.g. inconsistent metadata).
47    #[error("Invalid cross-language edge: {reason}")]
48    CrossLanguageError {
49        /// Why the edge construction failed.
50        reason: String,
51    },
52
53    /// An IO failure occurred while reading source input.
54    #[error("IO error reading {file}: {source}")]
55    IoError {
56        /// File being read when the failure occurred.
57        file: PathBuf,
58        /// Underlying IO error.
59        #[source]
60        source: std::io::Error,
61    },
62
63    /// Graph building exceeded its per-file time budget.
64    #[error("Graph build timed out after {timeout_ms} ms during {phase} in {file}")]
65    BuildTimedOut {
66        /// File being built when the timeout was reached.
67        file: PathBuf,
68        /// The build phase in progress when the timeout fired.
69        phase: &'static str,
70        /// Timeout budget in milliseconds.
71        timeout_ms: u64,
72    },
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::graph::node::Position;
79
80    fn make_test_span() -> Span {
81        Span::new(Position::new(10, 5), Position::new(10, 25))
82    }
83
84    // GraphResult tests
85    #[test]
86    fn test_graph_result_ok() {
87        let result: GraphResult<i32> = Ok(42);
88        assert!(result.is_ok());
89        if let Ok(value) = result {
90            assert_eq!(value, 42);
91        }
92    }
93
94    #[test]
95    fn test_graph_result_err() {
96        let result: GraphResult<i32> = Err(GraphBuilderError::ParseError {
97            span: make_test_span(),
98            reason: "test error".to_string(),
99        });
100        assert!(result.is_err());
101    }
102
103    // ParseError tests
104    #[test]
105    fn test_parse_error_display() {
106        let err = GraphBuilderError::ParseError {
107            span: make_test_span(),
108            reason: "unexpected token".to_string(),
109        };
110        let msg = format!("{err}");
111        assert!(msg.contains("Failed to parse AST node"));
112        assert!(msg.contains("unexpected token"));
113    }
114
115    #[test]
116    fn test_parse_error_debug() {
117        let err = GraphBuilderError::ParseError {
118            span: make_test_span(),
119            reason: "test".to_string(),
120        };
121        let debug = format!("{err:?}");
122        assert!(debug.contains("ParseError"));
123        assert!(debug.contains("span"));
124        assert!(debug.contains("reason"));
125    }
126
127    // UnsupportedConstruct tests
128    #[test]
129    fn test_unsupported_construct_display() {
130        let err = GraphBuilderError::UnsupportedConstruct {
131            construct: "async generator".to_string(),
132            span: make_test_span(),
133        };
134        let msg = format!("{err}");
135        assert!(msg.contains("Unsupported language construct"));
136        assert!(msg.contains("async generator"));
137    }
138
139    #[test]
140    fn test_unsupported_construct_debug() {
141        let err = GraphBuilderError::UnsupportedConstruct {
142            construct: "macro".to_string(),
143            span: make_test_span(),
144        };
145        let debug = format!("{err:?}");
146        assert!(debug.contains("UnsupportedConstruct"));
147        assert!(debug.contains("macro"));
148    }
149
150    // SymbolResolutionError tests
151    #[test]
152    fn test_symbol_resolution_error_display() {
153        let err = GraphBuilderError::SymbolResolutionError {
154            symbol: "MyClass".to_string(),
155            file: PathBuf::from("src/main.rs"),
156        };
157        let msg = format!("{err}");
158        assert!(msg.contains("Failed to resolve symbol"));
159        assert!(msg.contains("MyClass"));
160        assert!(msg.contains("src/main.rs"));
161    }
162
163    #[test]
164    fn test_symbol_resolution_error_debug() {
165        let err = GraphBuilderError::SymbolResolutionError {
166            symbol: "helper_fn".to_string(),
167            file: PathBuf::from("lib.rs"),
168        };
169        let debug = format!("{err:?}");
170        assert!(debug.contains("SymbolResolutionError"));
171        assert!(debug.contains("helper_fn"));
172    }
173
174    // CrossLanguageError tests
175    #[test]
176    fn test_cross_language_error_display() {
177        let err = GraphBuilderError::CrossLanguageError {
178            reason: "incompatible metadata formats".to_string(),
179        };
180        let msg = format!("{err}");
181        assert!(msg.contains("Invalid cross-language edge"));
182        assert!(msg.contains("incompatible metadata formats"));
183    }
184
185    #[test]
186    fn test_cross_language_error_debug() {
187        let err = GraphBuilderError::CrossLanguageError {
188            reason: "test reason".to_string(),
189        };
190        let debug = format!("{err:?}");
191        assert!(debug.contains("CrossLanguageError"));
192        assert!(debug.contains("test reason"));
193    }
194
195    // IoError tests
196    #[test]
197    fn test_io_error_display() {
198        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
199        let err = GraphBuilderError::IoError {
200            file: PathBuf::from("/tmp/missing.rs"),
201            source: io_err,
202        };
203        let msg = format!("{err}");
204        assert!(msg.contains("IO error reading"));
205        assert!(msg.contains("/tmp/missing.rs"));
206    }
207
208    #[test]
209    fn test_io_error_source() {
210        use std::error::Error;
211
212        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
213        let err = GraphBuilderError::IoError {
214            file: PathBuf::from("/etc/passwd"),
215            source: io_err,
216        };
217
218        // The #[source] attribute should make this accessible
219        let source = err.source();
220        assert!(source.is_some());
221    }
222
223    #[test]
224    fn test_io_error_debug() {
225        let io_err = std::io::Error::other("test");
226        let err = GraphBuilderError::IoError {
227            file: PathBuf::from("test.rs"),
228            source: io_err,
229        };
230        let debug = format!("{err:?}");
231        assert!(debug.contains("IoError"));
232        assert!(debug.contains("test.rs"));
233    }
234
235    #[test]
236    fn test_build_timed_out_display() {
237        let err = GraphBuilderError::BuildTimedOut {
238            file: PathBuf::from("large.cpp"),
239            phase: "walk_tree_for_graph",
240            timeout_ms: 10_000,
241        };
242        let msg = format!("{err}");
243        assert!(msg.contains("Graph build timed out"));
244        assert!(msg.contains("large.cpp"));
245        assert!(msg.contains("walk_tree_for_graph"));
246    }
247
248    // Pattern matching tests
249    #[test]
250    fn test_error_pattern_matching() {
251        let err = GraphBuilderError::ParseError {
252            span: make_test_span(),
253            reason: "test".to_string(),
254        };
255
256        match err {
257            GraphBuilderError::ParseError { span, reason } => {
258                assert_eq!(span.start.line, 10);
259                assert_eq!(reason, "test");
260            }
261            _ => panic!("Expected ParseError variant"),
262        }
263    }
264
265    #[test]
266    fn test_all_variants_are_error() {
267        use std::error::Error;
268
269        let errors: Vec<GraphBuilderError> = vec![
270            GraphBuilderError::ParseError {
271                span: make_test_span(),
272                reason: "test".to_string(),
273            },
274            GraphBuilderError::UnsupportedConstruct {
275                construct: "test".to_string(),
276                span: make_test_span(),
277            },
278            GraphBuilderError::SymbolResolutionError {
279                symbol: "test".to_string(),
280                file: PathBuf::from("test.rs"),
281            },
282            GraphBuilderError::CrossLanguageError {
283                reason: "test".to_string(),
284            },
285            GraphBuilderError::IoError {
286                file: PathBuf::from("test.rs"),
287                source: std::io::Error::other("test"),
288            },
289            GraphBuilderError::BuildTimedOut {
290                file: PathBuf::from("test.rs"),
291                phase: "test-phase",
292                timeout_ms: 1_000,
293            },
294        ];
295
296        for err in errors {
297            // All variants should implement std::error::Error
298            let _: &dyn Error = &err;
299            // All variants should have a non-empty Display
300            assert!(!format!("{err}").is_empty());
301        }
302    }
303}