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