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