Skip to main content

sqry_core/plugin/
error.rs

1//! Error types for the plugin system.
2
3use std::path::PathBuf;
4use thiserror::Error;
5
6/// Result type for plugin operations.
7pub type PluginResult<T> = Result<T, PluginError>;
8
9/// Errors that can occur during plugin operations.
10#[derive(Error, Debug)]
11pub enum PluginError {
12    /// Plugin not found for the given extension or language.
13    #[error("no plugin found for extension '{0}'")]
14    NotFound(String),
15
16    /// Failed to load external plugin from disk (Phase 3+).
17    #[error("failed to load plugin from {path}: {reason}")]
18    LoadFailed {
19        /// Path to the plugin file
20        path: PathBuf,
21        /// Reason for failure
22        reason: String,
23    },
24
25    /// Invalid plugin (missing required symbols, incompatible ABI, etc.).
26    #[error("invalid plugin: {0}")]
27    InvalidPlugin(String),
28
29    /// AST parsing failed.
30    #[error("AST parsing failed: {0}")]
31    Parse(#[from] ParseError),
32
33    /// Scope extraction failed.
34    #[error("scope extraction failed: {0}")]
35    Scope(#[from] ScopeError),
36
37    /// Node resolution failed.
38    #[error("symbol resolution failed: {0}")]
39    Resolution(#[from] ResolutionError),
40
41    /// Type mismatch during field evaluation.
42    #[error("Type mismatch for field '{field}': expected {expected_type}, got {got_type}")]
43    TypeMismatch {
44        /// Field name
45        field: String,
46        /// Expected type
47        expected_type: String,
48        /// Actual type received
49        got_type: String,
50    },
51}
52
53/// Errors during AST parsing.
54#[derive(Error, Debug)]
55pub enum ParseError {
56    /// tree-sitter failed to parse the source code.
57    #[error("tree-sitter parsing failed")]
58    TreeSitterFailed,
59
60    /// Failed to set language on parser.
61    #[error("failed to set language: {0}")]
62    LanguageSetFailed(String),
63
64    /// Invalid source code (not UTF-8).
65    #[error("invalid source code (not UTF-8)")]
66    InvalidSource,
67
68    /// Input exceeds maximum allowed size.
69    ///
70    /// This error is returned by `SafeParser` when the input content exceeds
71    /// the configured maximum size limit. This prevents unbounded memory allocation
72    /// from pathological inputs.
73    ///
74    /// # Remediation
75    ///
76    /// Split the file into smaller chunks or increase the `--parser-max-bytes` limit
77    /// within the allowed bounds (1-32 MiB).
78    #[error("input too large: {size} bytes exceeds limit of {max} bytes{}", file.as_ref().map(|f| format!(" (file: {})", f.display())).unwrap_or_default())]
79    InputTooLarge {
80        /// Actual size of the input in bytes
81        size: usize,
82        /// Maximum allowed size in bytes
83        max: usize,
84        /// Optional file path for debugging
85        file: Option<PathBuf>,
86    },
87
88    /// Parsing timed out.
89    ///
90    /// This error is returned by `SafeParser` when tree-sitter parsing exceeds
91    /// the configured timeout. This prevents runaway parsing on pathological inputs
92    /// that could cause exponential backtracking.
93    ///
94    /// # Remediation
95    ///
96    /// The file may contain malformed syntax causing parser pathology. Check the file
97    /// for validity or increase the `--parser-timeout-ms` limit within bounds.
98    #[error("parse timed out after {} ms{}", timeout_micros / 1000, file.as_ref().map(|f| format!(" (file: {})", f.display())).unwrap_or_default())]
99    ParseTimedOut {
100        /// Timeout in microseconds
101        timeout_micros: u64,
102        /// Optional file path for debugging
103        file: Option<PathBuf>,
104    },
105
106    /// Parsing was cancelled by external request.
107    ///
108    /// This error is returned when parsing is cancelled via a cancellation flag,
109    /// typically used by the indexer to abort long-running parses proactively.
110    #[error("parse cancelled: {reason}{}", file.as_ref().map(|f| format!(" (file: {})", f.display())).unwrap_or_default())]
111    ParseCancelled {
112        /// Reason for cancellation
113        reason: String,
114        /// Optional file path for debugging
115        file: Option<PathBuf>,
116    },
117
118    /// Other parse error.
119    #[error("parse error: {0}")]
120    Other(String),
121}
122
123/// Errors during scope extraction.
124#[derive(Error, Debug)]
125pub enum ScopeError {
126    /// Failed to compile tree-sitter query for scope extraction.
127    #[error("failed to compile scope query: {0}")]
128    QueryCompilationFailed(String),
129
130    /// Failed to extract scopes from AST.
131    #[error("failed to extract scopes: {0}")]
132    ExtractionFailed(String),
133
134    /// Invalid scope structure in AST.
135    #[error("invalid scope structure: {0}")]
136    InvalidStructure(String),
137
138    /// Other scope error.
139    #[error("scope error: {0}")]
140    Other(String),
141}
142
143/// Errors during symbol resolution (Phase 5+).
144#[derive(Error, Debug)]
145pub enum ResolutionError {
146    /// Node not found.
147    #[error("symbol '{0}' not found")]
148    NotFound(String),
149
150    /// Ambiguous symbol (multiple definitions).
151    #[error("symbol '{0}' is ambiguous (found {1} definitions)")]
152    Ambiguous(String, usize),
153
154    /// Resolution requires external data not available.
155    #[error("resolution requires {0}")]
156    RequiresData(String),
157
158    /// Other resolution error.
159    #[error("resolution error: {0}")]
160    Other(String),
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_plugin_error_not_found() {
169        let err = PluginError::NotFound("rs".to_string());
170        assert_eq!(err.to_string(), "no plugin found for extension 'rs'");
171    }
172
173    #[test]
174    fn test_plugin_error_load_failed() {
175        let err = PluginError::LoadFailed {
176            path: PathBuf::from("/path/to/plugin.so"),
177            reason: "symbol not found".to_string(),
178        };
179        assert!(err.to_string().contains("failed to load plugin"));
180        assert!(err.to_string().contains("/path/to/plugin.so"));
181    }
182
183    #[test]
184    fn test_parse_error_display() {
185        let err = ParseError::TreeSitterFailed;
186        assert_eq!(err.to_string(), "tree-sitter parsing failed");
187    }
188
189    #[test]
190    fn test_scope_error_display() {
191        let err = ScopeError::ExtractionFailed("invalid node".to_string());
192        assert!(err.to_string().contains("failed to extract scopes"));
193    }
194
195    #[test]
196    fn test_resolution_error_ambiguous() {
197        let err = ResolutionError::Ambiguous("foo".to_string(), 3);
198        assert!(err.to_string().contains("ambiguous"));
199        assert!(err.to_string().contains("3 definitions"));
200    }
201
202    #[test]
203    fn test_parse_error_input_too_large_without_file() {
204        let err = ParseError::InputTooLarge {
205            size: 15_000_000,
206            max: 10_000_000,
207            file: None,
208        };
209        let msg = err.to_string();
210        assert!(msg.contains("15000000 bytes"));
211        assert!(msg.contains("10000000 bytes"));
212        assert!(!msg.contains("file:"));
213    }
214
215    #[test]
216    fn test_parse_error_input_too_large_with_file() {
217        let err = ParseError::InputTooLarge {
218            size: 15_000_000,
219            max: 10_000_000,
220            file: Some(PathBuf::from("/path/to/large.rs")),
221        };
222        let msg = err.to_string();
223        assert!(msg.contains("15000000 bytes"));
224        assert!(msg.contains("10000000 bytes"));
225        assert!(msg.contains("/path/to/large.rs"));
226    }
227
228    #[test]
229    fn test_parse_error_timed_out() {
230        let err = ParseError::ParseTimedOut {
231            timeout_micros: 2_000_000,
232            file: Some(PathBuf::from("/path/to/slow.rs")),
233        };
234        let msg = err.to_string();
235        assert!(msg.contains("2000 ms"));
236        assert!(msg.contains("/path/to/slow.rs"));
237    }
238
239    #[test]
240    fn test_parse_error_cancelled() {
241        let err = ParseError::ParseCancelled {
242            reason: "indexer shutdown".to_string(),
243            file: Some(PathBuf::from("/path/to/file.rs")),
244        };
245        let msg = err.to_string();
246        assert!(msg.contains("indexer shutdown"));
247        assert!(msg.contains("/path/to/file.rs"));
248    }
249}