kcl_lib/
errors.rs

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