Skip to main content

kcl_lib/
errors.rs

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