1#[cfg(feature = "artifact-graph")]
2use std::collections::BTreeMap;
3
4use indexmap::IndexMap;
5pub use kcl_error::CompilationIssue;
6pub use kcl_error::Severity;
7pub use kcl_error::Suggestion;
8pub use kcl_error::Tag;
9use serde::Deserialize;
10use serde::Serialize;
11use thiserror::Error;
12use tower_lsp::lsp_types::Diagnostic;
13use tower_lsp::lsp_types::DiagnosticSeverity;
14
15use crate::ExecOutcome;
16use crate::ModuleId;
17use crate::SourceRange;
18use crate::exec::KclValue;
19#[cfg(feature = "artifact-graph")]
20use crate::execution::ArtifactCommand;
21#[cfg(feature = "artifact-graph")]
22use crate::execution::ArtifactGraph;
23use crate::execution::DefaultPlanes;
24#[cfg(feature = "artifact-graph")]
25use crate::execution::Operation;
26#[cfg(feature = "artifact-graph")]
27use crate::front::Number;
28#[cfg(feature = "artifact-graph")]
29use crate::front::Object;
30#[cfg(feature = "artifact-graph")]
31use crate::front::ObjectId;
32use crate::lsp::IntoDiagnostic;
33use crate::lsp::ToLspRange;
34use crate::modules::ModulePath;
35use crate::modules::ModuleSource;
36
37pub trait IsRetryable {
38 fn is_retryable(&self) -> bool;
41}
42
43#[derive(thiserror::Error, Debug)]
45pub enum ExecError {
46 #[error("{0}")]
47 Kcl(#[from] Box<crate::KclErrorWithOutputs>),
48 #[error("Could not connect to engine: {0}")]
49 Connection(#[from] ConnectionError),
50 #[error("PNG snapshot could not be decoded: {0}")]
51 BadPng(String),
52 #[error("Bad export: {0}")]
53 BadExport(String),
54}
55
56impl From<KclErrorWithOutputs> for ExecError {
57 fn from(error: KclErrorWithOutputs) -> Self {
58 ExecError::Kcl(Box::new(error))
59 }
60}
61
62#[derive(Debug, thiserror::Error)]
64#[error("{error}")]
65pub struct ExecErrorWithState {
66 pub error: ExecError,
67 pub exec_state: Option<crate::execution::ExecState>,
68}
69
70impl ExecErrorWithState {
71 #[cfg_attr(target_arch = "wasm32", expect(dead_code))]
72 pub fn new(error: ExecError, exec_state: crate::execution::ExecState) -> Self {
73 Self {
74 error,
75 exec_state: Some(exec_state),
76 }
77 }
78}
79
80impl IsRetryable for ExecErrorWithState {
81 fn is_retryable(&self) -> bool {
82 self.error.is_retryable()
83 }
84}
85
86impl ExecError {
87 pub fn as_kcl_error(&self) -> Option<&crate::KclError> {
88 let ExecError::Kcl(k) = &self else {
89 return None;
90 };
91 Some(&k.error)
92 }
93}
94
95impl IsRetryable for ExecError {
96 fn is_retryable(&self) -> bool {
97 matches!(self, ExecError::Kcl(kcl_error) if kcl_error.is_retryable())
98 }
99}
100
101impl From<ExecError> for ExecErrorWithState {
102 fn from(error: ExecError) -> Self {
103 Self {
104 error,
105 exec_state: None,
106 }
107 }
108}
109
110impl From<ConnectionError> for ExecErrorWithState {
111 fn from(error: ConnectionError) -> Self {
112 Self {
113 error: error.into(),
114 exec_state: None,
115 }
116 }
117}
118
119#[derive(thiserror::Error, Debug)]
121pub enum ConnectionError {
122 #[error("Could not create a Zoo client: {0}")]
123 CouldNotMakeClient(anyhow::Error),
124 #[error("Could not establish connection to engine: {0}")]
125 Establishing(anyhow::Error),
126}
127
128#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
129#[ts(export)]
130#[serde(tag = "kind", rename_all = "snake_case")]
131pub enum KclError {
132 #[error("lexical: {details:?}")]
133 Lexical { details: KclErrorDetails },
134 #[error("syntax: {details:?}")]
135 Syntax { details: KclErrorDetails },
136 #[error("semantic: {details:?}")]
137 Semantic { details: KclErrorDetails },
138 #[error("import cycle: {details:?}")]
139 ImportCycle { details: KclErrorDetails },
140 #[error("argument: {details:?}")]
141 Argument { details: KclErrorDetails },
142 #[error("type: {details:?}")]
143 Type { details: KclErrorDetails },
144 #[error("i/o: {details:?}")]
145 Io { details: KclErrorDetails },
146 #[error("unexpected: {details:?}")]
147 Unexpected { details: KclErrorDetails },
148 #[error("value already defined: {details:?}")]
149 ValueAlreadyDefined { details: KclErrorDetails },
150 #[error("undefined value: {details:?}")]
151 UndefinedValue {
152 details: KclErrorDetails,
153 name: Option<String>,
154 },
155 #[error("invalid expression: {details:?}")]
156 InvalidExpression { details: KclErrorDetails },
157 #[error("max call stack size exceeded: {details:?}")]
158 MaxCallStack { details: KclErrorDetails },
159 #[error("refactor: {details:?}")]
160 Refactor { details: KclErrorDetails },
161 #[error("engine: {details:?}")]
162 Engine { details: KclErrorDetails },
163 #[error("engine hangup: {details:?}")]
164 EngineHangup { details: KclErrorDetails },
165 #[error("engine internal: {details:?}")]
166 EngineInternal { details: KclErrorDetails },
167 #[error("internal error, please report to KittyCAD team: {details:?}")]
168 Internal { details: KclErrorDetails },
169}
170
171impl From<KclErrorWithOutputs> for KclError {
172 fn from(error: KclErrorWithOutputs) -> Self {
173 error.error
174 }
175}
176
177impl IsRetryable for KclError {
178 fn is_retryable(&self) -> bool {
179 matches!(self, KclError::EngineHangup { .. } | KclError::EngineInternal { .. })
180 }
181}
182
183#[derive(Error, Debug, Serialize, ts_rs::TS, Clone, PartialEq)]
184#[error("{error}")]
185#[ts(export)]
186#[serde(rename_all = "camelCase")]
187pub struct KclErrorWithOutputs {
188 pub error: KclError,
189 pub non_fatal: Vec<CompilationIssue>,
190 pub variables: IndexMap<String, KclValue>,
193 #[cfg(feature = "artifact-graph")]
194 pub operations: Vec<Operation>,
195 #[cfg(feature = "artifact-graph")]
198 pub _artifact_commands: Vec<ArtifactCommand>,
199 #[cfg(feature = "artifact-graph")]
200 pub artifact_graph: ArtifactGraph,
201 #[cfg(feature = "artifact-graph")]
202 #[serde(skip)]
203 pub scene_objects: Vec<Object>,
204 #[cfg(feature = "artifact-graph")]
205 #[serde(skip)]
206 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
207 #[cfg(feature = "artifact-graph")]
208 #[serde(skip)]
209 pub var_solutions: Vec<(SourceRange, Number)>,
210 pub scene_graph: Option<crate::front::SceneGraph>,
211 pub filenames: IndexMap<ModuleId, ModulePath>,
212 pub source_files: IndexMap<ModuleId, ModuleSource>,
213 pub default_planes: Option<DefaultPlanes>,
214}
215
216impl KclErrorWithOutputs {
217 #[allow(clippy::too_many_arguments)]
218 pub fn new(
219 error: KclError,
220 non_fatal: Vec<CompilationIssue>,
221 variables: IndexMap<String, KclValue>,
222 #[cfg(feature = "artifact-graph")] operations: Vec<Operation>,
223 #[cfg(feature = "artifact-graph")] artifact_commands: Vec<ArtifactCommand>,
224 #[cfg(feature = "artifact-graph")] artifact_graph: ArtifactGraph,
225 #[cfg(feature = "artifact-graph")] scene_objects: Vec<Object>,
226 #[cfg(feature = "artifact-graph")] source_range_to_object: BTreeMap<SourceRange, ObjectId>,
227 #[cfg(feature = "artifact-graph")] var_solutions: Vec<(SourceRange, Number)>,
228 filenames: IndexMap<ModuleId, ModulePath>,
229 source_files: IndexMap<ModuleId, ModuleSource>,
230 default_planes: Option<DefaultPlanes>,
231 ) -> Self {
232 Self {
233 error,
234 non_fatal,
235 variables,
236 #[cfg(feature = "artifact-graph")]
237 operations,
238 #[cfg(feature = "artifact-graph")]
239 _artifact_commands: artifact_commands,
240 #[cfg(feature = "artifact-graph")]
241 artifact_graph,
242 #[cfg(feature = "artifact-graph")]
243 scene_objects,
244 #[cfg(feature = "artifact-graph")]
245 source_range_to_object,
246 #[cfg(feature = "artifact-graph")]
247 var_solutions,
248 scene_graph: Default::default(),
249 filenames,
250 source_files,
251 default_planes,
252 }
253 }
254
255 pub fn no_outputs(error: KclError) -> Self {
256 Self {
257 error,
258 non_fatal: Default::default(),
259 variables: Default::default(),
260 #[cfg(feature = "artifact-graph")]
261 operations: Default::default(),
262 #[cfg(feature = "artifact-graph")]
263 _artifact_commands: Default::default(),
264 #[cfg(feature = "artifact-graph")]
265 artifact_graph: Default::default(),
266 #[cfg(feature = "artifact-graph")]
267 scene_objects: Default::default(),
268 #[cfg(feature = "artifact-graph")]
269 source_range_to_object: Default::default(),
270 #[cfg(feature = "artifact-graph")]
271 var_solutions: Default::default(),
272 scene_graph: Default::default(),
273 filenames: Default::default(),
274 source_files: Default::default(),
275 default_planes: Default::default(),
276 }
277 }
278
279 pub fn from_error_outcome(error: KclError, outcome: ExecOutcome) -> Self {
281 KclErrorWithOutputs {
282 error,
283 non_fatal: outcome.issues,
284 variables: outcome.variables,
285 #[cfg(feature = "artifact-graph")]
286 operations: outcome.operations,
287 #[cfg(feature = "artifact-graph")]
288 _artifact_commands: Default::default(),
289 #[cfg(feature = "artifact-graph")]
290 artifact_graph: outcome.artifact_graph,
291 #[cfg(feature = "artifact-graph")]
292 scene_objects: outcome.scene_objects,
293 #[cfg(feature = "artifact-graph")]
294 source_range_to_object: outcome.source_range_to_object,
295 #[cfg(feature = "artifact-graph")]
296 var_solutions: outcome.var_solutions,
297 scene_graph: Default::default(),
298 filenames: outcome.filenames,
299 source_files: Default::default(),
300 default_planes: outcome.default_planes,
301 }
302 }
303
304 pub fn into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
305 let mut source_ranges = self.error.source_ranges();
306
307 let first_source_range = source_ranges
309 .pop()
310 .ok_or_else(|| anyhow::anyhow!("No source ranges found"))?;
311
312 let source = self
313 .source_files
314 .get(&first_source_range.module_id())
315 .cloned()
316 .unwrap_or(ModuleSource {
317 source: code.to_string(),
318 path: self
319 .filenames
320 .get(&first_source_range.module_id())
321 .cloned()
322 .unwrap_or(ModulePath::Main),
323 });
324 let filename = source.path.to_string();
325 let kcl_source = source.source;
326
327 let mut related = Vec::new();
328 for source_range in source_ranges {
329 let module_id = source_range.module_id();
330 let source = self.source_files.get(&module_id).cloned().unwrap_or(ModuleSource {
331 source: code.to_string(),
332 path: self.filenames.get(&module_id).cloned().unwrap_or(ModulePath::Main),
333 });
334 let error = self.error.override_source_ranges(vec![source_range]);
335 let report = Report {
336 error,
337 kcl_source: source.source.to_string(),
338 filename: source.path.to_string(),
339 };
340 related.push(report);
341 }
342
343 Ok(ReportWithOutputs {
344 error: self,
345 kcl_source,
346 filename,
347 related,
348 })
349 }
350}
351
352impl IsRetryable for KclErrorWithOutputs {
353 fn is_retryable(&self) -> bool {
354 matches!(
355 self.error,
356 KclError::EngineHangup { .. } | KclError::EngineInternal { .. }
357 )
358 }
359}
360
361impl IntoDiagnostic for KclErrorWithOutputs {
362 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
363 let message = self.error.get_message();
364 let source_ranges = self.error.source_ranges();
365
366 source_ranges
367 .into_iter()
368 .map(|source_range| {
369 let source = self.source_files.get(&source_range.module_id()).cloned().or_else(|| {
370 self.filenames
371 .get(&source_range.module_id())
372 .cloned()
373 .map(|path| ModuleSource {
374 source: code.to_string(),
375 path,
376 })
377 });
378
379 let related_information = source.and_then(|source| {
380 let mut filename = source.path.to_string();
381 if !filename.starts_with("file://") {
382 filename = format!("file:///{}", filename.trim_start_matches("/"));
383 }
384
385 url::Url::parse(&filename).ok().map(|uri| {
386 vec![tower_lsp::lsp_types::DiagnosticRelatedInformation {
387 location: tower_lsp::lsp_types::Location {
388 uri,
389 range: source_range.to_lsp_range(&source.source),
390 },
391 message: message.to_string(),
392 }]
393 })
394 });
395
396 Diagnostic {
397 range: source_range.to_lsp_range(code),
398 severity: Some(self.severity()),
399 code: None,
400 code_description: None,
402 source: Some("kcl".to_string()),
403 related_information,
404 message: message.clone(),
405 tags: None,
406 data: None,
407 }
408 })
409 .collect()
410 }
411
412 fn severity(&self) -> DiagnosticSeverity {
413 DiagnosticSeverity::ERROR
414 }
415}
416
417#[derive(thiserror::Error, Debug)]
418#[error("{}", self.error.error.get_message())]
419pub struct ReportWithOutputs {
420 pub error: KclErrorWithOutputs,
421 pub kcl_source: String,
422 pub filename: String,
423 pub related: Vec<Report>,
424}
425
426impl miette::Diagnostic for ReportWithOutputs {
427 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
428 let family = match self.error.error {
429 KclError::Lexical { .. } => "Lexical",
430 KclError::Syntax { .. } => "Syntax",
431 KclError::Semantic { .. } => "Semantic",
432 KclError::ImportCycle { .. } => "ImportCycle",
433 KclError::Argument { .. } => "Argument",
434 KclError::Type { .. } => "Type",
435 KclError::Io { .. } => "I/O",
436 KclError::Unexpected { .. } => "Unexpected",
437 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
438 KclError::UndefinedValue { .. } => "UndefinedValue",
439 KclError::InvalidExpression { .. } => "InvalidExpression",
440 KclError::MaxCallStack { .. } => "MaxCallStack",
441 KclError::Refactor { .. } => "Refactor",
442 KclError::Engine { .. } => "Engine",
443 KclError::EngineHangup { .. } => "EngineHangup",
444 KclError::EngineInternal { .. } => "EngineInternal",
445 KclError::Internal { .. } => "Internal",
446 };
447 let error_string = format!("KCL {family} error");
448 Some(Box::new(error_string))
449 }
450
451 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
452 Some(&self.kcl_source)
453 }
454
455 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
456 let iter = self
457 .error
458 .error
459 .source_ranges()
460 .into_iter()
461 .map(miette::SourceSpan::from)
462 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
463 Some(Box::new(iter))
464 }
465
466 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
467 let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
468 Some(Box::new(iter))
469 }
470}
471
472#[derive(thiserror::Error, Debug)]
473#[error("{}", self.error.get_message())]
474pub struct Report {
475 pub error: KclError,
476 pub kcl_source: String,
477 pub filename: String,
478}
479
480impl miette::Diagnostic for Report {
481 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
482 let family = match self.error {
483 KclError::Lexical { .. } => "Lexical",
484 KclError::Syntax { .. } => "Syntax",
485 KclError::Semantic { .. } => "Semantic",
486 KclError::ImportCycle { .. } => "ImportCycle",
487 KclError::Argument { .. } => "Argument",
488 KclError::Type { .. } => "Type",
489 KclError::Io { .. } => "I/O",
490 KclError::Unexpected { .. } => "Unexpected",
491 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
492 KclError::UndefinedValue { .. } => "UndefinedValue",
493 KclError::InvalidExpression { .. } => "InvalidExpression",
494 KclError::MaxCallStack { .. } => "MaxCallStack",
495 KclError::Refactor { .. } => "Refactor",
496 KclError::Engine { .. } => "Engine",
497 KclError::EngineHangup { .. } => "EngineHangup",
498 KclError::EngineInternal { .. } => "EngineInternal",
499 KclError::Internal { .. } => "Internal",
500 };
501 let error_string = format!("KCL {family} error");
502 Some(Box::new(error_string))
503 }
504
505 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
506 Some(&self.kcl_source)
507 }
508
509 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
510 let iter = self
511 .error
512 .source_ranges()
513 .into_iter()
514 .map(miette::SourceSpan::from)
515 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
516 Some(Box::new(iter))
517 }
518}
519
520#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
521#[serde(rename_all = "camelCase")]
522#[error("{message}")]
523#[ts(export)]
524pub struct KclErrorDetails {
525 #[label(collection, "Errors")]
526 pub source_ranges: Vec<SourceRange>,
527 pub backtrace: Vec<super::BacktraceItem>,
528 #[serde(rename = "msg")]
529 pub message: String,
530}
531
532impl KclErrorDetails {
533 pub fn new(message: String, source_ranges: Vec<SourceRange>) -> KclErrorDetails {
534 let backtrace = source_ranges
535 .iter()
536 .map(|s| BacktraceItem {
537 source_range: *s,
538 fn_name: None,
539 })
540 .collect();
541 KclErrorDetails {
542 source_ranges,
543 backtrace,
544 message,
545 }
546 }
547}
548
549impl KclError {
550 pub fn internal(message: String) -> KclError {
551 KclError::Internal {
552 details: KclErrorDetails {
553 source_ranges: Default::default(),
554 backtrace: Default::default(),
555 message,
556 },
557 }
558 }
559
560 pub fn new_internal(details: KclErrorDetails) -> KclError {
561 KclError::Internal { details }
562 }
563
564 pub fn new_import_cycle(details: KclErrorDetails) -> KclError {
565 KclError::ImportCycle { details }
566 }
567
568 pub fn new_argument(details: KclErrorDetails) -> KclError {
569 KclError::Argument { details }
570 }
571
572 pub fn new_semantic(details: KclErrorDetails) -> KclError {
573 KclError::Semantic { details }
574 }
575
576 pub fn new_value_already_defined(details: KclErrorDetails) -> KclError {
577 KclError::ValueAlreadyDefined { details }
578 }
579
580 pub fn new_syntax(details: KclErrorDetails) -> KclError {
581 KclError::Syntax { details }
582 }
583
584 pub fn new_io(details: KclErrorDetails) -> KclError {
585 KclError::Io { details }
586 }
587
588 pub fn new_invalid_expression(details: KclErrorDetails) -> KclError {
589 KclError::InvalidExpression { details }
590 }
591
592 pub fn refactor(message: String) -> KclError {
593 KclError::Refactor {
594 details: KclErrorDetails {
595 source_ranges: Default::default(),
596 backtrace: Default::default(),
597 message,
598 },
599 }
600 }
601
602 pub fn new_engine(details: KclErrorDetails) -> KclError {
603 if details.message.eq_ignore_ascii_case("internal error") {
604 KclError::EngineInternal { details }
605 } else {
606 KclError::Engine { details }
607 }
608 }
609
610 pub fn new_engine_hangup(details: KclErrorDetails) -> KclError {
611 KclError::EngineHangup { details }
612 }
613
614 pub fn new_lexical(details: KclErrorDetails) -> KclError {
615 KclError::Lexical { details }
616 }
617
618 pub fn new_undefined_value(details: KclErrorDetails, name: Option<String>) -> KclError {
619 KclError::UndefinedValue { details, name }
620 }
621
622 pub fn new_type(details: KclErrorDetails) -> KclError {
623 KclError::Type { details }
624 }
625
626 pub fn get_message(&self) -> String {
628 format!("{}: {}", self.error_type(), self.message())
629 }
630
631 pub fn error_type(&self) -> &'static str {
632 match self {
633 KclError::Lexical { .. } => "lexical",
634 KclError::Syntax { .. } => "syntax",
635 KclError::Semantic { .. } => "semantic",
636 KclError::ImportCycle { .. } => "import cycle",
637 KclError::Argument { .. } => "argument",
638 KclError::Type { .. } => "type",
639 KclError::Io { .. } => "i/o",
640 KclError::Unexpected { .. } => "unexpected",
641 KclError::ValueAlreadyDefined { .. } => "value already defined",
642 KclError::UndefinedValue { .. } => "undefined value",
643 KclError::InvalidExpression { .. } => "invalid expression",
644 KclError::MaxCallStack { .. } => "max call stack",
645 KclError::Refactor { .. } => "refactor",
646 KclError::Engine { .. } => "engine",
647 KclError::EngineHangup { .. } => "engine hangup",
648 KclError::EngineInternal { .. } => "engine internal",
649 KclError::Internal { .. } => "internal",
650 }
651 }
652
653 pub fn source_ranges(&self) -> Vec<SourceRange> {
654 match &self {
655 KclError::Lexical { details: e } => e.source_ranges.clone(),
656 KclError::Syntax { details: e } => e.source_ranges.clone(),
657 KclError::Semantic { details: e } => e.source_ranges.clone(),
658 KclError::ImportCycle { details: e } => e.source_ranges.clone(),
659 KclError::Argument { details: e } => e.source_ranges.clone(),
660 KclError::Type { details: e } => e.source_ranges.clone(),
661 KclError::Io { details: e } => e.source_ranges.clone(),
662 KclError::Unexpected { details: e } => e.source_ranges.clone(),
663 KclError::ValueAlreadyDefined { details: e } => e.source_ranges.clone(),
664 KclError::UndefinedValue { details: e, .. } => e.source_ranges.clone(),
665 KclError::InvalidExpression { details: e } => e.source_ranges.clone(),
666 KclError::MaxCallStack { details: e } => e.source_ranges.clone(),
667 KclError::Refactor { details: e } => e.source_ranges.clone(),
668 KclError::Engine { details: e } => e.source_ranges.clone(),
669 KclError::EngineHangup { details: e } => e.source_ranges.clone(),
670 KclError::EngineInternal { details: e } => e.source_ranges.clone(),
671 KclError::Internal { details: e } => e.source_ranges.clone(),
672 }
673 }
674
675 pub fn message(&self) -> &str {
677 match &self {
678 KclError::Lexical { details: e } => &e.message,
679 KclError::Syntax { details: e } => &e.message,
680 KclError::Semantic { details: e } => &e.message,
681 KclError::ImportCycle { details: e } => &e.message,
682 KclError::Argument { details: e } => &e.message,
683 KclError::Type { details: e } => &e.message,
684 KclError::Io { details: e } => &e.message,
685 KclError::Unexpected { details: e } => &e.message,
686 KclError::ValueAlreadyDefined { details: e } => &e.message,
687 KclError::UndefinedValue { details: e, .. } => &e.message,
688 KclError::InvalidExpression { details: e } => &e.message,
689 KclError::MaxCallStack { details: e } => &e.message,
690 KclError::Refactor { details: e } => &e.message,
691 KclError::Engine { details: e } => &e.message,
692 KclError::EngineHangup { details: e } => &e.message,
693 KclError::EngineInternal { details: e } => &e.message,
694 KclError::Internal { details: e } => &e.message,
695 }
696 }
697
698 pub fn backtrace(&self) -> Vec<BacktraceItem> {
699 match self {
700 KclError::Lexical { details: e }
701 | KclError::Syntax { details: e }
702 | KclError::Semantic { details: e }
703 | KclError::ImportCycle { details: e }
704 | KclError::Argument { details: e }
705 | KclError::Type { details: e }
706 | KclError::Io { details: e }
707 | KclError::Unexpected { details: e }
708 | KclError::ValueAlreadyDefined { details: e }
709 | KclError::UndefinedValue { details: e, .. }
710 | KclError::InvalidExpression { details: e }
711 | KclError::MaxCallStack { details: e }
712 | KclError::Refactor { details: e }
713 | KclError::Engine { details: e }
714 | KclError::EngineHangup { details: e }
715 | KclError::EngineInternal { details: e }
716 | KclError::Internal { details: e } => e.backtrace.clone(),
717 }
718 }
719
720 pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
721 let mut new = self.clone();
722 match &mut new {
723 KclError::Lexical { details: e }
724 | KclError::Syntax { details: e }
725 | KclError::Semantic { details: e }
726 | KclError::ImportCycle { details: e }
727 | KclError::Argument { details: e }
728 | KclError::Type { details: e }
729 | KclError::Io { details: e }
730 | KclError::Unexpected { details: e }
731 | KclError::ValueAlreadyDefined { details: e }
732 | KclError::UndefinedValue { details: e, .. }
733 | KclError::InvalidExpression { details: e }
734 | KclError::MaxCallStack { details: e }
735 | KclError::Refactor { details: e }
736 | KclError::Engine { details: e }
737 | KclError::EngineHangup { details: e }
738 | KclError::EngineInternal { details: e }
739 | KclError::Internal { details: e } => {
740 e.backtrace = source_ranges
741 .iter()
742 .map(|s| BacktraceItem {
743 source_range: *s,
744 fn_name: None,
745 })
746 .collect();
747 e.source_ranges = source_ranges;
748 }
749 }
750
751 new
752 }
753
754 pub(crate) fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
755 let mut new = self.clone();
756 match &mut new {
757 KclError::Lexical { details: e }
758 | KclError::Syntax { details: e }
759 | KclError::Semantic { details: e }
760 | KclError::ImportCycle { details: e }
761 | KclError::Argument { details: e }
762 | KclError::Type { details: e }
763 | KclError::Io { details: e }
764 | KclError::Unexpected { details: e }
765 | KclError::ValueAlreadyDefined { details: e }
766 | KclError::UndefinedValue { details: e, .. }
767 | KclError::InvalidExpression { details: e }
768 | KclError::MaxCallStack { details: e }
769 | KclError::Refactor { details: e }
770 | KclError::Engine { details: e }
771 | KclError::EngineHangup { details: e }
772 | KclError::EngineInternal { details: e }
773 | KclError::Internal { details: e } => {
774 if let Some(item) = e.backtrace.last_mut() {
775 item.fn_name = last_fn_name;
776 }
777 e.backtrace.push(BacktraceItem {
778 source_range,
779 fn_name: None,
780 });
781 e.source_ranges.push(source_range);
782 }
783 }
784
785 new
786 }
787}
788
789#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS, thiserror::Error, miette::Diagnostic)]
790#[serde(rename_all = "camelCase")]
791#[ts(export)]
792pub struct BacktraceItem {
793 pub source_range: SourceRange,
794 pub fn_name: Option<String>,
795}
796
797impl std::fmt::Display for BacktraceItem {
798 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
799 if let Some(fn_name) = &self.fn_name {
800 write!(f, "{fn_name}: {:?}", self.source_range)
801 } else {
802 write!(f, "(fn): {:?}", self.source_range)
803 }
804 }
805}
806
807impl IntoDiagnostic for KclError {
808 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
809 let message = self.get_message();
810 let source_ranges = self.source_ranges();
811
812 let module_id = ModuleId::default();
814 let source_ranges = source_ranges
815 .iter()
816 .filter(|r| r.module_id() == module_id)
817 .collect::<Vec<_>>();
818
819 let mut diagnostics = Vec::new();
820 for source_range in &source_ranges {
821 diagnostics.push(Diagnostic {
822 range: source_range.to_lsp_range(code),
823 severity: Some(self.severity()),
824 code: None,
825 code_description: None,
827 source: Some("kcl".to_string()),
828 related_information: None,
829 message: message.clone(),
830 tags: None,
831 data: None,
832 });
833 }
834
835 diagnostics
836 }
837
838 fn severity(&self) -> DiagnosticSeverity {
839 DiagnosticSeverity::ERROR
840 }
841}
842
843impl From<KclError> for String {
846 fn from(error: KclError) -> Self {
847 serde_json::to_string(&error).unwrap()
848 }
849}
850
851impl From<String> for KclError {
852 fn from(error: String) -> Self {
853 serde_json::from_str(&error).unwrap()
854 }
855}
856
857#[cfg(feature = "pyo3")]
858impl From<pyo3::PyErr> for KclError {
859 fn from(error: pyo3::PyErr) -> Self {
860 KclError::new_internal(KclErrorDetails {
861 source_ranges: vec![],
862 backtrace: Default::default(),
863 message: error.to_string(),
864 })
865 }
866}
867
868#[cfg(feature = "pyo3")]
869impl From<KclError> for pyo3::PyErr {
870 fn from(error: KclError) -> Self {
871 pyo3::exceptions::PyException::new_err(error.to_string())
872 }
873}
874
875impl From<CompilationIssue> for KclErrorDetails {
876 fn from(err: CompilationIssue) -> Self {
877 let backtrace = vec![BacktraceItem {
878 source_range: err.source_range,
879 fn_name: None,
880 }];
881 KclErrorDetails {
882 source_ranges: vec![err.source_range],
883 backtrace,
884 message: err.message,
885 }
886 }
887}
888
889#[cfg(test)]
890mod tests {
891 use super::*;
892
893 #[test]
894 fn missing_filename_mapping_does_not_panic_when_building_diagnostics() {
895 let error = KclErrorWithOutputs::no_outputs(KclError::new_semantic(KclErrorDetails::new(
896 "boom".to_owned(),
897 vec![SourceRange::new(0, 1, ModuleId::from_usize(9))],
898 )));
899
900 let diagnostics = error.to_lsp_diagnostics("x");
901
902 assert_eq!(diagnostics.len(), 1);
903 assert_eq!(diagnostics[0].message, "semantic: boom");
904 assert_eq!(diagnostics[0].related_information, None);
905 }
906}