1use std::collections::BTreeMap;
2
3use indexmap::IndexMap;
4use kcl_api::NodePath;
5use kcl_api::Number;
6use kcl_api::Object;
7use kcl_api::ObjectId;
8pub use kcl_error::BacktraceItem;
9pub use kcl_error::BacktraceItemKind;
10pub use kcl_error::CompilationIssue;
11pub use kcl_error::IsRetryable;
12pub use kcl_error::KclError;
13pub use kcl_error::KclErrorDetails;
14pub use kcl_error::Severity;
15pub use kcl_error::Suggestion;
16pub use kcl_error::Tag;
17use serde::Serialize;
18use thiserror::Error;
19use tower_lsp::lsp_types::Diagnostic;
20use tower_lsp::lsp_types::DiagnosticSeverity;
21use uuid::Uuid;
22
23use crate::ExecOutcome;
24use crate::ModuleId;
25use crate::SourceRange;
26use crate::exec::KclValue;
27use crate::execution::ArtifactCommand;
28use crate::execution::ArtifactGraph;
29use crate::execution::DefaultPlanes;
30use crate::execution::KclValueView;
31use crate::execution::OperationsByModule;
32use crate::execution::RefactorMetadata;
33use crate::lsp_types::IntoDiagnostic;
34use crate::lsp_types::ToLspRange;
35use crate::modules::ModulePath;
36use crate::modules::ModuleSource;
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
137impl From<KclErrorWithOutputs> for KclError {
138 fn from(error: KclErrorWithOutputs) -> Self {
139 error.error
140 }
141}
142
143#[derive(Error, Debug, Serialize, ts_rs::TS, Clone, PartialEq)]
144#[error("{error}")]
145#[ts(export)]
146#[serde(rename_all = "camelCase")]
147pub struct KclErrorWithOutputs {
148 pub error: KclError,
149 pub non_fatal: Vec<CompilationIssue>,
150 pub variables: IndexMap<String, KclValueView>,
153 pub operations: OperationsByModule,
154 pub _artifact_commands: Vec<ArtifactCommand>,
157 pub artifact_graph: ArtifactGraph,
158 #[ts(skip)]
159 pub scene_objects: Vec<Object>,
160 #[serde(skip)]
161 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
162 #[serde(skip)]
163 pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
164 pub refactor_metadata: Vec<RefactorMetadata>,
165 pub scene_graph: Option<crate::front::SceneGraph>,
166 pub filenames: IndexMap<ModuleId, ModulePath>,
167 pub source_files: IndexMap<ModuleId, ModuleSource>,
168 pub default_planes: Option<DefaultPlanes>,
169 pub engine_scene_generation: Option<usize>,
173}
174
175impl KclErrorWithOutputs {
176 #[allow(clippy::too_many_arguments)]
177 pub fn new(
178 error: KclError,
179 non_fatal: Vec<CompilationIssue>,
180 variables: IndexMap<String, KclValue>,
181 operations: OperationsByModule,
182 artifact_commands: Vec<ArtifactCommand>,
183 artifact_graph: ArtifactGraph,
184 scene_objects: Vec<Object>,
185 source_range_to_object: BTreeMap<SourceRange, ObjectId>,
186 var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
187 refactor_metadata: Vec<RefactorMetadata>,
188 filenames: IndexMap<ModuleId, ModulePath>,
189 source_files: IndexMap<ModuleId, ModuleSource>,
190 default_planes: Option<DefaultPlanes>,
191 ) -> Self {
192 let variables_view = variables.into_iter().map(|(k, v)| (k, v.into())).collect();
193 Self {
194 error,
195 non_fatal,
196 variables: variables_view,
197 operations,
198 _artifact_commands: artifact_commands,
199 artifact_graph,
200 scene_objects,
201 source_range_to_object,
202 var_solutions,
203 refactor_metadata,
204 scene_graph: Default::default(),
205 filenames,
206 source_files,
207 default_planes,
208 engine_scene_generation: None,
209 }
210 }
211
212 pub fn no_outputs(error: KclError) -> Self {
213 Self {
214 error,
215 non_fatal: Default::default(),
216 variables: Default::default(),
217 operations: Default::default(),
218 _artifact_commands: Default::default(),
219 artifact_graph: Default::default(),
220 scene_objects: Default::default(),
221 source_range_to_object: Default::default(),
222 var_solutions: Default::default(),
223 refactor_metadata: Default::default(),
224 scene_graph: Default::default(),
225 filenames: Default::default(),
226 source_files: Default::default(),
227 default_planes: Default::default(),
228 engine_scene_generation: None,
229 }
230 }
231
232 pub fn from_error_outcome(error: KclError, outcome: ExecOutcome) -> Self {
234 KclErrorWithOutputs {
235 error,
236 non_fatal: outcome.issues,
237 variables: outcome.variables,
238 operations: outcome.operations,
239 _artifact_commands: Default::default(),
240 artifact_graph: outcome.artifact_graph,
241 scene_objects: outcome.scene_objects,
242 source_range_to_object: outcome.source_range_to_object,
243 var_solutions: outcome.var_solutions,
244 refactor_metadata: outcome.refactor_metadata,
245 scene_graph: Default::default(),
246 filenames: outcome.filenames,
247 source_files: outcome.source_files,
248 default_planes: outcome.default_planes,
249 engine_scene_generation: Some(outcome.engine_scene_generation),
250 }
251 }
252
253 pub fn sketch_constraint_report(&self) -> crate::SketchConstraintReport {
254 crate::execution::sketch_constraint_report_from_scene_objects(&self.scene_objects)
255 }
256
257 pub fn render_sketch_png_instance(
261 &self,
262 sketch_name: &str,
263 instance_index: Option<usize>,
264 ) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
265 crate::execution::render_sketch_png_from_scene_objects(&self.scene_objects, sketch_name, instance_index)
266 }
267
268 pub fn into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
269 let source_ranges = self.error.source_ranges();
270
271 let first_source_range = *source_ranges
278 .first()
279 .ok_or_else(|| anyhow::anyhow!("No source ranges found"))?;
280 let primary_module_id = first_source_range.module_id();
281
282 let module_source = |module_id: ModuleId| {
283 self.source_files.get(&module_id).cloned().unwrap_or(ModuleSource {
284 source: code.to_string(),
285 path: self.filenames.get(&module_id).cloned().unwrap_or(ModulePath::Main),
286 })
287 };
288 let source = module_source(primary_module_id);
289 let filename = source.path.to_string();
290 let kcl_source = source.source;
291
292 let backtrace = self.error.backtrace();
295
296 let mut primary_labels = vec![miette::LabeledSpan::new_with_span(
297 Some(filename.clone()),
298 miette::SourceSpan::from(first_source_range),
299 )];
300 let mut kept_ranges = vec![first_source_range];
301 let mut related = Vec::new();
302 for (index, source_range) in source_ranges.iter().copied().enumerate().skip(1) {
303 let keep = source_range.module_id() == primary_module_id
304 && !kept_ranges.iter().any(|kept| ranges_overlap(*kept, source_range));
305 let source = module_source(source_range.module_id());
306 let label = frame_label(&backtrace, source_ranges.len(), index).unwrap_or_else(|| source.path.to_string());
307 if keep {
308 primary_labels.push(miette::LabeledSpan::new_with_span(
309 Some(label),
310 miette::SourceSpan::from(source_range),
311 ));
312 kept_ranges.push(source_range);
313 } else {
314 let error = self.error.override_source_ranges(vec![source_range]);
315 related.push(Report {
316 error,
317 kcl_source: source.source,
318 filename: source.path.to_string(),
319 label,
320 });
321 }
322 }
323
324 Ok(ReportWithOutputs {
325 error: self,
326 kcl_source,
327 filename,
328 primary_labels,
329 related,
330 })
331 }
332}
333
334fn frame_label(backtrace: &[BacktraceItem], ranges_len: usize, index: usize) -> Option<String> {
340 if backtrace.len() != ranges_len {
341 return None;
342 }
343 let frame = &backtrace[index];
344 let name = frame.fn_name.as_ref()?;
345 match frame.kind {
346 BacktraceItemKind::Import => Some(name.clone()),
347 BacktraceItemKind::Call => Some(format!("in {name}()")),
348 }
349}
350
351fn ranges_overlap(a: SourceRange, b: SourceRange) -> bool {
356 if a.module_id() != b.module_id() {
357 return false;
358 }
359 if a.start() == b.start() && a.end() == b.end() {
360 return true;
361 }
362 a.start() < b.end() && b.start() < a.end()
363}
364
365impl IsRetryable for KclErrorWithOutputs {
366 fn is_retryable(&self) -> bool {
367 matches!(
368 self.error,
369 KclError::EngineHangup { .. } | KclError::EngineInternal { .. }
370 )
371 }
372}
373
374impl IntoDiagnostic for KclErrorWithOutputs {
375 fn to_lsp_diagnostics(&self, code: &str, uri: &tower_lsp::lsp_types::Url) -> Vec<Diagnostic> {
376 let message = self.error.get_message();
377 let source_ranges = self.error.source_ranges();
378 if source_ranges.is_empty() {
379 return Vec::new();
380 }
381
382 let primary_index = source_ranges.iter().position(|range| range.module_id().is_top_level());
389 let primary_range = primary_index.map(|index| source_ranges[index]).unwrap_or_default();
390
391 let backtrace = self.error.backtrace();
392 let related_information: Vec<tower_lsp::lsp_types::DiagnosticRelatedInformation> = source_ranges
393 .iter()
394 .enumerate()
395 .filter(|(index, _)| Some(*index) != primary_index)
396 .filter_map(|(index, source_range)| {
397 let location = if source_range.module_id().is_top_level() {
401 tower_lsp::lsp_types::Location {
402 uri: uri.clone(),
403 range: source_range.to_lsp_range(code),
404 }
405 } else {
406 let source = self.source_files.get(&source_range.module_id()).cloned().or_else(|| {
407 self.filenames
408 .get(&source_range.module_id())
409 .cloned()
410 .map(|path| ModuleSource {
411 source: code.to_string(),
412 path,
413 })
414 })?;
415 let mut filename = source.path.to_string();
416 if !filename.starts_with("file://") {
417 filename = format!("file:///{}", filename.trim_start_matches("/"));
418 }
419 tower_lsp::lsp_types::Location {
420 uri: url::Url::parse(&filename).ok()?,
421 range: source_range.to_lsp_range(&source.source),
422 }
423 };
424 Some(tower_lsp::lsp_types::DiagnosticRelatedInformation {
425 location,
426 message: frame_label(&backtrace, source_ranges.len(), index).unwrap_or_else(|| message.clone()),
427 })
428 })
429 .collect();
430
431 vec![Diagnostic {
432 range: primary_range.to_lsp_range(code),
433 severity: Some(self.severity()),
434 code: None,
435 code_description: None,
437 source: Some("kcl".to_string()),
438 related_information: (!related_information.is_empty()).then_some(related_information),
439 message,
440 tags: None,
441 data: None,
442 }]
443 }
444
445 fn severity(&self) -> DiagnosticSeverity {
446 DiagnosticSeverity::ERROR
447 }
448}
449
450#[derive(thiserror::Error, Debug)]
451#[error("{}", self.error.error.get_message())]
452pub struct ReportWithOutputs {
453 pub error: KclErrorWithOutputs,
454 pub kcl_source: String,
455 pub filename: String,
456 pub primary_labels: Vec<miette::LabeledSpan>,
459 pub related: Vec<Report>,
460}
461
462impl miette::Diagnostic for ReportWithOutputs {
463 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
464 let family = match self.error.error {
465 KclError::Lexical { .. } => "Lexical",
466 KclError::Syntax { .. } => "Syntax",
467 KclError::Semantic { .. } => "Semantic",
468 KclError::ImportCycle { .. } => "ImportCycle",
469 KclError::Argument { .. } => "Argument",
470 KclError::Type { .. } => "Type",
471 KclError::UserDefined { .. } => "UserDefined",
472 KclError::Io { .. } => "I/O",
473 KclError::Unexpected { .. } => "Unexpected",
474 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
475 KclError::UndefinedValue { .. } => "UndefinedValue",
476 KclError::InvalidExpression { .. } => "InvalidExpression",
477 KclError::MaxCallStack { .. } => "MaxCallStack",
478 KclError::Refactor { .. } => "Refactor",
479 KclError::Engine { .. } => "Engine",
480 KclError::EngineHangup { .. } => "EngineHangup",
481 KclError::EngineInternal { .. } => "EngineInternal",
482 KclError::Internal { .. } => "Internal",
483 };
484 let error_string = format!("KCL {family} error");
485 Some(Box::new(error_string))
486 }
487
488 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
489 Some(&self.kcl_source)
490 }
491
492 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
493 Some(Box::new(self.primary_labels.iter().cloned()))
494 }
495
496 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
497 let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
498 Some(Box::new(iter))
499 }
500}
501
502#[derive(thiserror::Error, Debug)]
503#[error("{}", self.error.get_message())]
504pub struct Report {
505 pub error: KclError,
506 pub kcl_source: String,
507 pub filename: String,
508 pub label: String,
511}
512
513impl miette::Diagnostic for Report {
514 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
515 let family = match self.error {
516 KclError::Lexical { .. } => "Lexical",
517 KclError::Syntax { .. } => "Syntax",
518 KclError::Semantic { .. } => "Semantic",
519 KclError::ImportCycle { .. } => "ImportCycle",
520 KclError::Argument { .. } => "Argument",
521 KclError::Type { .. } => "Type",
522 KclError::UserDefined { .. } => "UserDefined",
523 KclError::Io { .. } => "I/O",
524 KclError::Unexpected { .. } => "Unexpected",
525 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
526 KclError::UndefinedValue { .. } => "UndefinedValue",
527 KclError::InvalidExpression { .. } => "InvalidExpression",
528 KclError::MaxCallStack { .. } => "MaxCallStack",
529 KclError::Refactor { .. } => "Refactor",
530 KclError::Engine { .. } => "Engine",
531 KclError::EngineHangup { .. } => "EngineHangup",
532 KclError::EngineInternal { .. } => "EngineInternal",
533 KclError::Internal { .. } => "Internal",
534 };
535 let error_string = format!("KCL {family} error");
536 Some(Box::new(error_string))
537 }
538
539 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
540 Some(&self.kcl_source)
541 }
542
543 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
544 let iter = self
545 .error
546 .source_ranges()
547 .into_iter()
548 .map(miette::SourceSpan::from)
549 .map(|span| miette::LabeledSpan::new_with_span(Some(self.label.clone()), span));
550 Some(Box::new(iter))
551 }
552}
553
554#[derive(thiserror::Error, Debug)]
555#[error("{}", self.issue.message)]
556pub struct CompilationIssueReport {
557 pub issue: CompilationIssue,
558 pub kcl_source: String,
559 pub filename: String,
560}
561
562impl miette::Diagnostic for CompilationIssueReport {
563 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
564 let tag = match self.issue.tag {
565 Tag::Deprecated => "deprecated",
566 Tag::Unnecessary => "unnecessary",
567 Tag::UnknownNumericUnits => "unknown-numeric-units",
568 Tag::None => return None,
569 };
570 Some(Box::new(format!("KCL {tag}")))
571 }
572
573 fn severity(&self) -> Option<miette::Severity> {
574 Some(match self.issue.severity {
575 Severity::Warning => miette::Severity::Warning,
576 Severity::Error | Severity::Fatal => miette::Severity::Error,
577 })
578 }
579
580 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
581 self.issue
582 .suggestion
583 .as_ref()
584 .map(|s| Box::new(s.title.clone()) as Box<dyn std::fmt::Display>)
585 }
586
587 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
588 Some(&self.kcl_source)
589 }
590
591 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
592 let span = miette::SourceSpan::from(self.issue.source_range);
593 let label = miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span);
594 Some(Box::new(std::iter::once(label)))
595 }
596}
597
598pub fn render_compilation_issue_miette(
608 top_level_filename: &str,
609 top_level_source: &str,
610 source_files: &IndexMap<ModuleId, ModuleSource>,
611 issue: CompilationIssue,
612) -> String {
613 let module_id = issue.source_range.module_id();
614 let module_source = (!module_id.is_top_level())
615 .then(|| source_files.get(&module_id))
616 .flatten();
617 let (filename, kcl_source) = match module_source {
618 Some(module_source) => (module_source.path.to_string(), module_source.source.clone()),
619 None => (top_level_filename.to_owned(), top_level_source.to_owned()),
620 };
621 let report = CompilationIssueReport {
622 issue,
623 kcl_source,
624 filename,
625 };
626 let report = miette::Report::new(report);
627 format!("{report:?}")
628}
629
630impl IntoDiagnostic for KclError {
631 fn to_lsp_diagnostics(&self, code: &str, _uri: &tower_lsp::lsp_types::Url) -> Vec<Diagnostic> {
632 let message = self.get_message();
633 let source_ranges = self.source_ranges();
634
635 let module_id = ModuleId::default();
637 let source_ranges = source_ranges
638 .iter()
639 .filter(|r| r.module_id() == module_id)
640 .collect::<Vec<_>>();
641
642 let mut diagnostics = Vec::new();
643 for source_range in &source_ranges {
644 diagnostics.push(Diagnostic {
645 range: source_range.to_lsp_range(code),
646 severity: Some(self.severity()),
647 code: None,
648 code_description: None,
650 source: Some("kcl".to_string()),
651 related_information: None,
652 message: message.clone(),
653 tags: None,
654 data: None,
655 });
656 }
657
658 diagnostics
659 }
660
661 fn severity(&self) -> DiagnosticSeverity {
662 DiagnosticSeverity::ERROR
663 }
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669
670 #[test]
671 fn missing_filename_mapping_does_not_panic_when_building_diagnostics() {
672 let error = KclErrorWithOutputs::no_outputs(KclError::new_semantic(KclErrorDetails::new(
673 "boom".to_owned(),
674 vec![SourceRange::new(0, 1, ModuleId::from_usize(9))],
675 )));
676
677 let diagnostics = error.to_lsp_diagnostics("x", &"file:///test.kcl".try_into().unwrap());
678
679 assert_eq!(diagnostics.len(), 1);
680 assert_eq!(diagnostics[0].message, "semantic: boom");
681 assert_eq!(diagnostics[0].related_information, None);
682 }
683
684 #[test]
685 fn lsp_diagnostics_anchor_at_top_level_and_relate_imported_frames() {
686 let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
687 let imported_code = "// comment\nexport brokenValue = missingName + 1\n";
690 let imported_module = ModuleId::from_usize(1);
691 let missing_name_start = imported_code.find("missingName").unwrap();
692 let imported_range = SourceRange::new(missing_name_start, missing_name_start + 11, imported_module);
693 let import_stmt_range = SourceRange::new(0, 41, ModuleId::default());
694
695 let error = KclError::new_semantic(KclErrorDetails::new(
696 "`missingName` is not defined".to_owned(),
697 vec![imported_range],
698 ))
699 .add_import_location("assembly.kcl", import_stmt_range);
700 let mut error = KclErrorWithOutputs::no_outputs(error);
701 error.source_files.insert(
702 imported_module,
703 ModuleSource {
704 source: imported_code.to_owned(),
705 path: ModulePath::Local {
706 value: "/project/assembly.kcl".into(),
707 original_import_path: None,
708 },
709 },
710 );
711
712 let diagnostics = error.to_lsp_diagnostics(main_code, &"file:///project/main.kcl".try_into().unwrap());
713
714 assert_eq!(diagnostics.len(), 1);
717 assert_eq!(diagnostics[0].range.start.line, 0);
718 assert_eq!(diagnostics[0].range.end.line, 0);
719
720 let related = diagnostics[0].related_information.as_ref().unwrap();
723 assert_eq!(related.len(), 1);
724 assert!(related[0].location.uri.as_str().ends_with("assembly.kcl"));
725 assert_eq!(related[0].location.range.start.line, 1);
726 assert_eq!(related[0].message, "import assembly.kcl");
727 }
728
729 fn report_for(ranges: Vec<SourceRange>) -> ReportWithOutputs {
730 let error = KclError::new_semantic(KclErrorDetails::new("boom".to_owned(), ranges));
731 KclErrorWithOutputs::no_outputs(error)
732 .into_miette_report_with_outputs("code")
733 .unwrap()
734 }
735
736 #[test]
737 fn overlapping_same_file_ranges_become_related_reports() {
738 let module = ModuleId::default();
739 let narrow = SourceRange::new(10, 16, module);
740 let wide = SourceRange::new(0, 20, module);
741 let disjoint = SourceRange::new(30, 40, module);
742
743 let report = report_for(vec![narrow, wide, disjoint]);
744
745 assert_eq!(report.primary_labels.len(), 2);
748 assert_eq!(report.related.len(), 1);
749 assert_eq!(report.related[0].error.source_ranges(), vec![wide]);
750 }
751
752 #[test]
753 fn other_module_ranges_become_related_reports() {
754 let inner = SourceRange::new(0, 5, ModuleId::from_usize(7));
755 let outer = SourceRange::new(10, 20, ModuleId::default());
756
757 let report = report_for(vec![inner, outer]);
758
759 assert_eq!(report.primary_labels.len(), 1);
760 assert_eq!(report.related.len(), 1);
761 assert_eq!(report.related[0].error.source_ranges(), vec![outer]);
762 }
763
764 #[test]
765 fn labels_use_frame_names_when_available() {
766 let module = ModuleId::default();
767 let inner = SourceRange::new(10, 16, module);
768 let mid_call = SourceRange::new(30, 40, module);
769 let outer_call = SourceRange::new(0, 20, module);
770 let import_site = SourceRange::new(0, 5, ModuleId::from_usize(2));
771 let error = KclError::new_semantic(KclErrorDetails::new("boom".to_owned(), vec![inner]))
772 .add_unwind_location(Some("f".to_owned()), mid_call)
773 .add_unwind_location(Some("g".to_owned()), outer_call)
774 .add_import_location("part.kcl", import_site);
775
776 let report = KclErrorWithOutputs::no_outputs(error)
777 .into_miette_report_with_outputs("code")
778 .unwrap();
779
780 assert_eq!(report.primary_labels.len(), 2);
785 assert_eq!(report.primary_labels[1].label(), Some("in g()"));
786 assert_eq!(
787 report.related.iter().map(|r| r.label.as_str()).collect::<Vec<_>>(),
788 ["import part.kcl", report.related[1].filename.as_str()]
789 );
790 }
791
792 #[test]
793 fn repeated_frames_do_not_stack_duplicate_labels() {
794 let module = ModuleId::default();
797 let range = SourceRange::new(10, 16, module);
798
799 let report = report_for(vec![range, range, range]);
800
801 assert_eq!(report.primary_labels.len(), 1);
802 assert_eq!(report.related.len(), 2);
803 }
804
805 #[test]
806 fn compilation_issues_render_against_the_module_their_range_points_into() {
807 let imported_module = ModuleId::from_usize(1);
808 let imported_code = "// enough leading padding to push the range out of the top level\nexport value = 1 * 2\n";
811 let mul_start = imported_code.find("1 * 2").unwrap();
812 let mut source_files = IndexMap::new();
813 source_files.insert(
814 imported_module,
815 ModuleSource {
816 source: imported_code.to_owned(),
817 path: ModulePath::Local {
818 value: "/project/derived.kcl".into(),
819 original_import_path: None,
820 },
821 },
822 );
823 let issue_at = |source_range| CompilationIssue {
824 source_range,
825 message: "unknown units".to_owned(),
826 suggestion: None,
827 severity: Severity::Warning,
828 tag: Tag::UnknownNumericUnits,
829 };
830
831 let report = render_compilation_issue_miette(
833 "/project/main.kcl",
834 "top",
835 &source_files,
836 issue_at(SourceRange::new(mul_start, mul_start + 5, imported_module)),
837 );
838 assert!(report.contains("derived.kcl"), "{report}");
839 assert!(report.contains("1 * 2"), "{report}");
840 assert!(!report.contains("OutOfBounds"), "{report}");
841
842 let report = render_compilation_issue_miette(
845 "/project/main.kcl",
846 "top",
847 &source_files,
848 issue_at(SourceRange::new(0, 3, ModuleId::default())),
849 );
850 assert!(report.contains("main.kcl"), "{report}");
851
852 let report = render_compilation_issue_miette(
855 "/project/main.kcl",
856 "top",
857 &source_files,
858 issue_at(SourceRange::new(0, 3, ModuleId::from_usize(9))),
859 );
860 assert!(report.contains("main.kcl"), "{report}");
861 }
862}