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