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