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
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::graph::node::Position;
68
69    fn make_test_span() -> Span {
70        Span::new(Position::new(10, 5), Position::new(10, 25))
71    }
72
73    // GraphResult tests
74    #[test]
75    fn test_graph_result_ok() {
76        let result: GraphResult<i32> = Ok(42);
77        assert!(result.is_ok());
78        if let Ok(value) = result {
79            assert_eq!(value, 42);
80        }
81    }
82
83    #[test]
84    fn test_graph_result_err() {
85        let result: GraphResult<i32> = Err(GraphBuilderError::ParseError {
86            span: make_test_span(),
87            reason: "test error".to_string(),
88        });
89        assert!(result.is_err());
90    }
91
92    // ParseError tests
93    #[test]
94    fn test_parse_error_display() {
95        let err = GraphBuilderError::ParseError {
96            span: make_test_span(),
97            reason: "unexpected token".to_string(),
98        };
99        let msg = format!("{err}");
100        assert!(msg.contains("Failed to parse AST node"));
101        assert!(msg.contains("unexpected token"));
102    }
103
104    #[test]
105    fn test_parse_error_debug() {
106        let err = GraphBuilderError::ParseError {
107            span: make_test_span(),
108            reason: "test".to_string(),
109        };
110        let debug = format!("{err:?}");
111        assert!(debug.contains("ParseError"));
112        assert!(debug.contains("span"));
113        assert!(debug.contains("reason"));
114    }
115
116    // UnsupportedConstruct tests
117    #[test]
118    fn test_unsupported_construct_display() {
119        let err = GraphBuilderError::UnsupportedConstruct {
120            construct: "async generator".to_string(),
121            span: make_test_span(),
122        };
123        let msg = format!("{err}");
124        assert!(msg.contains("Unsupported language construct"));
125        assert!(msg.contains("async generator"));
126    }
127
128    #[test]
129    fn test_unsupported_construct_debug() {
130        let err = GraphBuilderError::UnsupportedConstruct {
131            construct: "macro".to_string(),
132            span: make_test_span(),
133        };
134        let debug = format!("{err:?}");
135        assert!(debug.contains("UnsupportedConstruct"));
136        assert!(debug.contains("macro"));
137    }
138
139    // SymbolResolutionError tests
140    #[test]
141    fn test_symbol_resolution_error_display() {
142        let err = GraphBuilderError::SymbolResolutionError {
143            symbol: "MyClass".to_string(),
144            file: PathBuf::from("src/main.rs"),
145        };
146        let msg = format!("{err}");
147        assert!(msg.contains("Failed to resolve symbol"));
148        assert!(msg.contains("MyClass"));
149        assert!(msg.contains("src/main.rs"));
150    }
151
152    #[test]
153    fn test_symbol_resolution_error_debug() {
154        let err = GraphBuilderError::SymbolResolutionError {
155            symbol: "helper_fn".to_string(),
156            file: PathBuf::from("lib.rs"),
157        };
158        let debug = format!("{err:?}");
159        assert!(debug.contains("SymbolResolutionError"));
160        assert!(debug.contains("helper_fn"));
161    }
162
163    // CrossLanguageError tests
164    #[test]
165    fn test_cross_language_error_display() {
166        let err = GraphBuilderError::CrossLanguageError {
167            reason: "incompatible metadata formats".to_string(),
168        };
169        let msg = format!("{err}");
170        assert!(msg.contains("Invalid cross-language edge"));
171        assert!(msg.contains("incompatible metadata formats"));
172    }
173
174    #[test]
175    fn test_cross_language_error_debug() {
176        let err = GraphBuilderError::CrossLanguageError {
177            reason: "test reason".to_string(),
178        };
179        let debug = format!("{err:?}");
180        assert!(debug.contains("CrossLanguageError"));
181        assert!(debug.contains("test reason"));
182    }
183
184    // IoError tests
185    #[test]
186    fn test_io_error_display() {
187        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
188        let err = GraphBuilderError::IoError {
189            file: PathBuf::from("/tmp/missing.rs"),
190            source: io_err,
191        };
192        let msg = format!("{err}");
193        assert!(msg.contains("IO error reading"));
194        assert!(msg.contains("/tmp/missing.rs"));
195    }
196
197    #[test]
198    fn test_io_error_source() {
199        use std::error::Error;
200
201        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
202        let err = GraphBuilderError::IoError {
203            file: PathBuf::from("/etc/passwd"),
204            source: io_err,
205        };
206
207        // The #[source] attribute should make this accessible
208        let source = err.source();
209        assert!(source.is_some());
210    }
211
212    #[test]
213    fn test_io_error_debug() {
214        let io_err = std::io::Error::other("test");
215        let err = GraphBuilderError::IoError {
216            file: PathBuf::from("test.rs"),
217            source: io_err,
218        };
219        let debug = format!("{err:?}");
220        assert!(debug.contains("IoError"));
221        assert!(debug.contains("test.rs"));
222    }
223
224    // Pattern matching tests
225    #[test]
226    fn test_error_pattern_matching() {
227        let err = GraphBuilderError::ParseError {
228            span: make_test_span(),
229            reason: "test".to_string(),
230        };
231
232        match err {
233            GraphBuilderError::ParseError { span, reason } => {
234                assert_eq!(span.start.line, 10);
235                assert_eq!(reason, "test");
236            }
237            _ => panic!("Expected ParseError variant"),
238        }
239    }
240
241    #[test]
242    fn test_all_variants_are_error() {
243        use std::error::Error;
244
245        let errors: Vec<GraphBuilderError> = vec![
246            GraphBuilderError::ParseError {
247                span: make_test_span(),
248                reason: "test".to_string(),
249            },
250            GraphBuilderError::UnsupportedConstruct {
251                construct: "test".to_string(),
252                span: make_test_span(),
253            },
254            GraphBuilderError::SymbolResolutionError {
255                symbol: "test".to_string(),
256                file: PathBuf::from("test.rs"),
257            },
258            GraphBuilderError::CrossLanguageError {
259                reason: "test".to_string(),
260            },
261            GraphBuilderError::IoError {
262                file: PathBuf::from("test.rs"),
263                source: std::io::Error::other("test"),
264            },
265        ];
266
267        for err in errors {
268            // All variants should implement std::error::Error
269            let _: &dyn Error = &err;
270            // All variants should have a non-empty Display
271            assert!(!format!("{err}").is_empty());
272        }
273    }
274}