kcl_lib/
errors.rs

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