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