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 into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
251 let source_ranges = self.error.source_ranges();
252
253 let first_source_range = *source_ranges
260 .first()
261 .ok_or_else(|| anyhow::anyhow!("No source ranges found"))?;
262 let primary_module_id = first_source_range.module_id();
263
264 let module_source = |module_id: ModuleId| {
265 self.source_files.get(&module_id).cloned().unwrap_or(ModuleSource {
266 source: code.to_string(),
267 path: self.filenames.get(&module_id).cloned().unwrap_or(ModulePath::Main),
268 })
269 };
270 let source = module_source(primary_module_id);
271 let filename = source.path.to_string();
272 let kcl_source = source.source;
273
274 let backtrace = self.error.backtrace();
277
278 let mut primary_labels = vec![miette::LabeledSpan::new_with_span(
279 Some(filename.clone()),
280 miette::SourceSpan::from(first_source_range),
281 )];
282 let mut kept_ranges = vec![first_source_range];
283 let mut related = Vec::new();
284 for (index, source_range) in source_ranges.iter().copied().enumerate().skip(1) {
285 let keep = source_range.module_id() == primary_module_id
286 && !kept_ranges.iter().any(|kept| ranges_overlap(*kept, source_range));
287 let source = module_source(source_range.module_id());
288 let label = frame_label(&backtrace, source_ranges.len(), index).unwrap_or_else(|| source.path.to_string());
289 if keep {
290 primary_labels.push(miette::LabeledSpan::new_with_span(
291 Some(label),
292 miette::SourceSpan::from(source_range),
293 ));
294 kept_ranges.push(source_range);
295 } else {
296 let error = self.error.override_source_ranges(vec![source_range]);
297 related.push(Report {
298 error,
299 kcl_source: source.source,
300 filename: source.path.to_string(),
301 label,
302 });
303 }
304 }
305
306 Ok(ReportWithOutputs {
307 error: self,
308 kcl_source,
309 filename,
310 primary_labels,
311 related,
312 })
313 }
314}
315
316fn frame_label(backtrace: &[BacktraceItem], ranges_len: usize, index: usize) -> Option<String> {
322 if backtrace.len() != ranges_len {
323 return None;
324 }
325 let frame = &backtrace[index];
326 let name = frame.fn_name.as_ref()?;
327 match frame.kind {
328 BacktraceItemKind::Import => Some(name.clone()),
329 BacktraceItemKind::Call => Some(format!("in {name}()")),
330 }
331}
332
333fn ranges_overlap(a: SourceRange, b: SourceRange) -> bool {
338 if a.module_id() != b.module_id() {
339 return false;
340 }
341 if a.start() == b.start() && a.end() == b.end() {
342 return true;
343 }
344 a.start() < b.end() && b.start() < a.end()
345}
346
347impl IsRetryable for KclErrorWithOutputs {
348 fn is_retryable(&self) -> bool {
349 matches!(
350 self.error,
351 KclError::EngineHangup { .. } | KclError::EngineInternal { .. }
352 )
353 }
354}
355
356impl IntoDiagnostic for KclErrorWithOutputs {
357 fn to_lsp_diagnostics(&self, code: &str, uri: &tower_lsp::lsp_types::Url) -> Vec<Diagnostic> {
358 let message = self.error.get_message();
359 let source_ranges = self.error.source_ranges();
360 if source_ranges.is_empty() {
361 return Vec::new();
362 }
363
364 let primary_index = source_ranges.iter().position(|range| range.module_id().is_top_level());
371 let primary_range = primary_index.map(|index| source_ranges[index]).unwrap_or_default();
372
373 let backtrace = self.error.backtrace();
374 let related_information: Vec<tower_lsp::lsp_types::DiagnosticRelatedInformation> = source_ranges
375 .iter()
376 .enumerate()
377 .filter(|(index, _)| Some(*index) != primary_index)
378 .filter_map(|(index, source_range)| {
379 let location = if source_range.module_id().is_top_level() {
383 tower_lsp::lsp_types::Location {
384 uri: uri.clone(),
385 range: source_range.to_lsp_range(code),
386 }
387 } else {
388 let source = self.source_files.get(&source_range.module_id()).cloned().or_else(|| {
389 self.filenames
390 .get(&source_range.module_id())
391 .cloned()
392 .map(|path| ModuleSource {
393 source: code.to_string(),
394 path,
395 })
396 })?;
397 let mut filename = source.path.to_string();
398 if !filename.starts_with("file://") {
399 filename = format!("file:///{}", filename.trim_start_matches("/"));
400 }
401 tower_lsp::lsp_types::Location {
402 uri: url::Url::parse(&filename).ok()?,
403 range: source_range.to_lsp_range(&source.source),
404 }
405 };
406 Some(tower_lsp::lsp_types::DiagnosticRelatedInformation {
407 location,
408 message: frame_label(&backtrace, source_ranges.len(), index).unwrap_or_else(|| message.clone()),
409 })
410 })
411 .collect();
412
413 vec![Diagnostic {
414 range: primary_range.to_lsp_range(code),
415 severity: Some(self.severity()),
416 code: None,
417 code_description: None,
419 source: Some("kcl".to_string()),
420 related_information: (!related_information.is_empty()).then_some(related_information),
421 message,
422 tags: None,
423 data: None,
424 }]
425 }
426
427 fn severity(&self) -> DiagnosticSeverity {
428 DiagnosticSeverity::ERROR
429 }
430}
431
432#[derive(thiserror::Error, Debug)]
433#[error("{}", self.error.error.get_message())]
434pub struct ReportWithOutputs {
435 pub error: KclErrorWithOutputs,
436 pub kcl_source: String,
437 pub filename: String,
438 pub primary_labels: Vec<miette::LabeledSpan>,
441 pub related: Vec<Report>,
442}
443
444impl miette::Diagnostic for ReportWithOutputs {
445 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
446 let family = match self.error.error {
447 KclError::Lexical { .. } => "Lexical",
448 KclError::Syntax { .. } => "Syntax",
449 KclError::Semantic { .. } => "Semantic",
450 KclError::ImportCycle { .. } => "ImportCycle",
451 KclError::Argument { .. } => "Argument",
452 KclError::Type { .. } => "Type",
453 KclError::UserDefined { .. } => "UserDefined",
454 KclError::Io { .. } => "I/O",
455 KclError::Unexpected { .. } => "Unexpected",
456 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
457 KclError::UndefinedValue { .. } => "UndefinedValue",
458 KclError::InvalidExpression { .. } => "InvalidExpression",
459 KclError::MaxCallStack { .. } => "MaxCallStack",
460 KclError::Refactor { .. } => "Refactor",
461 KclError::Engine { .. } => "Engine",
462 KclError::EngineHangup { .. } => "EngineHangup",
463 KclError::EngineInternal { .. } => "EngineInternal",
464 KclError::Internal { .. } => "Internal",
465 };
466 let error_string = format!("KCL {family} error");
467 Some(Box::new(error_string))
468 }
469
470 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
471 Some(&self.kcl_source)
472 }
473
474 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
475 Some(Box::new(self.primary_labels.iter().cloned()))
476 }
477
478 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
479 let iter = self.related.iter().map(|r| r as &dyn miette::Diagnostic);
480 Some(Box::new(iter))
481 }
482}
483
484#[derive(thiserror::Error, Debug)]
485#[error("{}", self.error.get_message())]
486pub struct Report {
487 pub error: KclError,
488 pub kcl_source: String,
489 pub filename: String,
490 pub label: String,
493}
494
495impl miette::Diagnostic for Report {
496 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
497 let family = match self.error {
498 KclError::Lexical { .. } => "Lexical",
499 KclError::Syntax { .. } => "Syntax",
500 KclError::Semantic { .. } => "Semantic",
501 KclError::ImportCycle { .. } => "ImportCycle",
502 KclError::Argument { .. } => "Argument",
503 KclError::Type { .. } => "Type",
504 KclError::UserDefined { .. } => "UserDefined",
505 KclError::Io { .. } => "I/O",
506 KclError::Unexpected { .. } => "Unexpected",
507 KclError::ValueAlreadyDefined { .. } => "ValueAlreadyDefined",
508 KclError::UndefinedValue { .. } => "UndefinedValue",
509 KclError::InvalidExpression { .. } => "InvalidExpression",
510 KclError::MaxCallStack { .. } => "MaxCallStack",
511 KclError::Refactor { .. } => "Refactor",
512 KclError::Engine { .. } => "Engine",
513 KclError::EngineHangup { .. } => "EngineHangup",
514 KclError::EngineInternal { .. } => "EngineInternal",
515 KclError::Internal { .. } => "Internal",
516 };
517 let error_string = format!("KCL {family} error");
518 Some(Box::new(error_string))
519 }
520
521 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
522 Some(&self.kcl_source)
523 }
524
525 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
526 let iter = self
527 .error
528 .source_ranges()
529 .into_iter()
530 .map(miette::SourceSpan::from)
531 .map(|span| miette::LabeledSpan::new_with_span(Some(self.label.clone()), span));
532 Some(Box::new(iter))
533 }
534}
535
536#[derive(thiserror::Error, Debug)]
537#[error("{}", self.issue.message)]
538pub struct CompilationIssueReport {
539 pub issue: CompilationIssue,
540 pub kcl_source: String,
541 pub filename: String,
542}
543
544impl miette::Diagnostic for CompilationIssueReport {
545 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
546 let tag = match self.issue.tag {
547 Tag::Deprecated => "deprecated",
548 Tag::Unnecessary => "unnecessary",
549 Tag::UnknownNumericUnits => "unknown-numeric-units",
550 Tag::None => return None,
551 };
552 Some(Box::new(format!("KCL {tag}")))
553 }
554
555 fn severity(&self) -> Option<miette::Severity> {
556 Some(match self.issue.severity {
557 Severity::Warning => miette::Severity::Warning,
558 Severity::Error | Severity::Fatal => miette::Severity::Error,
559 })
560 }
561
562 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
563 self.issue
564 .suggestion
565 .as_ref()
566 .map(|s| Box::new(s.title.clone()) as Box<dyn std::fmt::Display>)
567 }
568
569 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
570 Some(&self.kcl_source)
571 }
572
573 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
574 let span = miette::SourceSpan::from(self.issue.source_range);
575 let label = miette::LabeledSpan::new_with_span(Some(self.filename.to_string()), span);
576 Some(Box::new(std::iter::once(label)))
577 }
578}
579
580pub fn render_compilation_issue_miette(
590 top_level_filename: &str,
591 top_level_source: &str,
592 source_files: &IndexMap<ModuleId, ModuleSource>,
593 issue: CompilationIssue,
594) -> String {
595 let module_id = issue.source_range.module_id();
596 let module_source = (!module_id.is_top_level())
597 .then(|| source_files.get(&module_id))
598 .flatten();
599 let (filename, kcl_source) = match module_source {
600 Some(module_source) => (module_source.path.to_string(), module_source.source.clone()),
601 None => (top_level_filename.to_owned(), top_level_source.to_owned()),
602 };
603 let report = CompilationIssueReport {
604 issue,
605 kcl_source,
606 filename,
607 };
608 let report = miette::Report::new(report);
609 format!("{report:?}")
610}
611
612impl IntoDiagnostic for KclError {
613 fn to_lsp_diagnostics(&self, code: &str, _uri: &tower_lsp::lsp_types::Url) -> Vec<Diagnostic> {
614 let message = self.get_message();
615 let source_ranges = self.source_ranges();
616
617 let module_id = ModuleId::default();
619 let source_ranges = source_ranges
620 .iter()
621 .filter(|r| r.module_id() == module_id)
622 .collect::<Vec<_>>();
623
624 let mut diagnostics = Vec::new();
625 for source_range in &source_ranges {
626 diagnostics.push(Diagnostic {
627 range: source_range.to_lsp_range(code),
628 severity: Some(self.severity()),
629 code: None,
630 code_description: None,
632 source: Some("kcl".to_string()),
633 related_information: None,
634 message: message.clone(),
635 tags: None,
636 data: None,
637 });
638 }
639
640 diagnostics
641 }
642
643 fn severity(&self) -> DiagnosticSeverity {
644 DiagnosticSeverity::ERROR
645 }
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651
652 #[test]
653 fn missing_filename_mapping_does_not_panic_when_building_diagnostics() {
654 let error = KclErrorWithOutputs::no_outputs(KclError::new_semantic(KclErrorDetails::new(
655 "boom".to_owned(),
656 vec![SourceRange::new(0, 1, ModuleId::from_usize(9))],
657 )));
658
659 let diagnostics = error.to_lsp_diagnostics("x", &"file:///test.kcl".try_into().unwrap());
660
661 assert_eq!(diagnostics.len(), 1);
662 assert_eq!(diagnostics[0].message, "semantic: boom");
663 assert_eq!(diagnostics[0].related_information, None);
664 }
665
666 #[test]
667 fn lsp_diagnostics_anchor_at_top_level_and_relate_imported_frames() {
668 let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
669 let imported_code = "// comment\nexport brokenValue = missingName + 1\n";
672 let imported_module = ModuleId::from_usize(1);
673 let missing_name_start = imported_code.find("missingName").unwrap();
674 let imported_range = SourceRange::new(missing_name_start, missing_name_start + 11, imported_module);
675 let import_stmt_range = SourceRange::new(0, 41, ModuleId::default());
676
677 let error = KclError::new_semantic(KclErrorDetails::new(
678 "`missingName` is not defined".to_owned(),
679 vec![imported_range],
680 ))
681 .add_import_location("assembly.kcl", import_stmt_range);
682 let mut error = KclErrorWithOutputs::no_outputs(error);
683 error.source_files.insert(
684 imported_module,
685 ModuleSource {
686 source: imported_code.to_owned(),
687 path: ModulePath::Local {
688 value: "/project/assembly.kcl".into(),
689 original_import_path: None,
690 },
691 },
692 );
693
694 let diagnostics = error.to_lsp_diagnostics(main_code, &"file:///project/main.kcl".try_into().unwrap());
695
696 assert_eq!(diagnostics.len(), 1);
699 assert_eq!(diagnostics[0].range.start.line, 0);
700 assert_eq!(diagnostics[0].range.end.line, 0);
701
702 let related = diagnostics[0].related_information.as_ref().unwrap();
705 assert_eq!(related.len(), 1);
706 assert!(related[0].location.uri.as_str().ends_with("assembly.kcl"));
707 assert_eq!(related[0].location.range.start.line, 1);
708 assert_eq!(related[0].message, "import assembly.kcl");
709 }
710
711 fn report_for(ranges: Vec<SourceRange>) -> ReportWithOutputs {
712 let error = KclError::new_semantic(KclErrorDetails::new("boom".to_owned(), ranges));
713 KclErrorWithOutputs::no_outputs(error)
714 .into_miette_report_with_outputs("code")
715 .unwrap()
716 }
717
718 #[test]
719 fn overlapping_same_file_ranges_become_related_reports() {
720 let module = ModuleId::default();
721 let narrow = SourceRange::new(10, 16, module);
722 let wide = SourceRange::new(0, 20, module);
723 let disjoint = SourceRange::new(30, 40, module);
724
725 let report = report_for(vec![narrow, wide, disjoint]);
726
727 assert_eq!(report.primary_labels.len(), 2);
730 assert_eq!(report.related.len(), 1);
731 assert_eq!(report.related[0].error.source_ranges(), vec![wide]);
732 }
733
734 #[test]
735 fn other_module_ranges_become_related_reports() {
736 let inner = SourceRange::new(0, 5, ModuleId::from_usize(7));
737 let outer = SourceRange::new(10, 20, ModuleId::default());
738
739 let report = report_for(vec![inner, outer]);
740
741 assert_eq!(report.primary_labels.len(), 1);
742 assert_eq!(report.related.len(), 1);
743 assert_eq!(report.related[0].error.source_ranges(), vec![outer]);
744 }
745
746 #[test]
747 fn labels_use_frame_names_when_available() {
748 let module = ModuleId::default();
749 let inner = SourceRange::new(10, 16, module);
750 let mid_call = SourceRange::new(30, 40, module);
751 let outer_call = SourceRange::new(0, 20, module);
752 let import_site = SourceRange::new(0, 5, ModuleId::from_usize(2));
753 let error = KclError::new_semantic(KclErrorDetails::new("boom".to_owned(), vec![inner]))
754 .add_unwind_location(Some("f".to_owned()), mid_call)
755 .add_unwind_location(Some("g".to_owned()), outer_call)
756 .add_import_location("part.kcl", import_site);
757
758 let report = KclErrorWithOutputs::no_outputs(error)
759 .into_miette_report_with_outputs("code")
760 .unwrap();
761
762 assert_eq!(report.primary_labels.len(), 2);
767 assert_eq!(report.primary_labels[1].label(), Some("in g()"));
768 assert_eq!(
769 report.related.iter().map(|r| r.label.as_str()).collect::<Vec<_>>(),
770 ["import part.kcl", report.related[1].filename.as_str()]
771 );
772 }
773
774 #[test]
775 fn repeated_frames_do_not_stack_duplicate_labels() {
776 let module = ModuleId::default();
779 let range = SourceRange::new(10, 16, module);
780
781 let report = report_for(vec![range, range, range]);
782
783 assert_eq!(report.primary_labels.len(), 1);
784 assert_eq!(report.related.len(), 2);
785 }
786
787 #[test]
788 fn compilation_issues_render_against_the_module_their_range_points_into() {
789 let imported_module = ModuleId::from_usize(1);
790 let imported_code = "// enough leading padding to push the range out of the top level\nexport value = 1 * 2\n";
793 let mul_start = imported_code.find("1 * 2").unwrap();
794 let mut source_files = IndexMap::new();
795 source_files.insert(
796 imported_module,
797 ModuleSource {
798 source: imported_code.to_owned(),
799 path: ModulePath::Local {
800 value: "/project/derived.kcl".into(),
801 original_import_path: None,
802 },
803 },
804 );
805 let issue_at = |source_range| CompilationIssue {
806 source_range,
807 message: "unknown units".to_owned(),
808 suggestion: None,
809 severity: Severity::Warning,
810 tag: Tag::UnknownNumericUnits,
811 };
812
813 let report = render_compilation_issue_miette(
815 "/project/main.kcl",
816 "top",
817 &source_files,
818 issue_at(SourceRange::new(mul_start, mul_start + 5, imported_module)),
819 );
820 assert!(report.contains("derived.kcl"), "{report}");
821 assert!(report.contains("1 * 2"), "{report}");
822 assert!(!report.contains("OutOfBounds"), "{report}");
823
824 let report = render_compilation_issue_miette(
827 "/project/main.kcl",
828 "top",
829 &source_files,
830 issue_at(SourceRange::new(0, 3, ModuleId::default())),
831 );
832 assert!(report.contains("main.kcl"), "{report}");
833
834 let report = render_compilation_issue_miette(
837 "/project/main.kcl",
838 "top",
839 &source_files,
840 issue_at(SourceRange::new(0, 3, ModuleId::from_usize(9))),
841 );
842 assert!(report.contains("main.kcl"), "{report}");
843 }
844}