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
201#[derive(Error, Debug, Serialize, ts_rs::TS, Clone, PartialEq)]
202#[error("{error}")]
203#[ts(export)]
204#[serde(rename_all = "camelCase")]
205pub struct KclErrorWithOutputs {
206 pub error: KclError,
207 pub non_fatal: Vec<CompilationIssue>,
208 pub variables: IndexMap<String, KclValue>,
211 #[cfg(feature = "artifact-graph")]
212 pub operations: Vec<Operation>,
213 #[cfg(feature = "artifact-graph")]
216 pub _artifact_commands: Vec<ArtifactCommand>,
217 #[cfg(feature = "artifact-graph")]
218 pub artifact_graph: ArtifactGraph,
219 #[cfg(feature = "artifact-graph")]
220 #[serde(skip)]
221 pub scene_objects: Vec<Object>,
222 #[cfg(feature = "artifact-graph")]
223 #[serde(skip)]
224 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
225 #[cfg(feature = "artifact-graph")]
226 #[serde(skip)]
227 pub var_solutions: Vec<(SourceRange, Number)>,
228 pub scene_graph: Option<crate::front::SceneGraph>,
229 pub filenames: IndexMap<ModuleId, ModulePath>,
230 pub source_files: IndexMap<ModuleId, ModuleSource>,
231 pub default_planes: Option<DefaultPlanes>,
232}
233
234impl KclErrorWithOutputs {
235 #[allow(clippy::too_many_arguments)]
236 pub fn new(
237 error: KclError,
238 non_fatal: Vec<CompilationIssue>,
239 variables: IndexMap<String, KclValue>,
240 #[cfg(feature = "artifact-graph")] operations: Vec<Operation>,
241 #[cfg(feature = "artifact-graph")] artifact_commands: Vec<ArtifactCommand>,
242 #[cfg(feature = "artifact-graph")] artifact_graph: ArtifactGraph,
243 #[cfg(feature = "artifact-graph")] scene_objects: Vec<Object>,
244 #[cfg(feature = "artifact-graph")] source_range_to_object: BTreeMap<SourceRange, ObjectId>,
245 #[cfg(feature = "artifact-graph")] var_solutions: Vec<(SourceRange, Number)>,
246 filenames: IndexMap<ModuleId, ModulePath>,
247 source_files: IndexMap<ModuleId, ModuleSource>,
248 default_planes: Option<DefaultPlanes>,
249 ) -> Self {
250 Self {
251 error,
252 non_fatal,
253 variables,
254 #[cfg(feature = "artifact-graph")]
255 operations,
256 #[cfg(feature = "artifact-graph")]
257 _artifact_commands: artifact_commands,
258 #[cfg(feature = "artifact-graph")]
259 artifact_graph,
260 #[cfg(feature = "artifact-graph")]
261 scene_objects,
262 #[cfg(feature = "artifact-graph")]
263 source_range_to_object,
264 #[cfg(feature = "artifact-graph")]
265 var_solutions,
266 scene_graph: Default::default(),
267 filenames,
268 source_files,
269 default_planes,
270 }
271 }
272
273 pub fn no_outputs(error: KclError) -> Self {
274 Self {
275 error,
276 non_fatal: Default::default(),
277 variables: Default::default(),
278 #[cfg(feature = "artifact-graph")]
279 operations: Default::default(),
280 #[cfg(feature = "artifact-graph")]
281 _artifact_commands: Default::default(),
282 #[cfg(feature = "artifact-graph")]
283 artifact_graph: Default::default(),
284 #[cfg(feature = "artifact-graph")]
285 scene_objects: Default::default(),
286 #[cfg(feature = "artifact-graph")]
287 source_range_to_object: Default::default(),
288 #[cfg(feature = "artifact-graph")]
289 var_solutions: Default::default(),
290 scene_graph: Default::default(),
291 filenames: Default::default(),
292 source_files: Default::default(),
293 default_planes: Default::default(),
294 }
295 }
296
297 pub fn from_error_outcome(error: KclError, outcome: ExecOutcome) -> Self {
299 KclErrorWithOutputs {
300 error,
301 non_fatal: outcome.issues,
302 variables: outcome.variables,
303 #[cfg(feature = "artifact-graph")]
304 operations: outcome.operations,
305 #[cfg(feature = "artifact-graph")]
306 _artifact_commands: Default::default(),
307 #[cfg(feature = "artifact-graph")]
308 artifact_graph: outcome.artifact_graph,
309 #[cfg(feature = "artifact-graph")]
310 scene_objects: outcome.scene_objects,
311 #[cfg(feature = "artifact-graph")]
312 source_range_to_object: outcome.source_range_to_object,
313 #[cfg(feature = "artifact-graph")]
314 var_solutions: outcome.var_solutions,
315 scene_graph: Default::default(),
316 filenames: outcome.filenames,
317 source_files: Default::default(),
318 default_planes: outcome.default_planes,
319 }
320 }
321
322 #[cfg(feature = "artifact-graph")]
323 pub fn sketch_constraint_report(&self) -> crate::SketchConstraintReport {
324 crate::execution::sketch_constraint_report_from_scene_objects(&self.scene_objects)
325 }
326
327 pub fn into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
328 let mut source_ranges = self.error.source_ranges();
329
330 let first_source_range = source_ranges
332 .pop()
333 .ok_or_else(|| anyhow::anyhow!("No source ranges found"))?;
334
335 let source = self
336 .source_files
337 .get(&first_source_range.module_id())
338 .cloned()
339 .unwrap_or(ModuleSource {
340 source: code.to_string(),
341 path: self
342 .filenames
343 .get(&first_source_range.module_id())
344 .cloned()
345 .unwrap_or(ModulePath::Main),
346 });
347 let filename = source.path.to_string();
348 let kcl_source = source.source;
349
350 let mut related = Vec::new();
351 for source_range in source_ranges {
352 let module_id = source_range.module_id();
353 let source = self.source_files.get(&module_id).cloned().unwrap_or(ModuleSource {
354 source: code.to_string(),
355 path: self.filenames.get(&module_id).cloned().unwrap_or(ModulePath::Main),
356 });
357 let error = self.error.override_source_ranges(vec![source_range]);
358 let report = Report {
359 error,
360 kcl_source: source.source.to_string(),
361 filename: source.path.to_string(),
362 };
363 related.push(report);
364 }
365
366 Ok(ReportWithOutputs {
367 error: self,
368 kcl_source,
369 filename,
370 related,
371 })
372 }
373}
374
375impl IsRetryable for KclErrorWithOutputs {
376 fn is_retryable(&self) -> bool {
377 matches!(
378 self.error,
379 KclError::EngineHangup { .. } | KclError::EngineInternal { .. }
380 )
381 }
382}
383
384impl IntoDiagnostic for KclErrorWithOutputs {
385 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
386 let message = self.error.get_message();
387 let source_ranges = self.error.source_ranges();
388
389 source_ranges
390 .into_iter()
391 .map(|source_range| {
392 let source = self.source_files.get(&source_range.module_id()).cloned().or_else(|| {
393 self.filenames
394 .get(&source_range.module_id())
395 .cloned()
396 .map(|path| ModuleSource {
397 source: code.to_string(),
398 path,
399 })
400 });
401
402 let related_information = source.and_then(|source| {
403 let mut filename = source.path.to_string();
404 if !filename.starts_with("file://") {
405 filename = format!("file:///{}", filename.trim_start_matches("/"));
406 }
407
408 url::Url::parse(&filename).ok().map(|uri| {
409 vec![tower_lsp::lsp_types::DiagnosticRelatedInformation {
410 location: tower_lsp::lsp_types::Location {
411 uri,
412 range: source_range.to_lsp_range(&source.source),
413 },
414 message: message.to_string(),
415 }]
416 })
417 });
418
419 Diagnostic {
420 range: source_range.to_lsp_range(code),
421 severity: Some(self.severity()),
422 code: None,
423 code_description: None,
425 source: Some("kcl".to_string()),
426 related_information,
427 message: message.clone(),
428 tags: None,
429 data: None,
430 }
431 })
432 .collect()
433 }
434
435 fn severity(&self) -> DiagnosticSeverity {
436 DiagnosticSeverity::ERROR
437 }
438}
439
440#[derive(thiserror::Error, Debug)]
441#[error("{}", self.error.error.get_message())]
442pub struct ReportWithOutputs {
443 pub error: KclErrorWithOutputs,
444 pub kcl_source: String,
445 pub filename: String,
446 pub related: Vec<Report>,
447}
448
449impl miette::Diagnostic for ReportWithOutputs {
450 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
451 let family = match self.error.error {
452 KclError::Lexical { .. } => "Lexical",
453 KclError::Syntax { .. } => "Syntax",
454 KclError::Semantic { .. } => "Semantic",
455 KclError::ImportCycle { .. } => "ImportCycle",
456 KclError::Argument { .. } => "Argument",
457 KclError::Type { .. } => "Type",
458 KclError::Io { .. } => "I/O",
459 KclError::Unexpected { .. } => "Unexpected",
460 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
461 KclError::UndefinedValue { .. } => "UndefinedValue",
462 KclError::InvalidExpression { .. } => "InvalidExpression",
463 KclError::MaxCallStack { .. } => "MaxCallStack",
464 KclError::Refactor { .. } => "Refactor",
465 KclError::Engine { .. } => "Engine",
466 KclError::EngineHangup { .. } => "EngineHangup",
467 KclError::EngineInternal { .. } => "EngineInternal",
468 KclError::Internal { .. } => "Internal",
469 };
470 let error_string = format!("KCL {family} error");
471 Some(Box::new(error_string))
472 }
473
474 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
475 Some(&self.kcl_source)
476 }
477
478 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
479 let iter = self
480 .error
481 .error
482 .source_ranges()
483 .into_iter()
484 .map(miette::SourceSpan::from)
485 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
486 Some(Box::new(iter))
487 }
488
489 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
490 let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
491 Some(Box::new(iter))
492 }
493}
494
495#[derive(thiserror::Error, Debug)]
496#[error("{}", self.error.get_message())]
497pub struct Report {
498 pub error: KclError,
499 pub kcl_source: String,
500 pub filename: String,
501}
502
503impl miette::Diagnostic for Report {
504 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
505 let family = match self.error {
506 KclError::Lexical { .. } => "Lexical",
507 KclError::Syntax { .. } => "Syntax",
508 KclError::Semantic { .. } => "Semantic",
509 KclError::ImportCycle { .. } => "ImportCycle",
510 KclError::Argument { .. } => "Argument",
511 KclError::Type { .. } => "Type",
512 KclError::Io { .. } => "I/O",
513 KclError::Unexpected { .. } => "Unexpected",
514 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
515 KclError::UndefinedValue { .. } => "UndefinedValue",
516 KclError::InvalidExpression { .. } => "InvalidExpression",
517 KclError::MaxCallStack { .. } => "MaxCallStack",
518 KclError::Refactor { .. } => "Refactor",
519 KclError::Engine { .. } => "Engine",
520 KclError::EngineHangup { .. } => "EngineHangup",
521 KclError::EngineInternal { .. } => "EngineInternal",
522 KclError::Internal { .. } => "Internal",
523 };
524 let error_string = format!("KCL {family} error");
525 Some(Box::new(error_string))
526 }
527
528 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
529 Some(&self.kcl_source)
530 }
531
532 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
533 let iter = self
534 .error
535 .source_ranges()
536 .into_iter()
537 .map(miette::SourceSpan::from)
538 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
539 Some(Box::new(iter))
540 }
541}
542
543#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
544#[serde(rename_all = "camelCase")]
545#[error("{message}")]
546#[ts(export)]
547pub struct KclErrorDetails {
548 #[label(collection, "Errors")]
549 pub source_ranges: Vec<SourceRange>,
550 pub backtrace: Vec<super::BacktraceItem>,
551 #[serde(rename = "msg")]
552 pub message: String,
553}
554
555impl KclErrorDetails {
556 pub fn new(message: String, source_ranges: Vec<SourceRange>) -> KclErrorDetails {
557 let backtrace = source_ranges
558 .iter()
559 .map(|s| BacktraceItem {
560 source_range: *s,
561 fn_name: None,
562 })
563 .collect();
564 KclErrorDetails {
565 source_ranges,
566 backtrace,
567 message,
568 }
569 }
570}
571
572impl KclError {
573 pub fn internal(message: String) -> KclError {
574 KclError::Internal {
575 details: KclErrorDetails {
576 source_ranges: Default::default(),
577 backtrace: Default::default(),
578 message,
579 },
580 }
581 }
582
583 pub fn new_internal(details: KclErrorDetails) -> KclError {
584 KclError::Internal { details }
585 }
586
587 pub fn new_import_cycle(details: KclErrorDetails) -> KclError {
588 KclError::ImportCycle { details }
589 }
590
591 pub fn new_argument(details: KclErrorDetails) -> KclError {
592 KclError::Argument { details }
593 }
594
595 pub fn new_semantic(details: KclErrorDetails) -> KclError {
596 KclError::Semantic { details }
597 }
598
599 pub fn new_value_already_defined(details: KclErrorDetails) -> KclError {
600 KclError::ValueAlreadyDefined { details }
601 }
602
603 pub fn new_syntax(details: KclErrorDetails) -> KclError {
604 KclError::Syntax { details }
605 }
606
607 pub fn new_io(details: KclErrorDetails) -> KclError {
608 KclError::Io { details }
609 }
610
611 pub fn new_invalid_expression(details: KclErrorDetails) -> KclError {
612 KclError::InvalidExpression { details }
613 }
614
615 pub fn refactor(message: String) -> KclError {
616 KclError::Refactor {
617 details: KclErrorDetails {
618 source_ranges: Default::default(),
619 backtrace: Default::default(),
620 message,
621 },
622 }
623 }
624
625 pub fn new_engine(details: KclErrorDetails) -> KclError {
626 if details.message.eq_ignore_ascii_case("internal error") {
627 KclError::EngineInternal { details }
628 } else {
629 KclError::Engine { details }
630 }
631 }
632
633 pub fn new_engine_hangup(details: KclErrorDetails, api_call_id: Option<String>) -> KclError {
634 KclError::EngineHangup { details, api_call_id }
635 }
636
637 pub fn new_lexical(details: KclErrorDetails) -> KclError {
638 KclError::Lexical { details }
639 }
640
641 pub fn new_undefined_value(details: KclErrorDetails, name: Option<String>) -> KclError {
642 KclError::UndefinedValue { details, name }
643 }
644
645 pub fn new_type(details: KclErrorDetails) -> KclError {
646 KclError::Type { details }
647 }
648
649 pub fn is_undefined_value(&self) -> bool {
650 matches!(self, KclError::UndefinedValue { .. })
651 }
652
653 pub fn get_message(&self) -> String {
655 format!("{}: {}", self.error_type(), self.message())
656 }
657
658 pub fn error_type(&self) -> &'static str {
659 match self {
660 KclError::Lexical { .. } => "lexical",
661 KclError::Syntax { .. } => "syntax",
662 KclError::Semantic { .. } => "semantic",
663 KclError::ImportCycle { .. } => "import cycle",
664 KclError::Argument { .. } => "argument",
665 KclError::Type { .. } => "type",
666 KclError::Io { .. } => "i/o",
667 KclError::Unexpected { .. } => "unexpected",
668 KclError::ValueAlreadyDefined { .. } => "value already defined",
669 KclError::UndefinedValue { .. } => "undefined value",
670 KclError::InvalidExpression { .. } => "invalid expression",
671 KclError::MaxCallStack { .. } => "max call stack",
672 KclError::Refactor { .. } => "refactor",
673 KclError::Engine { .. } => "engine",
674 KclError::EngineHangup { .. } => "engine hangup",
675 KclError::EngineInternal { .. } => "engine internal",
676 KclError::Internal { .. } => "internal",
677 }
678 }
679
680 pub fn source_ranges(&self) -> Vec<SourceRange> {
681 match &self {
682 KclError::Lexical { details: e } => e.source_ranges.clone(),
683 KclError::Syntax { details: e } => e.source_ranges.clone(),
684 KclError::Semantic { details: e } => e.source_ranges.clone(),
685 KclError::ImportCycle { details: e } => e.source_ranges.clone(),
686 KclError::Argument { details: e } => e.source_ranges.clone(),
687 KclError::Type { details: e } => e.source_ranges.clone(),
688 KclError::Io { details: e } => e.source_ranges.clone(),
689 KclError::Unexpected { details: e } => e.source_ranges.clone(),
690 KclError::ValueAlreadyDefined { details: e } => e.source_ranges.clone(),
691 KclError::UndefinedValue { details: e, .. } => e.source_ranges.clone(),
692 KclError::InvalidExpression { details: e } => e.source_ranges.clone(),
693 KclError::MaxCallStack { details: e } => e.source_ranges.clone(),
694 KclError::Refactor { details: e } => e.source_ranges.clone(),
695 KclError::Engine { details: e } => e.source_ranges.clone(),
696 KclError::EngineHangup { details: e, .. } => e.source_ranges.clone(),
697 KclError::EngineInternal { details: e } => e.source_ranges.clone(),
698 KclError::Internal { details: e } => e.source_ranges.clone(),
699 }
700 }
701
702 pub fn message(&self) -> &str {
704 match &self {
705 KclError::Lexical { details: e } => &e.message,
706 KclError::Syntax { details: e } => &e.message,
707 KclError::Semantic { details: e } => &e.message,
708 KclError::ImportCycle { details: e } => &e.message,
709 KclError::Argument { details: e } => &e.message,
710 KclError::Type { details: e } => &e.message,
711 KclError::Io { details: e } => &e.message,
712 KclError::Unexpected { details: e } => &e.message,
713 KclError::ValueAlreadyDefined { details: e } => &e.message,
714 KclError::UndefinedValue { details: e, .. } => &e.message,
715 KclError::InvalidExpression { details: e } => &e.message,
716 KclError::MaxCallStack { details: e } => &e.message,
717 KclError::Refactor { details: e } => &e.message,
718 KclError::Engine { details: e } => &e.message,
719 KclError::EngineHangup { details: e, .. } => &e.message,
720 KclError::EngineInternal { details: e } => &e.message,
721 KclError::Internal { details: e } => &e.message,
722 }
723 }
724
725 pub fn backtrace(&self) -> Vec<BacktraceItem> {
726 match self {
727 KclError::Lexical { details: e }
728 | KclError::Syntax { details: e }
729 | KclError::Semantic { details: e }
730 | KclError::ImportCycle { details: e }
731 | KclError::Argument { details: e }
732 | KclError::Type { details: e }
733 | KclError::Io { details: e }
734 | KclError::Unexpected { details: e }
735 | KclError::ValueAlreadyDefined { details: e }
736 | KclError::UndefinedValue { details: e, .. }
737 | KclError::InvalidExpression { details: e }
738 | KclError::MaxCallStack { details: e }
739 | KclError::Refactor { details: e }
740 | KclError::Engine { details: e }
741 | KclError::EngineHangup { details: e, .. }
742 | KclError::EngineInternal { details: e }
743 | KclError::Internal { details: e } => e.backtrace.clone(),
744 }
745 }
746
747 pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
748 let mut new = self.clone();
749 match &mut new {
750 KclError::Lexical { details: e }
751 | KclError::Syntax { details: e }
752 | KclError::Semantic { details: e }
753 | KclError::ImportCycle { details: e }
754 | KclError::Argument { details: e }
755 | KclError::Type { details: e }
756 | KclError::Io { details: e }
757 | KclError::Unexpected { details: e }
758 | KclError::ValueAlreadyDefined { details: e }
759 | KclError::UndefinedValue { details: e, .. }
760 | KclError::InvalidExpression { details: e }
761 | KclError::MaxCallStack { details: e }
762 | KclError::Refactor { details: e }
763 | KclError::Engine { details: e }
764 | KclError::EngineHangup { details: e, .. }
765 | KclError::EngineInternal { details: e }
766 | KclError::Internal { details: e } => {
767 e.backtrace = source_ranges
768 .iter()
769 .map(|s| BacktraceItem {
770 source_range: *s,
771 fn_name: None,
772 })
773 .collect();
774 e.source_ranges = source_ranges;
775 }
776 }
777
778 new
779 }
780
781 pub(crate) fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
782 let mut new = self.clone();
783 match &mut new {
784 KclError::Lexical { details: e }
785 | KclError::Syntax { details: e }
786 | KclError::Semantic { details: e }
787 | KclError::ImportCycle { details: e }
788 | KclError::Argument { details: e }
789 | KclError::Type { details: e }
790 | KclError::Io { details: e }
791 | KclError::Unexpected { details: e }
792 | KclError::ValueAlreadyDefined { details: e }
793 | KclError::UndefinedValue { details: e, .. }
794 | KclError::InvalidExpression { details: e }
795 | KclError::MaxCallStack { details: e }
796 | KclError::Refactor { details: e }
797 | KclError::Engine { details: e }
798 | KclError::EngineHangup { details: e, .. }
799 | KclError::EngineInternal { details: e }
800 | KclError::Internal { details: e } => {
801 if let Some(item) = e.backtrace.last_mut() {
802 item.fn_name = last_fn_name;
803 }
804 e.backtrace.push(BacktraceItem {
805 source_range,
806 fn_name: None,
807 });
808 e.source_ranges.push(source_range);
809 }
810 }
811
812 new
813 }
814}
815
816#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS, thiserror::Error, miette::Diagnostic)]
817#[serde(rename_all = "camelCase")]
818#[ts(export)]
819pub struct BacktraceItem {
820 pub source_range: SourceRange,
821 pub fn_name: Option<String>,
822}
823
824impl std::fmt::Display for BacktraceItem {
825 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
826 if let Some(fn_name) = &self.fn_name {
827 write!(f, "{fn_name}: {:?}", self.source_range)
828 } else {
829 write!(f, "(fn): {:?}", self.source_range)
830 }
831 }
832}
833
834impl IntoDiagnostic for KclError {
835 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
836 let message = self.get_message();
837 let source_ranges = self.source_ranges();
838
839 let module_id = ModuleId::default();
841 let source_ranges = source_ranges
842 .iter()
843 .filter(|r| r.module_id() == module_id)
844 .collect::<Vec<_>>();
845
846 let mut diagnostics = Vec::new();
847 for source_range in &source_ranges {
848 diagnostics.push(Diagnostic {
849 range: source_range.to_lsp_range(code),
850 severity: Some(self.severity()),
851 code: None,
852 code_description: None,
854 source: Some("kcl".to_string()),
855 related_information: None,
856 message: message.clone(),
857 tags: None,
858 data: None,
859 });
860 }
861
862 diagnostics
863 }
864
865 fn severity(&self) -> DiagnosticSeverity {
866 DiagnosticSeverity::ERROR
867 }
868}
869
870impl From<KclError> for String {
873 fn from(error: KclError) -> Self {
874 serde_json::to_string(&error).unwrap()
875 }
876}
877
878impl From<String> for KclError {
879 fn from(error: String) -> Self {
880 serde_json::from_str(&error).unwrap()
881 }
882}
883
884#[cfg(feature = "pyo3")]
885impl From<pyo3::PyErr> for KclError {
886 fn from(error: pyo3::PyErr) -> Self {
887 KclError::new_internal(KclErrorDetails {
888 source_ranges: vec![],
889 backtrace: Default::default(),
890 message: error.to_string(),
891 })
892 }
893}
894
895#[cfg(feature = "pyo3")]
896impl From<KclError> for pyo3::PyErr {
897 fn from(error: KclError) -> Self {
898 pyo3::exceptions::PyException::new_err(error.to_string())
899 }
900}
901
902impl From<CompilationIssue> for KclErrorDetails {
903 fn from(err: CompilationIssue) -> Self {
904 let backtrace = vec![BacktraceItem {
905 source_range: err.source_range,
906 fn_name: None,
907 }];
908 KclErrorDetails {
909 source_ranges: vec![err.source_range],
910 backtrace,
911 message: err.message,
912 }
913 }
914}
915
916#[cfg(test)]
917mod tests {
918 use super::*;
919
920 #[test]
921 fn missing_filename_mapping_does_not_panic_when_building_diagnostics() {
922 let error = KclErrorWithOutputs::no_outputs(KclError::new_semantic(KclErrorDetails::new(
923 "boom".to_owned(),
924 vec![SourceRange::new(0, 1, ModuleId::from_usize(9))],
925 )));
926
927 let diagnostics = error.to_lsp_diagnostics("x");
928
929 assert_eq!(diagnostics.len(), 1);
930 assert_eq!(diagnostics[0].message, "semantic: boom");
931 assert_eq!(diagnostics[0].related_information, None);
932 }
933}