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