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