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::Io { .. } => "I/O",
381 KclError::Unexpected { .. } => "Unexpected",
382 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
383 KclError::UndefinedValue { .. } => "UndefinedValue",
384 KclError::InvalidExpression { .. } => "InvalidExpression",
385 KclError::MaxCallStack { .. } => "MaxCallStack",
386 KclError::Refactor { .. } => "Refactor",
387 KclError::Engine { .. } => "Engine",
388 KclError::EngineHangup { .. } => "EngineHangup",
389 KclError::EngineInternal { .. } => "EngineInternal",
390 KclError::Internal { .. } => "Internal",
391 };
392 let error_string = format!("KCL {family} error");
393 Some(Box::new(error_string))
394 }
395
396 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
397 Some(&self.kcl_source)
398 }
399
400 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
401 let iter = self
402 .error
403 .error
404 .source_ranges()
405 .into_iter()
406 .map(miette::SourceSpan::from)
407 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
408 Some(Box::new(iter))
409 }
410
411 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
412 let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
413 Some(Box::new(iter))
414 }
415}
416
417#[derive(thiserror::Error, Debug)]
418#[error("{}", self.error.get_message())]
419pub struct Report {
420 pub error: KclError,
421 pub kcl_source: String,
422 pub filename: String,
423}
424
425impl miette::Diagnostic for Report {
426 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
427 let family = match self.error {
428 KclError::Lexical { .. } => "Lexical",
429 KclError::Syntax { .. } => "Syntax",
430 KclError::Semantic { .. } => "Semantic",
431 KclError::ImportCycle { .. } => "ImportCycle",
432 KclError::Argument { .. } => "Argument",
433 KclError::Type { .. } => "Type",
434 KclError::Io { .. } => "I/O",
435 KclError::Unexpected { .. } => "Unexpected",
436 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
437 KclError::UndefinedValue { .. } => "UndefinedValue",
438 KclError::InvalidExpression { .. } => "InvalidExpression",
439 KclError::MaxCallStack { .. } => "MaxCallStack",
440 KclError::Refactor { .. } => "Refactor",
441 KclError::Engine { .. } => "Engine",
442 KclError::EngineHangup { .. } => "EngineHangup",
443 KclError::EngineInternal { .. } => "EngineInternal",
444 KclError::Internal { .. } => "Internal",
445 };
446 let error_string = format!("KCL {family} error");
447 Some(Box::new(error_string))
448 }
449
450 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
451 Some(&self.kcl_source)
452 }
453
454 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
455 let iter = self
456 .error
457 .source_ranges()
458 .into_iter()
459 .map(miette::SourceSpan::from)
460 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
461 Some(Box::new(iter))
462 }
463}
464
465#[derive(thiserror::Error, Debug)]
466#[error("{}", self.issue.message)]
467pub struct CompilationIssueReport {
468 pub issue: CompilationIssue,
469 pub kcl_source: String,
470 pub filename: String,
471}
472
473impl miette::Diagnostic for CompilationIssueReport {
474 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
475 let tag = match self.issue.tag {
476 Tag::Deprecated => "deprecated",
477 Tag::Unnecessary => "unnecessary",
478 Tag::UnknownNumericUnits => "unknown-numeric-units",
479 Tag::None => return None,
480 };
481 Some(Box::new(format!("KCL {tag}")))
482 }
483
484 fn severity(&self) -> Option<miette::Severity> {
485 Some(match self.issue.severity {
486 Severity::Warning => miette::Severity::Warning,
487 Severity::Error | Severity::Fatal => miette::Severity::Error,
488 })
489 }
490
491 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
492 self.issue
493 .suggestion
494 .as_ref()
495 .map(|s| Box::new(s.title.clone()) as Box<dyn std::fmt::Display>)
496 }
497
498 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
499 Some(&self.kcl_source)
500 }
501
502 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
503 let span = miette::SourceSpan::from(self.issue.source_range);
504 let label = miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span);
505 Some(Box::new(std::iter::once(label)))
506 }
507}
508
509pub fn render_compilation_issue_miette(filename: &str, source: &str, issue: CompilationIssue) -> String {
512 let report = CompilationIssueReport {
513 issue,
514 kcl_source: source.to_owned(),
515 filename: filename.to_owned(),
516 };
517 let report = miette::Report::new(report);
518 format!("{report:?}")
519}
520
521impl IntoDiagnostic for KclError {
522 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
523 let message = self.get_message();
524 let source_ranges = self.source_ranges();
525
526 let module_id = ModuleId::default();
528 let source_ranges = source_ranges
529 .iter()
530 .filter(|r| r.module_id() == module_id)
531 .collect::<Vec<_>>();
532
533 let mut diagnostics = Vec::new();
534 for source_range in &source_ranges {
535 diagnostics.push(Diagnostic {
536 range: source_range.to_lsp_range(code),
537 severity: Some(self.severity()),
538 code: None,
539 code_description: None,
541 source: Some("kcl".to_string()),
542 related_information: None,
543 message: message.clone(),
544 tags: None,
545 data: None,
546 });
547 }
548
549 diagnostics
550 }
551
552 fn severity(&self) -> DiagnosticSeverity {
553 DiagnosticSeverity::ERROR
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560
561 #[test]
562 fn missing_filename_mapping_does_not_panic_when_building_diagnostics() {
563 let error = KclErrorWithOutputs::no_outputs(KclError::new_semantic(KclErrorDetails::new(
564 "boom".to_owned(),
565 vec![SourceRange::new(0, 1, ModuleId::from_usize(9))],
566 )));
567
568 let diagnostics = error.to_lsp_diagnostics("x");
569
570 assert_eq!(diagnostics.len(), 1);
571 assert_eq!(diagnostics[0].message, "semantic: boom");
572 assert_eq!(diagnostics[0].related_information, None);
573 }
574}