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    /// Internal invariant violation or propagated error from a subsystem that
75    /// does not fit one of the more specific variants above.
76    ///
77    /// Used when bridging between error models — e.g., when the incremental
78    /// rebuild engine surfaces a failure from the full-build pipeline
79    /// (`anyhow::Result<CodeGraph>` → `GraphResult<CodeGraph>`) or when an
80    /// internal precondition is violated.
81    #[error("Internal graph builder error: {reason}")]
82    Internal {
83        /// Human-readable description of the failure.
84        reason: String,
85    },
86
87    /// Graph build was cancelled cooperatively via a
88    /// [`CancellationToken`][token].
89    ///
90    /// The incremental rebuild engine polls the shared cancellation flag at
91    /// every pass boundary. When the flag is set, the pass returns this
92    /// variant immediately — the partially-built graph is discarded and no
93    /// `publish_graph` boundary is crossed.
94    ///
95    /// This variant is a **cooperative signal**, not a fault: the daemon's
96    /// rebuild dispatcher treats `Cancelled` as a normal outcome whenever a
97    /// later rebuild supersedes an in-flight one. Callers that expect
98    /// side-effect-free cancellation should special-case this variant and
99    /// skip error logging.
100    ///
101    /// [token]: crate::graph::unified::build::CancellationToken
102    #[error("graph build cancelled")]
103    Cancelled,
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::graph::node::Position;
110
111    fn make_test_span() -> Span {
112        Span::new(Position::new(10, 5), Position::new(10, 25))
113    }
114
115    // GraphResult tests
116    #[test]
117    fn test_graph_result_ok() {
118        let result: GraphResult<i32> = Ok(42);
119        assert!(result.is_ok());
120        if let Ok(value) = result {
121            assert_eq!(value, 42);
122        }
123    }
124
125    #[test]
126    fn test_graph_result_err() {
127        let result: GraphResult<i32> = Err(GraphBuilderError::ParseError {
128            span: make_test_span(),
129            reason: "test error".to_string(),
130        });
131        assert!(result.is_err());
132    }
133
134    // ParseError tests
135    #[test]
136    fn test_parse_error_display() {
137        let err = GraphBuilderError::ParseError {
138            span: make_test_span(),
139            reason: "unexpected token".to_string(),
140        };
141        let msg = format!("{err}");
142        assert!(msg.contains("Failed to parse AST node"));
143        assert!(msg.contains("unexpected token"));
144    }
145
146    #[test]
147    fn test_parse_error_debug() {
148        let err = GraphBuilderError::ParseError {
149            span: make_test_span(),
150            reason: "test".to_string(),
151        };
152        let debug = format!("{err:?}");
153        assert!(debug.contains("ParseError"));
154        assert!(debug.contains("span"));
155        assert!(debug.contains("reason"));
156    }
157
158    // UnsupportedConstruct tests
159    #[test]
160    fn test_unsupported_construct_display() {
161        let err = GraphBuilderError::UnsupportedConstruct {
162            construct: "async generator".to_string(),
163            span: make_test_span(),
164        };
165        let msg = format!("{err}");
166        assert!(msg.contains("Unsupported language construct"));
167        assert!(msg.contains("async generator"));
168    }
169
170    #[test]
171    fn test_unsupported_construct_debug() {
172        let err = GraphBuilderError::UnsupportedConstruct {
173            construct: "macro".to_string(),
174            span: make_test_span(),
175        };
176        let debug = format!("{err:?}");
177        assert!(debug.contains("UnsupportedConstruct"));
178        assert!(debug.contains("macro"));
179    }
180
181    // SymbolResolutionError tests
182    #[test]
183    fn test_symbol_resolution_error_display() {
184        let err = GraphBuilderError::SymbolResolutionError {
185            symbol: "MyClass".to_string(),
186            file: PathBuf::from("src/main.rs"),
187        };
188        let msg = format!("{err}");
189        assert!(msg.contains("Failed to resolve symbol"));
190        assert!(msg.contains("MyClass"));
191        assert!(msg.contains("src/main.rs"));
192    }
193
194    #[test]
195    fn test_symbol_resolution_error_debug() {
196        let err = GraphBuilderError::SymbolResolutionError {
197            symbol: "helper_fn".to_string(),
198            file: PathBuf::from("lib.rs"),
199        };
200        let debug = format!("{err:?}");
201        assert!(debug.contains("SymbolResolutionError"));
202        assert!(debug.contains("helper_fn"));
203    }
204
205    // CrossLanguageError tests
206    #[test]
207    fn test_cross_language_error_display() {
208        let err = GraphBuilderError::CrossLanguageError {
209            reason: "incompatible metadata formats".to_string(),
210        };
211        let msg = format!("{err}");
212        assert!(msg.contains("Invalid cross-language edge"));
213        assert!(msg.contains("incompatible metadata formats"));
214    }
215
216    #[test]
217    fn test_cross_language_error_debug() {
218        let err = GraphBuilderError::CrossLanguageError {
219            reason: "test reason".to_string(),
220        };
221        let debug = format!("{err:?}");
222        assert!(debug.contains("CrossLanguageError"));
223        assert!(debug.contains("test reason"));
224    }
225
226    // IoError tests
227    #[test]
228    fn test_io_error_display() {
229        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
230        let err = GraphBuilderError::IoError {
231            file: PathBuf::from("/tmp/missing.rs"),
232            source: io_err,
233        };
234        let msg = format!("{err}");
235        assert!(msg.contains("IO error reading"));
236        assert!(msg.contains("/tmp/missing.rs"));
237    }
238
239    #[test]
240    fn test_io_error_source() {
241        use std::error::Error;
242
243        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
244        let err = GraphBuilderError::IoError {
245            file: PathBuf::from("/etc/passwd"),
246            source: io_err,
247        };
248
249        // The #[source] attribute should make this accessible
250        let source = err.source();
251        assert!(source.is_some());
252    }
253
254    #[test]
255    fn test_io_error_debug() {
256        let io_err = std::io::Error::other("test");
257        let err = GraphBuilderError::IoError {
258            file: PathBuf::from("test.rs"),
259            source: io_err,
260        };
261        let debug = format!("{err:?}");
262        assert!(debug.contains("IoError"));
263        assert!(debug.contains("test.rs"));
264    }
265
266    #[test]
267    fn test_build_timed_out_display() {
268        let err = GraphBuilderError::BuildTimedOut {
269            file: PathBuf::from("large.cpp"),
270            phase: "walk_tree_for_graph",
271            timeout_ms: 10_000,
272        };
273        let msg = format!("{err}");
274        assert!(msg.contains("Graph build timed out"));
275        assert!(msg.contains("large.cpp"));
276        assert!(msg.contains("walk_tree_for_graph"));
277    }
278
279    // Pattern matching tests
280    #[test]
281    fn test_error_pattern_matching() {
282        let err = GraphBuilderError::ParseError {
283            span: make_test_span(),
284            reason: "test".to_string(),
285        };
286
287        match err {
288            GraphBuilderError::ParseError { span, reason } => {
289                assert_eq!(span.start.line, 10);
290                assert_eq!(reason, "test");
291            }
292            _ => panic!("Expected ParseError variant"),
293        }
294    }
295
296    #[test]
297    fn test_all_variants_are_error() {
298        use std::error::Error;
299
300        let errors: Vec<GraphBuilderError> = vec![
301            GraphBuilderError::ParseError {
302                span: make_test_span(),
303                reason: "test".to_string(),
304            },
305            GraphBuilderError::UnsupportedConstruct {
306                construct: "test".to_string(),
307                span: make_test_span(),
308            },
309            GraphBuilderError::SymbolResolutionError {
310                symbol: "test".to_string(),
311                file: PathBuf::from("test.rs"),
312            },
313            GraphBuilderError::CrossLanguageError {
314                reason: "test".to_string(),
315            },
316            GraphBuilderError::IoError {
317                file: PathBuf::from("test.rs"),
318                source: std::io::Error::other("test"),
319            },
320            GraphBuilderError::BuildTimedOut {
321                file: PathBuf::from("test.rs"),
322                phase: "test-phase",
323                timeout_ms: 1_000,
324            },
325            GraphBuilderError::Internal {
326                reason: "test".to_string(),
327            },
328            GraphBuilderError::Cancelled,
329        ];
330
331        for err in errors {
332            // All variants should implement std::error::Error
333            let _: &dyn Error = &err;
334            // All variants should have a non-empty Display
335            assert!(!format!("{err}").is_empty());
336        }
337    }
338
339    // Cancelled variant tests
340    #[test]
341    fn test_cancelled_display() {
342        let err = GraphBuilderError::Cancelled;
343        let msg = format!("{err}");
344        assert_eq!(msg, "graph build cancelled");
345    }
346
347    #[test]
348    fn test_cancelled_debug() {
349        let err = GraphBuilderError::Cancelled;
350        let debug = format!("{err:?}");
351        assert_eq!(debug, "Cancelled");
352    }
353}