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