Skip to main content

kcl_lib/
errors.rs

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