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