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