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