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