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