1use std::collections::BTreeMap;
2
3use indexmap::IndexMap;
4use kcl_api::NodePath;
5pub use kcl_error::BacktraceItem;
6pub use kcl_error::CompilationIssue;
7pub use kcl_error::IsRetryable;
8pub use kcl_error::KclError;
9pub use kcl_error::KclErrorDetails;
10pub use kcl_error::Severity;
11pub use kcl_error::Suggestion;
12pub use kcl_error::Tag;
13use serde::Serialize;
14use thiserror::Error;
15use tower_lsp::lsp_types::Diagnostic;
16use tower_lsp::lsp_types::DiagnosticSeverity;
17use uuid::Uuid;
18
19use crate::ExecOutcome;
20use crate::ModuleId;
21use crate::SourceRange;
22use crate::exec::KclValue;
23use crate::execution::ArtifactCommand;
24use crate::execution::ArtifactGraph;
25use crate::execution::DefaultPlanes;
26use crate::execution::KclValueView;
27use crate::execution::OperationsByModule;
28use crate::execution::RefactorMetadata;
29use crate::front::Number;
30use crate::front::Object;
31use crate::front::ObjectId;
32use crate::lsp::IntoDiagnostic;
33use crate::lsp::ToLspRange;
34use crate::modules::ModulePath;
35use crate::modules::ModuleSource;
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
136impl From<KclErrorWithOutputs> for KclError {
137 fn from(error: KclErrorWithOutputs) -> Self {
138 error.error
139 }
140}
141
142#[derive(Error, Debug, Serialize, ts_rs::TS, Clone, PartialEq)]
143#[error("{error}")]
144#[ts(export)]
145#[serde(rename_all = "camelCase")]
146pub struct KclErrorWithOutputs {
147 pub error: KclError,
148 pub non_fatal: Vec<CompilationIssue>,
149 pub variables: IndexMap<String, KclValueView>,
152 pub operations: OperationsByModule,
153 pub _artifact_commands: Vec<ArtifactCommand>,
156 pub artifact_graph: ArtifactGraph,
157 #[serde(skip)]
158 pub scene_objects: Vec<Object>,
159 #[serde(skip)]
160 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
161 #[serde(skip)]
162 pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
163 pub refactor_metadata: Vec<RefactorMetadata>,
164 pub scene_graph: Option<crate::front::SceneGraph>,
165 pub filenames: IndexMap<ModuleId, ModulePath>,
166 pub source_files: IndexMap<ModuleId, ModuleSource>,
167 pub default_planes: Option<DefaultPlanes>,
168}
169
170impl KclErrorWithOutputs {
171 #[allow(clippy::too_many_arguments)]
172 pub fn new(
173 error: KclError,
174 non_fatal: Vec<CompilationIssue>,
175 variables: IndexMap<String, KclValue>,
176 operations: OperationsByModule,
177 artifact_commands: Vec<ArtifactCommand>,
178 artifact_graph: ArtifactGraph,
179 scene_objects: Vec<Object>,
180 source_range_to_object: BTreeMap<SourceRange, ObjectId>,
181 var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
182 refactor_metadata: Vec<RefactorMetadata>,
183 filenames: IndexMap<ModuleId, ModulePath>,
184 source_files: IndexMap<ModuleId, ModuleSource>,
185 default_planes: Option<DefaultPlanes>,
186 ) -> Self {
187 let variables_view = variables.into_iter().map(|(k, v)| (k, v.into())).collect();
188 Self {
189 error,
190 non_fatal,
191 variables: variables_view,
192 operations,
193 _artifact_commands: artifact_commands,
194 artifact_graph,
195 scene_objects,
196 source_range_to_object,
197 var_solutions,
198 refactor_metadata,
199 scene_graph: Default::default(),
200 filenames,
201 source_files,
202 default_planes,
203 }
204 }
205
206 pub fn no_outputs(error: KclError) -> Self {
207 Self {
208 error,
209 non_fatal: Default::default(),
210 variables: Default::default(),
211 operations: Default::default(),
212 _artifact_commands: Default::default(),
213 artifact_graph: Default::default(),
214 scene_objects: Default::default(),
215 source_range_to_object: Default::default(),
216 var_solutions: Default::default(),
217 refactor_metadata: Default::default(),
218 scene_graph: Default::default(),
219 filenames: Default::default(),
220 source_files: Default::default(),
221 default_planes: Default::default(),
222 }
223 }
224
225 pub fn from_error_outcome(error: KclError, outcome: ExecOutcome) -> Self {
227 KclErrorWithOutputs {
228 error,
229 non_fatal: outcome.issues,
230 variables: outcome.variables,
231 operations: outcome.operations,
232 _artifact_commands: Default::default(),
233 artifact_graph: outcome.artifact_graph,
234 scene_objects: outcome.scene_objects,
235 source_range_to_object: outcome.source_range_to_object,
236 var_solutions: outcome.var_solutions,
237 refactor_metadata: outcome.refactor_metadata,
238 scene_graph: Default::default(),
239 filenames: outcome.filenames,
240 source_files: Default::default(),
241 default_planes: outcome.default_planes,
242 }
243 }
244
245 pub fn sketch_constraint_report(&self) -> crate::SketchConstraintReport {
246 crate::execution::sketch_constraint_report_from_scene_objects(&self.scene_objects)
247 }
248
249 pub fn into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
250 let mut source_ranges = self.error.source_ranges();
251
252 let first_source_range = source_ranges
254 .pop()
255 .ok_or_else(|| anyhow::anyhow!("No source ranges found"))?;
256
257 let source = self
258 .source_files
259 .get(&first_source_range.module_id())
260 .cloned()
261 .unwrap_or(ModuleSource {
262 source: code.to_string(),
263 path: self
264 .filenames
265 .get(&first_source_range.module_id())
266 .cloned()
267 .unwrap_or(ModulePath::Main),
268 });
269 let filename = source.path.to_string();
270 let kcl_source = source.source;
271
272 let mut related = Vec::new();
273 for source_range in source_ranges {
274 let module_id = source_range.module_id();
275 let source = self.source_files.get(&module_id).cloned().unwrap_or(ModuleSource {
276 source: code.to_string(),
277 path: self.filenames.get(&module_id).cloned().unwrap_or(ModulePath::Main),
278 });
279 let error = self.error.override_source_ranges(vec![source_range]);
280 let report = Report {
281 error,
282 kcl_source: source.source.to_string(),
283 filename: source.path.to_string(),
284 };
285 related.push(report);
286 }
287
288 Ok(ReportWithOutputs {
289 error: self,
290 kcl_source,
291 filename,
292 related,
293 })
294 }
295}
296
297impl IsRetryable for KclErrorWithOutputs {
298 fn is_retryable(&self) -> bool {
299 matches!(
300 self.error,
301 KclError::EngineHangup { .. } | KclError::EngineInternal { .. }
302 )
303 }
304}
305
306impl IntoDiagnostic for KclErrorWithOutputs {
307 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
308 let message = self.error.get_message();
309 let source_ranges = self.error.source_ranges();
310
311 source_ranges
312 .into_iter()
313 .map(|source_range| {
314 let source = self.source_files.get(&source_range.module_id()).cloned().or_else(|| {
315 self.filenames
316 .get(&source_range.module_id())
317 .cloned()
318 .map(|path| ModuleSource {
319 source: code.to_string(),
320 path,
321 })
322 });
323
324 let related_information = source.and_then(|source| {
325 let mut filename = source.path.to_string();
326 if !filename.starts_with("file://") {
327 filename = format!("file:///{}", filename.trim_start_matches("/"));
328 }
329
330 url::Url::parse(&filename).ok().map(|uri| {
331 vec![tower_lsp::lsp_types::DiagnosticRelatedInformation {
332 location: tower_lsp::lsp_types::Location {
333 uri,
334 range: source_range.to_lsp_range(&source.source),
335 },
336 message: message.to_string(),
337 }]
338 })
339 });
340
341 Diagnostic {
342 range: source_range.to_lsp_range(code),
343 severity: Some(self.severity()),
344 code: None,
345 code_description: None,
347 source: Some("kcl".to_string()),
348 related_information,
349 message: message.clone(),
350 tags: None,
351 data: None,
352 }
353 })
354 .collect()
355 }
356
357 fn severity(&self) -> DiagnosticSeverity {
358 DiagnosticSeverity::ERROR
359 }
360}
361
362#[derive(thiserror::Error, Debug)]
363#[error("{}", self.error.error.get_message())]
364pub struct ReportWithOutputs {
365 pub error: KclErrorWithOutputs,
366 pub kcl_source: String,
367 pub filename: String,
368 pub related: Vec<Report>,
369}
370
371impl miette::Diagnostic for ReportWithOutputs {
372 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
373 let family = match self.error.error {
374 KclError::Lexical { .. } => "Lexical",
375 KclError::Syntax { .. } => "Syntax",
376 KclError::Semantic { .. } => "Semantic",
377 KclError::ImportCycle { .. } => "ImportCycle",
378 KclError::Argument { .. } => "Argument",
379 KclError::Type { .. } => "Type",
380 KclError::UserDefined { .. } => "UserDefined",
381 KclError::Io { .. } => "I/O",
382 KclError::Unexpected { .. } => "Unexpected",
383 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
384 KclError::UndefinedValue { .. } => "UndefinedValue",
385 KclError::InvalidExpression { .. } => "InvalidExpression",
386 KclError::MaxCallStack { .. } => "MaxCallStack",
387 KclError::Refactor { .. } => "Refactor",
388 KclError::Engine { .. } => "Engine",
389 KclError::EngineHangup { .. } => "EngineHangup",
390 KclError::EngineInternal { .. } => "EngineInternal",
391 KclError::Internal { .. } => "Internal",
392 };
393 let error_string = format!("KCL {family} error");
394 Some(Box::new(error_string))
395 }
396
397 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
398 Some(&self.kcl_source)
399 }
400
401 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
402 let iter = self
403 .error
404 .error
405 .source_ranges()
406 .into_iter()
407 .map(miette::SourceSpan::from)
408 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
409 Some(Box::new(iter))
410 }
411
412 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
413 let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
414 Some(Box::new(iter))
415 }
416}
417
418#[derive(thiserror::Error, Debug)]
419#[error("{}", self.error.get_message())]
420pub struct Report {
421 pub error: KclError,
422 pub kcl_source: String,
423 pub filename: String,
424}
425
426impl miette::Diagnostic for Report {
427 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
428 let family = match self.error {
429 KclError::Lexical { .. } => "Lexical",
430 KclError::Syntax { .. } => "Syntax",
431 KclError::Semantic { .. } => "Semantic",
432 KclError::ImportCycle { .. } => "ImportCycle",
433 KclError::Argument { .. } => "Argument",
434 KclError::Type { .. } => "Type",
435 KclError::UserDefined { .. } => "UserDefined",
436 KclError::Io { .. } => "I/O",
437 KclError::Unexpected { .. } => "Unexpected",
438 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
439 KclError::UndefinedValue { .. } => "UndefinedValue",
440 KclError::InvalidExpression { .. } => "InvalidExpression",
441 KclError::MaxCallStack { .. } => "MaxCallStack",
442 KclError::Refactor { .. } => "Refactor",
443 KclError::Engine { .. } => "Engine",
444 KclError::EngineHangup { .. } => "EngineHangup",
445 KclError::EngineInternal { .. } => "EngineInternal",
446 KclError::Internal { .. } => "Internal",
447 };
448 let error_string = format!("KCL {family} error");
449 Some(Box::new(error_string))
450 }
451
452 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
453 Some(&self.kcl_source)
454 }
455
456 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
457 let iter = self
458 .error
459 .source_ranges()
460 .into_iter()
461 .map(miette::SourceSpan::from)
462 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
463 Some(Box::new(iter))
464 }
465}
466
467#[derive(thiserror::Error, Debug)]
468#[error("{}", self.issue.message)]
469pub struct CompilationIssueReport {
470 pub issue: CompilationIssue,
471 pub kcl_source: String,
472 pub filename: String,
473}
474
475impl miette::Diagnostic for CompilationIssueReport {
476 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
477 let tag = match self.issue.tag {
478 Tag::Deprecated => "deprecated",
479 Tag::Unnecessary => "unnecessary",
480 Tag::UnknownNumericUnits => "unknown-numeric-units",
481 Tag::None => return None,
482 };
483 Some(Box::new(format!("KCL {tag}")))
484 }
485
486 fn severity(&self) -> Option<miette::Severity> {
487 Some(match self.issue.severity {
488 Severity::Warning => miette::Severity::Warning,
489 Severity::Error | Severity::Fatal => miette::Severity::Error,
490 })
491 }
492
493 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
494 self.issue
495 .suggestion
496 .as_ref()
497 .map(|s| Box::new(s.title.clone()) as Box<dyn std::fmt::Display>)
498 }
499
500 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
501 Some(&self.kcl_source)
502 }
503
504 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
505 let span = miette::SourceSpan::from(self.issue.source_range);
506 let label = miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span);
507 Some(Box::new(std::iter::once(label)))
508 }
509}
510
511pub fn render_compilation_issue_miette(filename: &str, source: &str, issue: CompilationIssue) -> String {
514 let report = CompilationIssueReport {
515 issue,
516 kcl_source: source.to_owned(),
517 filename: filename.to_owned(),
518 };
519 let report = miette::Report::new(report);
520 format!("{report:?}")
521}
522
523impl IntoDiagnostic for KclError {
524 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
525 let message = self.get_message();
526 let source_ranges = self.source_ranges();
527
528 let module_id = ModuleId::default();
530 let source_ranges = source_ranges
531 .iter()
532 .filter(|r| r.module_id() == module_id)
533 .collect::<Vec<_>>();
534
535 let mut diagnostics = Vec::new();
536 for source_range in &source_ranges {
537 diagnostics.push(Diagnostic {
538 range: source_range.to_lsp_range(code),
539 severity: Some(self.severity()),
540 code: None,
541 code_description: None,
543 source: Some("kcl".to_string()),
544 related_information: None,
545 message: message.clone(),
546 tags: None,
547 data: None,
548 });
549 }
550
551 diagnostics
552 }
553
554 fn severity(&self) -> DiagnosticSeverity {
555 DiagnosticSeverity::ERROR
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562
563 #[test]
564 fn missing_filename_mapping_does_not_panic_when_building_diagnostics() {
565 let error = KclErrorWithOutputs::no_outputs(KclError::new_semantic(KclErrorDetails::new(
566 "boom".to_owned(),
567 vec![SourceRange::new(0, 1, ModuleId::from_usize(9))],
568 )));
569
570 let diagnostics = error.to_lsp_diagnostics("x");
571
572 assert_eq!(diagnostics.len(), 1);
573 assert_eq!(diagnostics[0].message, "semantic: boom");
574 assert_eq!(diagnostics[0].related_information, None);
575 }
576}