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