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