Skip to main content

lsp_max_ast/core/
errors.rs

1/*
2This file is part of auto-lsp.
3Copyright (C) 2025 CLAUZEL Adrien
4
5auto-lsp is free software: you can redistribute it and/or modify
6it under the terms of the GNU General Public License as published by
7the Free Software Foundation, either version 3 of the License, or
8(at your option) any later version.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13GNU General Public License for more details.
14
15You should have received a copy of the GNU General Public License
16along with this program.  If not, see <http://www.gnu.org/licenses/>
17*/
18
19use std::{collections::HashMap, path::PathBuf, str::Utf8Error};
20
21use ariadne::{ColorGenerator, Fmt, Label, ReportBuilder, Source};
22use lsp_types_max::Uri as Url;
23use thiserror::Error;
24
25use crate::core::document::Document;
26
27/// Error type coming from either tree-sitter or ast parsing.
28///
29/// This error is only produced by auto-lsp.
30///
31/// [`ParseError`] can be converted to [`lsp_types_max::Diagnostic`] to be sent to the client.
32///
33/// An ariadne report can be generated from [`ParseError`] using the `to_label` method.
34#[derive(Error, Clone, Debug, PartialEq, Eq)]
35pub enum ParseError {
36    #[error("{error}")]
37    LexerError {
38        span: tree_sitter::Range,
39        #[source]
40        error: LexerError,
41    },
42    #[error("{error}")]
43    AstError {
44        span: tree_sitter::Range,
45        #[source]
46        error: AstError,
47    },
48}
49
50impl ParseError {
51    pub fn to_lsp_diagnostic(
52        &self,
53        doc: &Document,
54    ) -> Result<lsp_types_max::Diagnostic, DocumentError> {
55        let (range, message) = match self {
56            ParseError::AstError { span: range, error } => (range, error.to_string()),
57            ParseError::LexerError { span: range, error } => (range, error.to_string()),
58        };
59        Ok(lsp_types_max::Diagnostic {
60            range: doc.denormalize_range(range)?,
61            severity: Some(lsp_types_max::DiagnosticSeverity::ERROR),
62            message,
63            code: Some(lsp_types_max::NumberOrString::String("AUTO_LSP".into())),
64            ..Default::default()
65        })
66    }
67
68    /// Creates a label for the error using ariadne.
69    pub fn to_label(
70        &self,
71        source: &Source<&str>,
72        colors: &mut ColorGenerator,
73        report: &mut ReportBuilder<'_, std::ops::Range<usize>>,
74    ) {
75        let range = match self {
76            ParseError::LexerError { span: range, .. } => range,
77            ParseError::AstError { span: range, .. } => range,
78        };
79        let start_line = source.line(range.start_point.column).unwrap().offset();
80        let end_line = source.line(range.end_point.row).unwrap().offset();
81        let start = start_line + range.start_point.column;
82        let end = end_line + range.end_point.column;
83        let curr_color = colors.next();
84
85        report.add_label(
86            Label::new(start..end)
87                .with_message(format!("{}", self.to_string().fg(curr_color)))
88                .with_color(curr_color),
89        );
90    }
91}
92
93/// Error type for AST parsing.
94#[derive(Error, Clone, Debug, PartialEq, Eq)]
95pub enum AstError {
96    #[error("Unexpected {symbol} in {parent_name}")]
97    UnexpectedSymbol {
98        range: tree_sitter::Range,
99        symbol: &'static str,
100        parent_name: &'static str,
101    },
102}
103
104impl From<AstError> for ParseError {
105    fn from(error: AstError) -> Self {
106        let range = match &error {
107            AstError::UnexpectedSymbol { range, .. } => *range,
108        };
109        Self::AstError { span: range, error }
110    }
111}
112
113/// Error type for tree-sitter.
114///
115/// Can either be a syntax error or a missing symbol error.
116#[derive(Error, Clone, Debug, PartialEq, Eq)]
117pub enum LexerError {
118    #[error("{error}")]
119    Missing {
120        range: tree_sitter::Range,
121        error: String,
122        // Missing node's symbol name as it appears in the grammar ignoring aliases as a string
123        grammar_name: &'static str,
124    },
125    #[error("{error}")]
126    Syntax {
127        range: tree_sitter::Range,
128        error: String,
129        affected: String,
130    },
131}
132
133impl From<LexerError> for ParseError {
134    fn from(error: LexerError) -> Self {
135        let range = match &error {
136            LexerError::Missing { range, .. } => *range,
137            LexerError::Syntax { range, .. } => *range,
138        };
139        Self::LexerError { span: range, error }
140    }
141}
142
143/// Main accumulator for parse errors
144///
145/// This is meant to be used in salsa queries to accumulate parse errors.
146#[derive(Debug)]
147#[salsa::accumulator]
148pub struct ParseErrorAccumulator(pub ParseError);
149
150impl ParseErrorAccumulator {
151    pub fn to_lsp_diagnostic(
152        &self,
153        doc: &Document,
154    ) -> Result<lsp_types_max::Diagnostic, DocumentError> {
155        self.0.to_lsp_diagnostic(doc)
156    }
157
158    pub fn to_label(
159        &self,
160        source: &Source<&str>,
161        colors: &mut ColorGenerator,
162        report: &mut ReportBuilder<'_, std::ops::Range<usize>>,
163    ) {
164        self.0.to_label(source, colors, report);
165    }
166}
167
168impl From<&ParseError> for ParseErrorAccumulator {
169    fn from(diagnostic: &ParseError) -> Self {
170        Self(diagnostic.clone())
171    }
172}
173
174impl From<ParseError> for ParseErrorAccumulator {
175    fn from(diagnostic: ParseError) -> Self {
176        Self(diagnostic)
177    }
178}
179
180impl From<&ParseErrorAccumulator> for ParseError {
181    fn from(diagnostic: &ParseErrorAccumulator) -> Self {
182        diagnostic.0.clone()
183    }
184}
185
186impl From<LexerError> for ParseErrorAccumulator {
187    fn from(error: LexerError) -> Self {
188        Self(error.into())
189    }
190}
191
192impl From<AstError> for ParseErrorAccumulator {
193    fn from(error: AstError) -> Self {
194        Self(ParseError::from(error))
195    }
196}
197
198/// Error type for position errors.
199///
200/// Emitted by [`crate::core::ast::AstNode::get_text`] when slicing source code.
201#[derive(Error, Clone, Debug, PartialEq, Eq)]
202pub enum PositionError {
203    #[error("Failed to get text in {range:?}")]
204    WrongTextRange { range: std::ops::Range<usize> },
205    #[error("Failed to get text in {range:?}: Encountered UTF-8 error {utf8_error}")]
206    UTF8Error {
207        range: std::ops::Range<usize>,
208        utf8_error: Utf8Error,
209    },
210}
211
212/// Error type produced by the runtime - aka the server -.
213#[derive(Error, Clone, Debug, PartialEq, Eq)]
214pub enum RuntimeError {
215    #[error("Document error in {uri:?}: {error}")]
216    DocumentError {
217        uri: Url,
218        #[source]
219        error: DocumentError,
220    },
221    #[error("Missing initialization options from client")]
222    MissingOptions,
223    #[error(transparent)]
224    DataBaseError(#[from] DataBaseError),
225    #[error(transparent)]
226    FileSystemError(#[from] FileSystemError),
227    #[error(transparent)]
228    ExtensionError(#[from] ExtensionError),
229}
230
231impl From<(&Url, DocumentError)> for RuntimeError {
232    fn from((uri, error): (&Url, DocumentError)) -> Self {
233        RuntimeError::DocumentError {
234            uri: uri.clone(),
235            error,
236        }
237    }
238}
239
240impl From<(&Url, TreeSitterError)> for RuntimeError {
241    fn from((uri, error): (&Url, TreeSitterError)) -> Self {
242        RuntimeError::DocumentError {
243            uri: uri.clone(),
244            error: DocumentError::TreeSitter(error),
245        }
246    }
247}
248
249impl From<(&Url, TexterError)> for RuntimeError {
250    fn from((uri, error): (&Url, TexterError)) -> Self {
251        RuntimeError::DocumentError {
252            uri: uri.clone(),
253            error: DocumentError::Texter(error),
254        }
255    }
256}
257
258/// Error types produced by the server when performing file system operations.
259#[derive(Error, Clone, Debug, PartialEq, Eq)]
260pub enum FileSystemError {
261    #[cfg(windows)]
262    #[error("Invalid host '{host}' for file path: {path:?}")]
263    FileUrlHost { host: String, path: Url },
264    #[error("Failed to convert url {path:?} to file path")]
265    FileUrlToFilePath { path: Url },
266    #[error("Failed to convert file path {path:?} to url")]
267    FilePathToUrl { path: PathBuf },
268    #[error("Failed to get extension of file {path:?}")]
269    FileExtension { path: Url },
270    #[error("Failed to open file {path:?}: {error}")]
271    FileOpen { path: Url, error: String },
272    #[error("Failed to read file {path:?}: {error}")]
273    FileRead { path: Url, error: String },
274    #[error(transparent)]
275    ExtensionError(#[from] ExtensionError),
276}
277
278/// Error type for file extensions and parsers associated with them.
279#[derive(Error, Clone, Debug, PartialEq, Eq)]
280pub enum ExtensionError {
281    #[error("Unknown file extension {extension}, available extensions are: {available:?}")]
282    UnknownExtension {
283        extension: String,
284        available: HashMap<String, String>,
285    },
286    #[error("No parser found for extension {extension}, available parsers are: {available:?}")]
287    UnknownParser {
288        extension: String,
289        available: Vec<&'static str>,
290    },
291}
292
293/// Error type triggered by the database.
294#[derive(Error, Clone, Debug, PartialEq, Eq)]
295pub enum DataBaseError {
296    #[error("Failed to get file {uri:?}")]
297    FileNotFound { uri: Url },
298    #[error("File {uri:?} already exists")]
299    FileAlreadyExists { uri: Url },
300    #[error("Document error in {uri:?}: {error}")]
301    DocumentError {
302        uri: Url,
303        #[source]
304        error: DocumentError,
305    },
306}
307
308impl From<(&Url, DocumentError)> for DataBaseError {
309    fn from((uri, error): (&Url, DocumentError)) -> Self {
310        DataBaseError::DocumentError {
311            uri: uri.clone(),
312            error,
313        }
314    }
315}
316
317impl From<(&Url, TreeSitterError)> for DataBaseError {
318    fn from((uri, error): (&Url, TreeSitterError)) -> Self {
319        DataBaseError::DocumentError {
320            uri: uri.clone(),
321            error: DocumentError::TreeSitter(error),
322        }
323    }
324}
325
326/// Error type for document handling
327///
328/// Produced by an error coming from either tree-sitter or texter.
329#[derive(Error, Clone, Debug, PartialEq, Eq)]
330pub enum DocumentError {
331    #[error(transparent)]
332    TreeSitter(#[from] TreeSitterError),
333    #[error(transparent)]
334    Texter(#[from] TexterError),
335}
336
337#[derive(Error, Clone, Debug, PartialEq, Eq)]
338pub enum TreeSitterError {
339    #[error("Tree sitter failed to parse tree")]
340    TreeSitterParser,
341}
342
343#[derive(Error, Clone, Debug, PartialEq, Eq)]
344pub enum TexterError {
345    #[error("Texter failed to handle document")]
346    TexterError(#[from] texter::error::Error),
347}
348
349// thiserror's `#[from]` only generates direct From impls, so it does not chain
350// `texter::error::Error` → `TexterError` → `DocumentError` automatically. Add the shortcut
351// explicitly so `?` propagates raw texter errors straight into `Result<_, DocumentError>`.
352impl From<texter::error::Error> for DocumentError {
353    fn from(error: texter::error::Error) -> Self {
354        DocumentError::Texter(TexterError::from(error))
355    }
356}