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