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    execution::DefaultPlanes,
11    lsp::IntoDiagnostic,
12    modules::{ModulePath, ModuleSource},
13    source_range::SourceRange,
14    ModuleId,
15};
16
17#[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#[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#[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: {0:?}")]
95    Lexical(KclErrorDetails),
96    #[error("syntax: {0:?}")]
97    Syntax(KclErrorDetails),
98    #[error("semantic: {0:?}")]
99    Semantic(KclErrorDetails),
100    #[error("import cycle: {0:?}")]
101    ImportCycle(KclErrorDetails),
102    #[error("type: {0:?}")]
103    Type(KclErrorDetails),
104    #[error("i/o: {0:?}")]
105    Io(KclErrorDetails),
106    #[error("unexpected: {0:?}")]
107    Unexpected(KclErrorDetails),
108    #[error("value already defined: {0:?}")]
109    ValueAlreadyDefined(KclErrorDetails),
110    #[error("undefined value: {0:?}")]
111    UndefinedValue(KclErrorDetails),
112    #[error("invalid expression: {0:?}")]
113    InvalidExpression(KclErrorDetails),
114    #[error("engine: {0:?}")]
115    Engine(KclErrorDetails),
116    #[error("internal error, please report to KittyCAD team: {0:?}")]
117    Internal(KclErrorDetails),
118}
119
120impl From<KclErrorWithOutputs> for KclError {
121    fn from(error: KclErrorWithOutputs) -> Self {
122        error.error
123    }
124}
125
126#[derive(Error, Debug, Serialize, ts_rs::TS, Clone, PartialEq)]
127#[error("{error}")]
128#[ts(export)]
129#[serde(rename_all = "camelCase")]
130pub struct KclErrorWithOutputs {
131    pub error: KclError,
132    pub non_fatal: Vec<CompilationError>,
133    #[cfg(feature = "artifact-graph")]
134    pub operations: Vec<Operation>,
135    #[cfg(feature = "artifact-graph")]
136    pub artifact_commands: Vec<ArtifactCommand>,
137    #[cfg(feature = "artifact-graph")]
138    pub artifact_graph: ArtifactGraph,
139    pub filenames: IndexMap<ModuleId, ModulePath>,
140    pub source_files: IndexMap<ModuleId, ModuleSource>,
141    pub default_planes: Option<DefaultPlanes>,
142}
143
144impl KclErrorWithOutputs {
145    #[allow(clippy::too_many_arguments)]
146    pub fn new(
147        error: KclError,
148        non_fatal: Vec<CompilationError>,
149        #[cfg(feature = "artifact-graph")] operations: Vec<Operation>,
150        #[cfg(feature = "artifact-graph")] artifact_commands: Vec<ArtifactCommand>,
151        #[cfg(feature = "artifact-graph")] artifact_graph: ArtifactGraph,
152        filenames: IndexMap<ModuleId, ModulePath>,
153        source_files: IndexMap<ModuleId, ModuleSource>,
154        default_planes: Option<DefaultPlanes>,
155    ) -> Self {
156        Self {
157            error,
158            non_fatal,
159            #[cfg(feature = "artifact-graph")]
160            operations,
161            #[cfg(feature = "artifact-graph")]
162            artifact_commands,
163            #[cfg(feature = "artifact-graph")]
164            artifact_graph,
165            filenames,
166            source_files,
167            default_planes,
168        }
169    }
170    pub fn no_outputs(error: KclError) -> Self {
171        Self {
172            error,
173            non_fatal: Default::default(),
174            #[cfg(feature = "artifact-graph")]
175            operations: Default::default(),
176            #[cfg(feature = "artifact-graph")]
177            artifact_commands: Default::default(),
178            #[cfg(feature = "artifact-graph")]
179            artifact_graph: Default::default(),
180            filenames: Default::default(),
181            source_files: Default::default(),
182            default_planes: Default::default(),
183        }
184    }
185    pub fn into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
186        let mut source_ranges = self.error.source_ranges();
187
188        let first_source_range = source_ranges
190            .pop()
191            .ok_or_else(|| anyhow::anyhow!("No source ranges found"))?;
192
193        let source = self
194            .source_files
195            .get(&first_source_range.module_id())
196            .cloned()
197            .unwrap_or(ModuleSource {
198                source: code.to_string(),
199                path: self
200                    .filenames
201                    .get(&first_source_range.module_id())
202                    .cloned()
203                    .unwrap_or(ModulePath::Main),
204            });
205        let filename = source.path.to_string();
206        let kcl_source = source.source.to_string();
207
208        let mut related = Vec::new();
209        for source_range in source_ranges {
210            let module_id = source_range.module_id();
211            let source = self.source_files.get(&module_id).cloned().unwrap_or(ModuleSource {
212                source: code.to_string(),
213                path: self.filenames.get(&module_id).cloned().unwrap_or(ModulePath::Main),
214            });
215            let error = self.error.override_source_ranges(vec![source_range]);
216            let report = Report {
217                error,
218                kcl_source: source.source.to_string(),
219                filename: source.path.to_string(),
220            };
221            related.push(report);
222        }
223
224        Ok(ReportWithOutputs {
225            error: self,
226            kcl_source,
227            filename,
228            related,
229        })
230    }
231}
232
233impl IntoDiagnostic for KclErrorWithOutputs {
234    fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
235        let message = self.error.get_message();
236        let source_ranges = self.error.source_ranges();
237
238        source_ranges
239            .into_iter()
240            .map(|source_range| {
241                let source = self
242                    .source_files
243                    .get(&source_range.module_id())
244                    .cloned()
245                    .unwrap_or(ModuleSource {
246                        source: code.to_string(),
247                        path: self.filenames.get(&source_range.module_id()).unwrap().clone(),
248                    });
249                let mut filename = source.path.to_string();
250                if !filename.starts_with("file://") {
251                    filename = format!("file:///{}", filename.trim_start_matches("/"));
252                }
253
254                let related_information = if let Ok(uri) = url::Url::parse(&filename) {
255                    Some(vec![tower_lsp::lsp_types::DiagnosticRelatedInformation {
256                        location: tower_lsp::lsp_types::Location {
257                            uri,
258                            range: source_range.to_lsp_range(&source.source),
259                        },
260                        message: message.to_string(),
261                    }])
262                } else {
263                    None
264                };
265
266                Diagnostic {
267                    range: source_range.to_lsp_range(code),
268                    severity: Some(self.severity()),
269                    code: None,
270                    code_description: None,
272                    source: Some("kcl".to_string()),
273                    related_information,
274                    message: message.clone(),
275                    tags: None,
276                    data: None,
277                }
278            })
279            .collect()
280    }
281
282    fn severity(&self) -> DiagnosticSeverity {
283        DiagnosticSeverity::ERROR
284    }
285}
286
287#[derive(thiserror::Error, Debug)]
288#[error("{}", self.error.error.get_message())]
289pub struct ReportWithOutputs {
290    pub error: KclErrorWithOutputs,
291    pub kcl_source: String,
292    pub filename: String,
293    pub related: Vec<Report>,
294}
295
296impl miette::Diagnostic for ReportWithOutputs {
297    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
298        let family = match self.error.error {
299            KclError::Lexical(_) => "Lexical",
300            KclError::Syntax(_) => "Syntax",
301            KclError::Semantic(_) => "Semantic",
302            KclError::ImportCycle(_) => "ImportCycle",
303            KclError::Type(_) => "Type",
304            KclError::Io(_) => "I/O",
305            KclError::Unexpected(_) => "Unexpected",
306            KclError::ValueAlreadyDefined(_) => "ValueAlreadyDefined",
307            KclError::UndefinedValue(_) => "UndefinedValue",
308            KclError::InvalidExpression(_) => "InvalidExpression",
309            KclError::Engine(_) => "Engine",
310            KclError::Internal(_) => "Internal",
311        };
312        let error_string = format!("KCL {family} error");
313        Some(Box::new(error_string))
314    }
315
316    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
317        Some(&self.kcl_source)
318    }
319
320    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
321        let iter = self
322            .error
323            .error
324            .source_ranges()
325            .clone()
326            .into_iter()
327            .map(miette::SourceSpan::from)
328            .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
329        Some(Box::new(iter))
330    }
331
332    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
333        let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
334        Some(Box::new(iter))
335    }
336}
337
338#[derive(thiserror::Error, Debug)]
339#[error("{}", self.error.get_message())]
340pub struct Report {
341    pub error: KclError,
342    pub kcl_source: String,
343    pub filename: String,
344}
345
346impl miette::Diagnostic for Report {
347    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
348        let family = match self.error {
349            KclError::Lexical(_) => "Lexical",
350            KclError::Syntax(_) => "Syntax",
351            KclError::Semantic(_) => "Semantic",
352            KclError::ImportCycle(_) => "ImportCycle",
353            KclError::Type(_) => "Type",
354            KclError::Io(_) => "I/O",
355            KclError::Unexpected(_) => "Unexpected",
356            KclError::ValueAlreadyDefined(_) => "ValueAlreadyDefined",
357            KclError::UndefinedValue(_) => "UndefinedValue",
358            KclError::InvalidExpression(_) => "InvalidExpression",
359            KclError::Engine(_) => "Engine",
360            KclError::Internal(_) => "Internal",
361        };
362        let error_string = format!("KCL {family} error");
363        Some(Box::new(error_string))
364    }
365
366    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
367        Some(&self.kcl_source)
368    }
369
370    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
371        let iter = self
372            .error
373            .source_ranges()
374            .clone()
375            .into_iter()
376            .map(miette::SourceSpan::from)
377            .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
378        Some(Box::new(iter))
379    }
380}
381
382#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
383#[error("{message}")]
384#[ts(export)]
385pub struct KclErrorDetails {
386    #[serde(rename = "sourceRanges")]
387    #[label(collection, "Errors")]
388    pub source_ranges: Vec<SourceRange>,
389    #[serde(rename = "msg")]
390    pub message: String,
391}
392
393impl KclError {
394    pub fn internal(message: String) -> KclError {
395        KclError::Internal(KclErrorDetails {
396            source_ranges: Default::default(),
397            message,
398        })
399    }
400
401    pub fn get_message(&self) -> String {
403        format!("{}: {}", self.error_type(), self.message())
404    }
405
406    pub fn error_type(&self) -> &'static str {
407        match self {
408            KclError::Lexical(_) => "lexical",
409            KclError::Syntax(_) => "syntax",
410            KclError::Semantic(_) => "semantic",
411            KclError::ImportCycle(_) => "import cycle",
412            KclError::Type(_) => "type",
413            KclError::Io(_) => "i/o",
414            KclError::Unexpected(_) => "unexpected",
415            KclError::ValueAlreadyDefined(_) => "value already defined",
416            KclError::UndefinedValue(_) => "undefined value",
417            KclError::InvalidExpression(_) => "invalid expression",
418            KclError::Engine(_) => "engine",
419            KclError::Internal(_) => "internal",
420        }
421    }
422
423    pub fn source_ranges(&self) -> Vec<SourceRange> {
424        match &self {
425            KclError::Lexical(e) => e.source_ranges.clone(),
426            KclError::Syntax(e) => e.source_ranges.clone(),
427            KclError::Semantic(e) => e.source_ranges.clone(),
428            KclError::ImportCycle(e) => e.source_ranges.clone(),
429            KclError::Type(e) => e.source_ranges.clone(),
430            KclError::Io(e) => e.source_ranges.clone(),
431            KclError::Unexpected(e) => e.source_ranges.clone(),
432            KclError::ValueAlreadyDefined(e) => e.source_ranges.clone(),
433            KclError::UndefinedValue(e) => e.source_ranges.clone(),
434            KclError::InvalidExpression(e) => e.source_ranges.clone(),
435            KclError::Engine(e) => e.source_ranges.clone(),
436            KclError::Internal(e) => e.source_ranges.clone(),
437        }
438    }
439
440    pub fn message(&self) -> &str {
442        match &self {
443            KclError::Lexical(e) => &e.message,
444            KclError::Syntax(e) => &e.message,
445            KclError::Semantic(e) => &e.message,
446            KclError::ImportCycle(e) => &e.message,
447            KclError::Type(e) => &e.message,
448            KclError::Io(e) => &e.message,
449            KclError::Unexpected(e) => &e.message,
450            KclError::ValueAlreadyDefined(e) => &e.message,
451            KclError::UndefinedValue(e) => &e.message,
452            KclError::InvalidExpression(e) => &e.message,
453            KclError::Engine(e) => &e.message,
454            KclError::Internal(e) => &e.message,
455        }
456    }
457
458    pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
459        let mut new = self.clone();
460        match &mut new {
461            KclError::Lexical(e) => e.source_ranges = source_ranges,
462            KclError::Syntax(e) => e.source_ranges = source_ranges,
463            KclError::Semantic(e) => e.source_ranges = source_ranges,
464            KclError::ImportCycle(e) => e.source_ranges = source_ranges,
465            KclError::Type(e) => e.source_ranges = source_ranges,
466            KclError::Io(e) => e.source_ranges = source_ranges,
467            KclError::Unexpected(e) => e.source_ranges = source_ranges,
468            KclError::ValueAlreadyDefined(e) => e.source_ranges = source_ranges,
469            KclError::UndefinedValue(e) => e.source_ranges = source_ranges,
470            KclError::InvalidExpression(e) => e.source_ranges = source_ranges,
471            KclError::Engine(e) => e.source_ranges = source_ranges,
472            KclError::Internal(e) => e.source_ranges = source_ranges,
473        }
474
475        new
476    }
477
478    pub(crate) fn add_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
479        let mut new = self.clone();
480        match &mut new {
481            KclError::Lexical(e) => e.source_ranges.extend(source_ranges),
482            KclError::Syntax(e) => e.source_ranges.extend(source_ranges),
483            KclError::Semantic(e) => e.source_ranges.extend(source_ranges),
484            KclError::ImportCycle(e) => e.source_ranges.extend(source_ranges),
485            KclError::Type(e) => e.source_ranges.extend(source_ranges),
486            KclError::Io(e) => e.source_ranges.extend(source_ranges),
487            KclError::Unexpected(e) => e.source_ranges.extend(source_ranges),
488            KclError::ValueAlreadyDefined(e) => e.source_ranges.extend(source_ranges),
489            KclError::UndefinedValue(e) => e.source_ranges.extend(source_ranges),
490            KclError::InvalidExpression(e) => e.source_ranges.extend(source_ranges),
491            KclError::Engine(e) => e.source_ranges.extend(source_ranges),
492            KclError::Internal(e) => e.source_ranges.extend(source_ranges),
493        }
494
495        new
496    }
497}
498
499impl IntoDiagnostic for KclError {
500    fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
501        let message = self.get_message();
502        let source_ranges = self.source_ranges();
503
504        let module_id = ModuleId::default();
506        let source_ranges = source_ranges
507            .iter()
508            .filter(|r| r.module_id() == module_id)
509            .collect::<Vec<_>>();
510
511        let mut diagnostics = Vec::new();
512        for source_range in &source_ranges {
513            diagnostics.push(Diagnostic {
514                range: source_range.to_lsp_range(code),
515                severity: Some(self.severity()),
516                code: None,
517                code_description: None,
519                source: Some("kcl".to_string()),
520                related_information: None,
521                message: message.clone(),
522                tags: None,
523                data: None,
524            });
525        }
526
527        diagnostics
528    }
529
530    fn severity(&self) -> DiagnosticSeverity {
531        DiagnosticSeverity::ERROR
532    }
533}
534
535impl From<KclError> for String {
538    fn from(error: KclError) -> Self {
539        serde_json::to_string(&error).unwrap()
540    }
541}
542
543impl From<String> for KclError {
544    fn from(error: String) -> Self {
545        serde_json::from_str(&error).unwrap()
546    }
547}
548
549#[cfg(feature = "pyo3")]
550impl From<pyo3::PyErr> for KclError {
551    fn from(error: pyo3::PyErr) -> Self {
552        KclError::Internal(KclErrorDetails {
553            source_ranges: vec![],
554            message: error.to_string(),
555        })
556    }
557}
558
559#[cfg(feature = "pyo3")]
560impl From<KclError> for pyo3::PyErr {
561    fn from(error: KclError) -> Self {
562        pyo3::exceptions::PyException::new_err(error.to_string())
563    }
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS, PartialEq, Eq)]
568#[ts(export)]
569pub struct CompilationError {
570    #[serde(rename = "sourceRange")]
571    pub source_range: SourceRange,
572    pub message: String,
573    pub suggestion: Option<Suggestion>,
574    pub severity: Severity,
575    pub tag: Tag,
576}
577
578impl CompilationError {
579    pub(crate) fn err(source_range: SourceRange, message: impl ToString) -> CompilationError {
580        CompilationError {
581            source_range,
582            message: message.to_string(),
583            suggestion: None,
584            severity: Severity::Error,
585            tag: Tag::None,
586        }
587    }
588
589    pub(crate) fn fatal(source_range: SourceRange, message: impl ToString) -> CompilationError {
590        CompilationError {
591            source_range,
592            message: message.to_string(),
593            suggestion: None,
594            severity: Severity::Fatal,
595            tag: Tag::None,
596        }
597    }
598
599    pub(crate) fn with_suggestion(
600        self,
601        suggestion_title: impl ToString,
602        suggestion_insert: impl ToString,
603        source_range: Option<SourceRange>,
605        tag: Tag,
606    ) -> CompilationError {
607        CompilationError {
608            suggestion: Some(Suggestion {
609                title: suggestion_title.to_string(),
610                insert: suggestion_insert.to_string(),
611                source_range: source_range.unwrap_or(self.source_range),
612            }),
613            tag,
614            ..self
615        }
616    }
617
618    #[cfg(test)]
619    pub fn apply_suggestion(&self, src: &str) -> Option<String> {
620        let suggestion = self.suggestion.as_ref()?;
621        Some(format!(
622            "{}{}{}",
623            &src[0..suggestion.source_range.start()],
624            suggestion.insert,
625            &src[suggestion.source_range.end()..]
626        ))
627    }
628}
629
630impl From<CompilationError> for KclErrorDetails {
631    fn from(err: CompilationError) -> Self {
632        KclErrorDetails {
633            source_ranges: vec![err.source_range],
634            message: err.message,
635        }
636    }
637}
638
639#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, ts_rs::TS)]
640#[ts(export)]
641pub enum Severity {
642    Warning,
643    Error,
644    Fatal,
645}
646
647impl Severity {
648    pub fn is_err(self) -> bool {
649        match self {
650            Severity::Warning => false,
651            Severity::Error | Severity::Fatal => true,
652        }
653    }
654}
655
656#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, ts_rs::TS)]
657#[ts(export)]
658pub enum Tag {
659    Deprecated,
660    Unnecessary,
661    None,
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS, PartialEq, Eq, JsonSchema)]
665#[ts(export)]
666pub struct Suggestion {
667    pub title: String,
668    pub insert: String,
669    pub source_range: SourceRange,
670}
671
672pub type LspSuggestion = (Suggestion, tower_lsp::lsp_types::Range);
673
674impl Suggestion {
675    pub fn to_lsp_edit(&self, code: &str) -> LspSuggestion {
676        let range = self.source_range.to_lsp_range(code);
677        (self.clone(), range)
678    }
679}