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