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