Skip to main content

kcl_lib/
errors.rs

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