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::front::Number;
29use crate::front::Object;
30use crate::front::ObjectId;
31use crate::lsp::IntoDiagnostic;
32use crate::lsp::ToLspRange;
33use crate::modules::ModulePath;
34use crate::modules::ModuleSource;
35
36#[derive(thiserror::Error, Debug)]
38pub enum ExecError {
39 #[error("{0}")]
40 Kcl(#[from] Box<crate::KclErrorWithOutputs>),
41 #[error("Could not connect to engine: {0}")]
42 Connection(#[from] ConnectionError),
43 #[error("PNG snapshot could not be decoded: {0}")]
44 BadPng(String),
45 #[error("Bad export: {0}")]
46 BadExport(String),
47}
48
49impl From<KclErrorWithOutputs> for ExecError {
50 fn from(error: KclErrorWithOutputs) -> Self {
51 ExecError::Kcl(Box::new(error))
52 }
53}
54
55#[derive(Debug, thiserror::Error)]
57#[error("{error}")]
58pub struct ExecErrorWithState {
59 pub error: ExecError,
60 pub exec_state: Option<crate::execution::ExecState>,
61 #[cfg(feature = "snapshot-engine-responses")]
62 pub responses: Option<IndexMap<Uuid, kittycad_modeling_cmds::websocket::WebSocketResponse>>,
63}
64
65impl ExecErrorWithState {
66 #[cfg_attr(target_arch = "wasm32", expect(dead_code))]
67 pub fn new(
68 error: ExecError,
69 exec_state: crate::execution::ExecState,
70 #[cfg_attr(not(feature = "snapshot-engine-responses"), expect(unused_variables))] responses: Option<
71 IndexMap<Uuid, kittycad_modeling_cmds::websocket::WebSocketResponse>,
72 >,
73 ) -> Self {
74 Self {
75 error,
76 exec_state: Some(exec_state),
77 #[cfg(feature = "snapshot-engine-responses")]
78 responses,
79 }
80 }
81}
82
83impl IsRetryable for ExecErrorWithState {
84 fn is_retryable(&self) -> bool {
85 self.error.is_retryable()
86 }
87}
88
89impl ExecError {
90 pub fn as_kcl_error(&self) -> Option<&crate::KclError> {
91 let ExecError::Kcl(k) = &self else {
92 return None;
93 };
94 Some(&k.error)
95 }
96}
97
98impl IsRetryable for ExecError {
99 fn is_retryable(&self) -> bool {
100 matches!(self, ExecError::Kcl(kcl_error) if kcl_error.is_retryable())
101 }
102}
103
104impl From<ExecError> for ExecErrorWithState {
105 fn from(error: ExecError) -> Self {
106 Self {
107 error,
108 exec_state: None,
109 #[cfg(feature = "snapshot-engine-responses")]
110 responses: None,
111 }
112 }
113}
114
115impl From<ConnectionError> for ExecErrorWithState {
116 fn from(error: ConnectionError) -> Self {
117 Self {
118 error: error.into(),
119 exec_state: None,
120 #[cfg(feature = "snapshot-engine-responses")]
121 responses: None,
122 }
123 }
124}
125
126#[derive(thiserror::Error, Debug)]
128pub enum ConnectionError {
129 #[error("Could not create a Zoo client: {0}")]
130 CouldNotMakeClient(anyhow::Error),
131 #[error("Could not establish connection to engine: {0}")]
132 Establishing(anyhow::Error),
133}
134
135impl From<KclErrorWithOutputs> for KclError {
136 fn from(error: KclErrorWithOutputs) -> Self {
137 error.error
138 }
139}
140
141#[derive(Error, Debug, Serialize, ts_rs::TS, Clone, PartialEq)]
142#[error("{error}")]
143#[ts(export)]
144#[serde(rename_all = "camelCase")]
145pub struct KclErrorWithOutputs {
146 pub error: KclError,
147 pub non_fatal: Vec<CompilationIssue>,
148 pub variables: IndexMap<String, KclValueView>,
151 pub operations: OperationsByModule,
152 pub _artifact_commands: Vec<ArtifactCommand>,
155 pub artifact_graph: ArtifactGraph,
156 #[serde(skip)]
157 pub scene_objects: Vec<Object>,
158 #[serde(skip)]
159 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
160 #[serde(skip)]
161 pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
162 pub scene_graph: Option<crate::front::SceneGraph>,
163 pub filenames: IndexMap<ModuleId, ModulePath>,
164 pub source_files: IndexMap<ModuleId, ModuleSource>,
165 pub default_planes: Option<DefaultPlanes>,
166}
167
168impl KclErrorWithOutputs {
169 #[allow(clippy::too_many_arguments)]
170 pub fn new(
171 error: KclError,
172 non_fatal: Vec<CompilationIssue>,
173 variables: IndexMap<String, KclValue>,
174 operations: OperationsByModule,
175 artifact_commands: Vec<ArtifactCommand>,
176 artifact_graph: ArtifactGraph,
177 scene_objects: Vec<Object>,
178 source_range_to_object: BTreeMap<SourceRange, ObjectId>,
179 var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
180 filenames: IndexMap<ModuleId, ModulePath>,
181 source_files: IndexMap<ModuleId, ModuleSource>,
182 default_planes: Option<DefaultPlanes>,
183 ) -> Self {
184 let variables_view = variables.into_iter().map(|(k, v)| (k, v.into())).collect();
185 Self {
186 error,
187 non_fatal,
188 variables: variables_view,
189 operations,
190 _artifact_commands: artifact_commands,
191 artifact_graph,
192 scene_objects,
193 source_range_to_object,
194 var_solutions,
195 scene_graph: Default::default(),
196 filenames,
197 source_files,
198 default_planes,
199 }
200 }
201
202 pub fn no_outputs(error: KclError) -> Self {
203 Self {
204 error,
205 non_fatal: Default::default(),
206 variables: Default::default(),
207 operations: Default::default(),
208 _artifact_commands: Default::default(),
209 artifact_graph: Default::default(),
210 scene_objects: Default::default(),
211 source_range_to_object: Default::default(),
212 var_solutions: Default::default(),
213 scene_graph: Default::default(),
214 filenames: Default::default(),
215 source_files: Default::default(),
216 default_planes: Default::default(),
217 }
218 }
219
220 pub fn from_error_outcome(error: KclError, outcome: ExecOutcome) -> Self {
222 KclErrorWithOutputs {
223 error,
224 non_fatal: outcome.issues,
225 variables: outcome.variables,
226 operations: outcome.operations,
227 _artifact_commands: Default::default(),
228 artifact_graph: outcome.artifact_graph,
229 scene_objects: outcome.scene_objects,
230 source_range_to_object: outcome.source_range_to_object,
231 var_solutions: outcome.var_solutions,
232 scene_graph: Default::default(),
233 filenames: outcome.filenames,
234 source_files: Default::default(),
235 default_planes: outcome.default_planes,
236 }
237 }
238
239 pub fn sketch_constraint_report(&self) -> crate::SketchConstraintReport {
240 crate::execution::sketch_constraint_report_from_scene_objects(&self.scene_objects)
241 }
242
243 pub fn into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
244 let mut source_ranges = self.error.source_ranges();
245
246 let first_source_range = source_ranges
248 .pop()
249 .ok_or_else(|| anyhow::anyhow!("No source ranges found"))?;
250
251 let source = self
252 .source_files
253 .get(&first_source_range.module_id())
254 .cloned()
255 .unwrap_or(ModuleSource {
256 source: code.to_string(),
257 path: self
258 .filenames
259 .get(&first_source_range.module_id())
260 .cloned()
261 .unwrap_or(ModulePath::Main),
262 });
263 let filename = source.path.to_string();
264 let kcl_source = source.source;
265
266 let mut related = Vec::new();
267 for source_range in source_ranges {
268 let module_id = source_range.module_id();
269 let source = self.source_files.get(&module_id).cloned().unwrap_or(ModuleSource {
270 source: code.to_string(),
271 path: self.filenames.get(&module_id).cloned().unwrap_or(ModulePath::Main),
272 });
273 let error = self.error.override_source_ranges(vec![source_range]);
274 let report = Report {
275 error,
276 kcl_source: source.source.to_string(),
277 filename: source.path.to_string(),
278 };
279 related.push(report);
280 }
281
282 Ok(ReportWithOutputs {
283 error: self,
284 kcl_source,
285 filename,
286 related,
287 })
288 }
289}
290
291impl IsRetryable for KclErrorWithOutputs {
292 fn is_retryable(&self) -> bool {
293 matches!(
294 self.error,
295 KclError::EngineHangup { .. } | KclError::EngineInternal { .. }
296 )
297 }
298}
299
300impl IntoDiagnostic for KclErrorWithOutputs {
301 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
302 let message = self.error.get_message();
303 let source_ranges = self.error.source_ranges();
304
305 source_ranges
306 .into_iter()
307 .map(|source_range| {
308 let source = self.source_files.get(&source_range.module_id()).cloned().or_else(|| {
309 self.filenames
310 .get(&source_range.module_id())
311 .cloned()
312 .map(|path| ModuleSource {
313 source: code.to_string(),
314 path,
315 })
316 });
317
318 let related_information = source.and_then(|source| {
319 let mut filename = source.path.to_string();
320 if !filename.starts_with("file://") {
321 filename = format!("file:///{}", filename.trim_start_matches("/"));
322 }
323
324 url::Url::parse(&filename).ok().map(|uri| {
325 vec![tower_lsp::lsp_types::DiagnosticRelatedInformation {
326 location: tower_lsp::lsp_types::Location {
327 uri,
328 range: source_range.to_lsp_range(&source.source),
329 },
330 message: message.to_string(),
331 }]
332 })
333 });
334
335 Diagnostic {
336 range: source_range.to_lsp_range(code),
337 severity: Some(self.severity()),
338 code: None,
339 code_description: None,
341 source: Some("kcl".to_string()),
342 related_information,
343 message: message.clone(),
344 tags: None,
345 data: None,
346 }
347 })
348 .collect()
349 }
350
351 fn severity(&self) -> DiagnosticSeverity {
352 DiagnosticSeverity::ERROR
353 }
354}
355
356#[derive(thiserror::Error, Debug)]
357#[error("{}", self.error.error.get_message())]
358pub struct ReportWithOutputs {
359 pub error: KclErrorWithOutputs,
360 pub kcl_source: String,
361 pub filename: String,
362 pub related: Vec<Report>,
363}
364
365impl miette::Diagnostic for ReportWithOutputs {
366 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
367 let family = match self.error.error {
368 KclError::Lexical { .. } => "Lexical",
369 KclError::Syntax { .. } => "Syntax",
370 KclError::Semantic { .. } => "Semantic",
371 KclError::ImportCycle { .. } => "ImportCycle",
372 KclError::Argument { .. } => "Argument",
373 KclError::Type { .. } => "Type",
374 KclError::Io { .. } => "I/O",
375 KclError::Unexpected { .. } => "Unexpected",
376 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
377 KclError::UndefinedValue { .. } => "UndefinedValue",
378 KclError::InvalidExpression { .. } => "InvalidExpression",
379 KclError::MaxCallStack { .. } => "MaxCallStack",
380 KclError::Refactor { .. } => "Refactor",
381 KclError::Engine { .. } => "Engine",
382 KclError::EngineHangup { .. } => "EngineHangup",
383 KclError::EngineInternal { .. } => "EngineInternal",
384 KclError::Internal { .. } => "Internal",
385 };
386 let error_string = format!("KCL {family} error");
387 Some(Box::new(error_string))
388 }
389
390 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
391 Some(&self.kcl_source)
392 }
393
394 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
395 let iter = self
396 .error
397 .error
398 .source_ranges()
399 .into_iter()
400 .map(miette::SourceSpan::from)
401 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
402 Some(Box::new(iter))
403 }
404
405 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
406 let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
407 Some(Box::new(iter))
408 }
409}
410
411#[derive(thiserror::Error, Debug)]
412#[error("{}", self.error.get_message())]
413pub struct Report {
414 pub error: KclError,
415 pub kcl_source: String,
416 pub filename: String,
417}
418
419impl miette::Diagnostic for Report {
420 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
421 let family = match self.error {
422 KclError::Lexical { .. } => "Lexical",
423 KclError::Syntax { .. } => "Syntax",
424 KclError::Semantic { .. } => "Semantic",
425 KclError::ImportCycle { .. } => "ImportCycle",
426 KclError::Argument { .. } => "Argument",
427 KclError::Type { .. } => "Type",
428 KclError::Io { .. } => "I/O",
429 KclError::Unexpected { .. } => "Unexpected",
430 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
431 KclError::UndefinedValue { .. } => "UndefinedValue",
432 KclError::InvalidExpression { .. } => "InvalidExpression",
433 KclError::MaxCallStack { .. } => "MaxCallStack",
434 KclError::Refactor { .. } => "Refactor",
435 KclError::Engine { .. } => "Engine",
436 KclError::EngineHangup { .. } => "EngineHangup",
437 KclError::EngineInternal { .. } => "EngineInternal",
438 KclError::Internal { .. } => "Internal",
439 };
440 let error_string = format!("KCL {family} error");
441 Some(Box::new(error_string))
442 }
443
444 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
445 Some(&self.kcl_source)
446 }
447
448 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
449 let iter = self
450 .error
451 .source_ranges()
452 .into_iter()
453 .map(miette::SourceSpan::from)
454 .map(|span| miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span));
455 Some(Box::new(iter))
456 }
457}
458
459#[derive(thiserror::Error, Debug)]
460#[error("{}", self.issue.message)]
461pub struct CompilationIssueReport {
462 pub issue: CompilationIssue,
463 pub kcl_source: String,
464 pub filename: String,
465}
466
467impl miette::Diagnostic for CompilationIssueReport {
468 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
469 let tag = match self.issue.tag {
470 Tag::Deprecated => "deprecated",
471 Tag::Unnecessary => "unnecessary",
472 Tag::UnknownNumericUnits => "unknown-numeric-units",
473 Tag::None => return None,
474 };
475 Some(Box::new(format!("KCL {tag}")))
476 }
477
478 fn severity(&self) -> Option<miette::Severity> {
479 Some(match self.issue.severity {
480 Severity::Warning => miette::Severity::Warning,
481 Severity::Error | Severity::Fatal => miette::Severity::Error,
482 })
483 }
484
485 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
486 self.issue
487 .suggestion
488 .as_ref()
489 .map(|s| Box::new(s.title.clone()) as Box<dyn std::fmt::Display>)
490 }
491
492 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
493 Some(&self.kcl_source)
494 }
495
496 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
497 let span = miette::SourceSpan::from(self.issue.source_range);
498 let label = miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span);
499 Some(Box::new(std::iter::once(label)))
500 }
501}
502
503pub fn render_compilation_issue_miette(filename: &str, source: &str, issue: CompilationIssue) -> String {
506 let report = CompilationIssueReport {
507 issue,
508 kcl_source: source.to_owned(),
509 filename: filename.to_owned(),
510 };
511 let report = miette::Report::new(report);
512 format!("{report:?}")
513}
514
515impl IntoDiagnostic for KclError {
516 fn to_lsp_diagnostics(&self, code: &str) -> Vec<Diagnostic> {
517 let message = self.get_message();
518 let source_ranges = self.source_ranges();
519
520 let module_id = ModuleId::default();
522 let source_ranges = source_ranges
523 .iter()
524 .filter(|r| r.module_id() == module_id)
525 .collect::<Vec<_>>();
526
527 let mut diagnostics = Vec::new();
528 for source_range in &source_ranges {
529 diagnostics.push(Diagnostic {
530 range: source_range.to_lsp_range(code),
531 severity: Some(self.severity()),
532 code: None,
533 code_description: None,
535 source: Some("kcl".to_string()),
536 related_information: None,
537 message: message.clone(),
538 tags: None,
539 data: None,
540 });
541 }
542
543 diagnostics
544 }
545
546 fn severity(&self) -> DiagnosticSeverity {
547 DiagnosticSeverity::ERROR
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554
555 #[test]
556 fn missing_filename_mapping_does_not_panic_when_building_diagnostics() {
557 let error = KclErrorWithOutputs::no_outputs(KclError::new_semantic(KclErrorDetails::new(
558 "boom".to_owned(),
559 vec![SourceRange::new(0, 1, ModuleId::from_usize(9))],
560 )));
561
562 let diagnostics = error.to_lsp_diagnostics("x");
563
564 assert_eq!(diagnostics.len(), 1);
565 assert_eq!(diagnostics[0].message, "semantic: boom");
566 assert_eq!(diagnostics[0].related_information, None);
567 }
568}