kcl_lib/
errors.rs

1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
5
6use crate::{
7    execution::{ArtifactCommand, ArtifactGraph, Operation},
8    lsp::IntoDiagnostic,
9    modules::{ModulePath, ModuleSource},
10    source_range::SourceRange,
11    ModuleId,
12};
13
14/// How did the KCL execution fail
15#[derive(thiserror::Error, Debug)]
16pub enum ExecError {
17    #[error("{0}")]
18    Kcl(#[from] Box<crate::KclErrorWithOutputs>),
19    #[error("Could not connect to engine: {0}")]
20    Connection(#[from] ConnectionError),
21    #[error("PNG snapshot could not be decoded: {0}")]
22    BadPng(String),
23}
24
25impl From<KclErrorWithOutputs> for ExecError {
26    fn from(error: KclErrorWithOutputs) -> Self {
27        ExecError::Kcl(Box::new(error))
28    }
29}
30
31/// How did the KCL execution fail, with extra state.
32#[cfg_attr(target_arch = "wasm32", expect(dead_code))]
33#[derive(Debug)]
34pub struct ExecErrorWithState {
35    pub error: ExecError,
36    pub exec_state: Option<crate::execution::ExecState>,
37}
38
39impl ExecErrorWithState {
40    #[cfg_attr(target_arch = "wasm32", expect(dead_code))]
41    pub fn new(error: ExecError, exec_state: crate::execution::ExecState) -> Self {
42        Self {
43            error,
44            exec_state: Some(exec_state),
45        }
46    }
47}
48
49impl From<ExecError> for ExecErrorWithState {
50    fn from(error: ExecError) -> Self {
51        Self {
52            error,
53            exec_state: None,
54        }
55    }
56}
57
58impl From<ConnectionError> for ExecErrorWithState {
59    fn from(error: ConnectionError) -> Self {
60        Self {
61            error: error.into(),
62            exec_state: None,
63        }
64    }
65}
66
67/// How did KCL client fail to connect to the engine
68#[derive(thiserror::Error, Debug)]
69pub enum ConnectionError {
70    #[error("Could not create a Zoo client: {0}")]
71    CouldNotMakeClient(anyhow::Error),
72    #[error("Could not establish connection to engine: {0}")]
73    Establishing(anyhow::Error),
74}
75
76#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
77#[ts(export)]
78#[serde(tag = "kind", rename_all = "snake_case")]
79pub enum KclError {
80    #[error("lexical: {0:?}")]
81    Lexical(KclErrorDetails),
82    #[error("syntax: {0:?}")]
83    Syntax(KclErrorDetails),
84    #[error("semantic: {0:?}")]
85    Semantic(KclErrorDetails),
86    #[error("import cycle: {0:?}")]
87    ImportCycle(KclErrorDetails),
88    #[error("type: {0:?}")]
89    Type(KclErrorDetails),
90    #[error("unimplemented: {0:?}")]
91    Unimplemented(KclErrorDetails),
92    #[error("unexpected: {0:?}")]
93    Unexpected(KclErrorDetails),
94    #[error("value already defined: {0:?}")]
95    ValueAlreadyDefined(KclErrorDetails),
96    #[error("undefined value: {0:?}")]
97    UndefinedValue(KclErrorDetails),
98    #[error("invalid expression: {0:?}")]
99    InvalidExpression(KclErrorDetails),
100    #[error("engine: {0:?}")]
101    Engine(KclErrorDetails),
102    #[error("internal error, please report to KittyCAD team: {0:?}")]
103    Internal(KclErrorDetails),
104}
105
106impl From<KclErrorWithOutputs> for KclError {
107    fn from(error: KclErrorWithOutputs) -> Self {
108        error.error
109    }
110}
111
112#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq)]
113#[error("{error}")]
114#[ts(export)]
115#[serde(rename_all = "camelCase")]
116pub struct KclErrorWithOutputs {
117    pub error: KclError,
118    pub operations: Vec<Operation>,
119    pub artifact_commands: Vec<ArtifactCommand>,
120    pub artifact_graph: ArtifactGraph,
121    pub filenames: IndexMap<ModuleId, ModulePath>,
122    pub source_files: IndexMap<ModuleId, ModuleSource>,
123}
124
125impl KclErrorWithOutputs {
126    pub fn new(
127        error: KclError,
128        operations: Vec<Operation>,
129        artifact_commands: Vec<ArtifactCommand>,
130        artifact_graph: ArtifactGraph,
131        filenames: IndexMap<ModuleId, ModulePath>,
132        source_files: IndexMap<ModuleId, ModuleSource>,
133    ) -> Self {
134        Self {
135            error,
136            operations,
137            artifact_commands,
138            artifact_graph,
139            filenames,
140            source_files,
141        }
142    }
143    pub fn no_outputs(error: KclError) -> Self {
144        Self {
145            error,
146            operations: Default::default(),
147            artifact_commands: Default::default(),
148            artifact_graph: Default::default(),
149            filenames: Default::default(),
150            source_files: Default::default(),
151        }
152    }
153    pub fn into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
154        let mut source_ranges = self.error.source_ranges();
155
156        // Pop off the first source range to get the filename.
157        let first_source_range = source_ranges
158            .pop()
159            .ok_or_else(|| anyhow::anyhow!("No source ranges found"))?;
160
161        let source = self
162            .source_files
163            .get(&first_source_range.module_id())
164            .cloned()
165            .unwrap_or(ModuleSource {
166                source: code.to_string(),
167                path: self
168                    .filenames
169                    .get(&first_source_range.module_id())
170                    .ok_or_else(|| {
171                        anyhow::anyhow!(
172                            "Could not find filename for module id: {:?}",
173                            first_source_range.module_id()
174                        )
175                    })?
176                    .clone(),
177            });
178        let filename = source.path.to_string();
179        let kcl_source = source.source.to_string();
180
181        let mut related = Vec::new();
182        for source_range in source_ranges {
183            let module_id = source_range.module_id();
184            let source = self.source_files.get(&module_id).cloned().unwrap_or(ModuleSource {
185                source: code.to_string(),
186                path: self
187                    .filenames
188                    .get(&module_id)
189                    .ok_or_else(|| anyhow::anyhow!("Could not find filename for module id: {:?}", module_id))?
190                    .clone(),
191            });
192            let error = self.error.override_source_ranges(vec![source_range]);
193            let report = Report {
194                error,
195                kcl_source: source.source.to_string(),
196                filename: source.path.to_string(),
197            };
198            related.push(report);
199        }
200
201        Ok(ReportWithOutputs {
202            error: self,
203            kcl_source,
204            filename,
205            related,
206        })
207    }
208}
209
210impl IntoDiagnostic for KclErrorWithOutputs {
211    fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
212        let message = self.error.get_message();
213        let source_ranges = self.error.source_ranges();
214
215        source_ranges
216            .into_iter()
217            .map(|source_range| {
218                let source = self
219                    .source_files
220                    .get(&source_range.module_id())
221                    .cloned()
222                    .unwrap_or(ModuleSource {
223                        source: code.to_string(),
224                        path: self.filenames.get(&source_range.module_id()).unwrap().clone(),
225                    });
226                let mut filename = source.path.to_string();
227                if !filename.starts_with("file://") {
228                    filename = format!("file:///{}", filename.trim_start_matches("/"));
229                }
230
231                let related_information = if let Ok(uri) = url::Url::parse(&filename) {
232                    Some(vec![tower_lsp::lsp_types::DiagnosticRelatedInformation {
233                        location: tower_lsp::lsp_types::Location {
234                            uri,
235                            range: source_range.to_lsp_range(&source.source),
236                        },
237                        message: message.to_string(),
238                    }])
239                } else {
240                    None
241                };
242
243                Diagnostic {
244                    range: source_range.to_lsp_range(code),
245                    severity: Some(self.severity()),
246                    code: None,
247                    // TODO: this is neat we can pass a URL to a help page here for this specific error.
248                    code_description: None,
249                    source: Some("kcl".to_string()),
250                    related_information,
251                    message: message.clone(),
252                    tags: None,
253                    data: None,
254                }
255            })
256            .collect()
257    }
258
259    fn severity(&self) -> DiagnosticSeverity {
260        DiagnosticSeverity::ERROR
261    }
262}
263
264#[derive(thiserror::Error, Debug)]
265#[error("{}", self.error.error.get_message())]
266pub struct ReportWithOutputs {
267    pub error: KclErrorWithOutputs,
268    pub kcl_source: String,
269    pub filename: String,
270    pub related: Vec<Report>,
271}
272
273impl miette::Diagnostic for ReportWithOutputs {
274    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
275        let family = match self.error.error {
276            KclError::Lexical(_) => "Lexical",
277            KclError::Syntax(_) => "Syntax",
278            KclError::Semantic(_) => "Semantic",
279            KclError::ImportCycle(_) => "ImportCycle",
280            KclError::Type(_) => "Type",
281            KclError::Unimplemented(_) => "Unimplemented",
282            KclError::Unexpected(_) => "Unexpected",
283            KclError::ValueAlreadyDefined(_) => "ValueAlreadyDefined",
284            KclError::UndefinedValue(_) => "UndefinedValue",
285            KclError::InvalidExpression(_) => "InvalidExpression",
286            KclError::Engine(_) => "Engine",
287            KclError::Internal(_) => "Internal",
288        };
289        let error_string = format!("KCL {family} error");
290        Some(Box::new(error_string))
291    }
292
293    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
294        Some(&self.kcl_source)
295    }
296
297    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
298        let iter = self
299            .error
300            .error
301            .source_ranges()
302            .clone()
303            .into_iter()
304            .map(miette::SourceSpan::from)
305            .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
306        Some(Box::new(iter))
307    }
308
309    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
310        let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
311        Some(Box::new(iter))
312    }
313}
314
315#[derive(thiserror::Error, Debug)]
316#[error("{}", self.error.get_message())]
317pub struct Report {
318    pub error: KclError,
319    pub kcl_source: String,
320    pub filename: String,
321}
322
323impl miette::Diagnostic for Report {
324    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
325        let family = match self.error {
326            KclError::Lexical(_) => "Lexical",
327            KclError::Syntax(_) => "Syntax",
328            KclError::Semantic(_) => "Semantic",
329            KclError::ImportCycle(_) => "ImportCycle",
330            KclError::Type(_) => "Type",
331            KclError::Unimplemented(_) => "Unimplemented",
332            KclError::Unexpected(_) => "Unexpected",
333            KclError::ValueAlreadyDefined(_) => "ValueAlreadyDefined",
334            KclError::UndefinedValue(_) => "UndefinedValue",
335            KclError::InvalidExpression(_) => "InvalidExpression",
336            KclError::Engine(_) => "Engine",
337            KclError::Internal(_) => "Internal",
338        };
339        let error_string = format!("KCL {family} error");
340        Some(Box::new(error_string))
341    }
342
343    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
344        Some(&self.kcl_source)
345    }
346
347    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
348        let iter = self
349            .error
350            .source_ranges()
351            .clone()
352            .into_iter()
353            .map(miette::SourceSpan::from)
354            .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
355        Some(Box::new(iter))
356    }
357}
358
359#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
360#[error("{message}")]
361#[ts(export)]
362pub struct KclErrorDetails {
363    #[serde(rename = "sourceRanges")]
364    #[label(collection, "Errors")]
365    pub source_ranges: Vec<SourceRange>,
366    #[serde(rename = "msg")]
367    pub message: String,
368}
369
370impl KclError {
371    pub fn internal(message: String) -> KclError {
372        KclError::Internal(KclErrorDetails {
373            source_ranges: Default::default(),
374            message,
375        })
376    }
377
378    /// Get the error message.
379    pub fn get_message(&self) -> String {
380        format!("{}: {}", self.error_type(), self.message())
381    }
382
383    pub fn error_type(&self) -> &'static str {
384        match self {
385            KclError::Lexical(_) => "lexical",
386            KclError::Syntax(_) => "syntax",
387            KclError::Semantic(_) => "semantic",
388            KclError::ImportCycle(_) => "import cycle",
389            KclError::Type(_) => "type",
390            KclError::Unimplemented(_) => "unimplemented",
391            KclError::Unexpected(_) => "unexpected",
392            KclError::ValueAlreadyDefined(_) => "value already defined",
393            KclError::UndefinedValue(_) => "undefined value",
394            KclError::InvalidExpression(_) => "invalid expression",
395            KclError::Engine(_) => "engine",
396            KclError::Internal(_) => "internal",
397        }
398    }
399
400    pub fn source_ranges(&self) -> Vec<SourceRange> {
401        match &self {
402            KclError::Lexical(e) => e.source_ranges.clone(),
403            KclError::Syntax(e) => e.source_ranges.clone(),
404            KclError::Semantic(e) => e.source_ranges.clone(),
405            KclError::ImportCycle(e) => e.source_ranges.clone(),
406            KclError::Type(e) => e.source_ranges.clone(),
407            KclError::Unimplemented(e) => e.source_ranges.clone(),
408            KclError::Unexpected(e) => e.source_ranges.clone(),
409            KclError::ValueAlreadyDefined(e) => e.source_ranges.clone(),
410            KclError::UndefinedValue(e) => e.source_ranges.clone(),
411            KclError::InvalidExpression(e) => e.source_ranges.clone(),
412            KclError::Engine(e) => e.source_ranges.clone(),
413            KclError::Internal(e) => e.source_ranges.clone(),
414        }
415    }
416
417    /// Get the inner error message.
418    pub fn message(&self) -> &str {
419        match &self {
420            KclError::Lexical(e) => &e.message,
421            KclError::Syntax(e) => &e.message,
422            KclError::Semantic(e) => &e.message,
423            KclError::ImportCycle(e) => &e.message,
424            KclError::Type(e) => &e.message,
425            KclError::Unimplemented(e) => &e.message,
426            KclError::Unexpected(e) => &e.message,
427            KclError::ValueAlreadyDefined(e) => &e.message,
428            KclError::UndefinedValue(e) => &e.message,
429            KclError::InvalidExpression(e) => &e.message,
430            KclError::Engine(e) => &e.message,
431            KclError::Internal(e) => &e.message,
432        }
433    }
434
435    pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
436        let mut new = self.clone();
437        match &mut new {
438            KclError::Lexical(e) => e.source_ranges = source_ranges,
439            KclError::Syntax(e) => e.source_ranges = source_ranges,
440            KclError::Semantic(e) => e.source_ranges = source_ranges,
441            KclError::ImportCycle(e) => e.source_ranges = source_ranges,
442            KclError::Type(e) => e.source_ranges = source_ranges,
443            KclError::Unimplemented(e) => e.source_ranges = source_ranges,
444            KclError::Unexpected(e) => e.source_ranges = source_ranges,
445            KclError::ValueAlreadyDefined(e) => e.source_ranges = source_ranges,
446            KclError::UndefinedValue(e) => e.source_ranges = source_ranges,
447            KclError::InvalidExpression(e) => e.source_ranges = source_ranges,
448            KclError::Engine(e) => e.source_ranges = source_ranges,
449            KclError::Internal(e) => e.source_ranges = source_ranges,
450        }
451
452        new
453    }
454
455    pub(crate) fn add_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
456        let mut new = self.clone();
457        match &mut new {
458            KclError::Lexical(e) => e.source_ranges.extend(source_ranges),
459            KclError::Syntax(e) => e.source_ranges.extend(source_ranges),
460            KclError::Semantic(e) => e.source_ranges.extend(source_ranges),
461            KclError::ImportCycle(e) => e.source_ranges.extend(source_ranges),
462            KclError::Type(e) => e.source_ranges.extend(source_ranges),
463            KclError::Unimplemented(e) => e.source_ranges.extend(source_ranges),
464            KclError::Unexpected(e) => e.source_ranges.extend(source_ranges),
465            KclError::ValueAlreadyDefined(e) => e.source_ranges.extend(source_ranges),
466            KclError::UndefinedValue(e) => e.source_ranges.extend(source_ranges),
467            KclError::InvalidExpression(e) => e.source_ranges.extend(source_ranges),
468            KclError::Engine(e) => e.source_ranges.extend(source_ranges),
469            KclError::Internal(e) => e.source_ranges.extend(source_ranges),
470        }
471
472        new
473    }
474}
475
476impl IntoDiagnostic for KclError {
477    fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
478        let message = self.get_message();
479        let source_ranges = self.source_ranges();
480
481        // Limit to only errors in the top-level file.
482        let module_id = ModuleId::default();
483        let source_ranges = source_ranges
484            .iter()
485            .filter(|r| r.module_id() == module_id)
486            .collect::<Vec<_>>();
487
488        let mut diagnostics = Vec::new();
489        for source_range in &source_ranges {
490            diagnostics.push(Diagnostic {
491                range: source_range.to_lsp_range(code),
492                severity: Some(self.severity()),
493                code: None,
494                // TODO: this is neat we can pass a URL to a help page here for this specific error.
495                code_description: None,
496                source: Some("kcl".to_string()),
497                related_information: None,
498                message: message.clone(),
499                tags: None,
500                data: None,
501            });
502        }
503
504        diagnostics
505    }
506
507    fn severity(&self) -> DiagnosticSeverity {
508        DiagnosticSeverity::ERROR
509    }
510}
511
512/// This is different than to_string() in that it will serialize the Error
513/// the struct as JSON so we can deserialize it on the js side.
514impl From<KclError> for String {
515    fn from(error: KclError) -> Self {
516        serde_json::to_string(&error).unwrap()
517    }
518}
519
520impl From<String> for KclError {
521    fn from(error: String) -> Self {
522        serde_json::from_str(&error).unwrap()
523    }
524}
525
526#[cfg(feature = "pyo3")]
527impl From<pyo3::PyErr> for KclError {
528    fn from(error: pyo3::PyErr) -> Self {
529        KclError::Internal(KclErrorDetails {
530            source_ranges: vec![],
531            message: error.to_string(),
532        })
533    }
534}
535
536#[cfg(feature = "pyo3")]
537impl From<KclError> for pyo3::PyErr {
538    fn from(error: KclError) -> Self {
539        pyo3::exceptions::PyException::new_err(error.to_string())
540    }
541}
542
543/// An error which occurred during parsing, etc.
544#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
545#[ts(export)]
546pub struct CompilationError {
547    #[serde(rename = "sourceRange")]
548    pub source_range: SourceRange,
549    pub message: String,
550    pub suggestion: Option<Suggestion>,
551    pub severity: Severity,
552    pub tag: Tag,
553}
554
555impl CompilationError {
556    pub(crate) fn err(source_range: SourceRange, message: impl ToString) -> CompilationError {
557        CompilationError {
558            source_range,
559            message: message.to_string(),
560            suggestion: None,
561            severity: Severity::Error,
562            tag: Tag::None,
563        }
564    }
565
566    pub(crate) fn fatal(source_range: SourceRange, message: impl ToString) -> CompilationError {
567        CompilationError {
568            source_range,
569            message: message.to_string(),
570            suggestion: None,
571            severity: Severity::Fatal,
572            tag: Tag::None,
573        }
574    }
575
576    pub(crate) fn with_suggestion(
577        self,
578        suggestion_title: impl ToString,
579        suggestion_insert: impl ToString,
580        // Will use the error source range if none is supplied
581        source_range: Option<SourceRange>,
582        tag: Tag,
583    ) -> CompilationError {
584        CompilationError {
585            suggestion: Some(Suggestion {
586                title: suggestion_title.to_string(),
587                insert: suggestion_insert.to_string(),
588                source_range: source_range.unwrap_or(self.source_range),
589            }),
590            tag,
591            ..self
592        }
593    }
594
595    #[cfg(test)]
596    pub fn apply_suggestion(&self, src: &str) -> Option<String> {
597        let suggestion = self.suggestion.as_ref()?;
598        Some(format!(
599            "{}{}{}",
600            &src[0..suggestion.source_range.start()],
601            suggestion.insert,
602            &src[suggestion.source_range.end()..]
603        ))
604    }
605}
606
607impl From<CompilationError> for KclErrorDetails {
608    fn from(err: CompilationError) -> Self {
609        KclErrorDetails {
610            source_ranges: vec![err.source_range],
611            message: err.message,
612        }
613    }
614}
615
616#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, ts_rs::TS)]
617#[ts(export)]
618pub enum Severity {
619    Warning,
620    Error,
621    Fatal,
622}
623
624impl Severity {
625    pub fn is_err(self) -> bool {
626        match self {
627            Severity::Warning => false,
628            Severity::Error | Severity::Fatal => true,
629        }
630    }
631}
632
633#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, ts_rs::TS)]
634#[ts(export)]
635pub enum Tag {
636    Deprecated,
637    Unnecessary,
638    None,
639}
640
641#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
642#[ts(export)]
643pub struct Suggestion {
644    pub title: String,
645    pub insert: String,
646    pub source_range: SourceRange,
647}