1use crate::{
8 ProgrammaticError,
9 runtime::{
10 AuditProgrammaticOutput, BoundaryViolationsProgrammaticOutput,
11 CircularDependenciesProgrammaticOutput, CombinedProgrammaticOutput,
12 DeadCodeProgrammaticOutput, DecisionSurfaceProgrammaticOutput,
13 DuplicationProgrammaticOutput, FeatureFlagsProgrammaticOutput, HealthJsonReportInput,
14 HealthProgrammaticOutput, TraceCloneProgrammaticOutput, TraceDependencyProgrammaticOutput,
15 TraceErrorProgrammaticOutput, TraceExportProgrammaticOutput, TraceFileProgrammaticOutput,
16 TraceImportPathProgrammaticOutput, serialize_health_report_json,
17 },
18};
19use fallow_output::{
20 AUDIT_SCHEMA_VERSION, CheckOutput, GroupByMode, RootEnvelopeMode,
21 build_decision_surface_output, serialize_check_json_output,
22 serialize_decision_surface_json_output, serialize_dupes_json_output,
23 serialize_feature_flags_json_output, strip_root_prefix,
24};
25use fallow_types::envelope::{ElapsedMs, SchemaVersion, ToolVersion};
26use fallow_types::workspace::{WorkspaceDiagnostic, merge_workspace_diagnostics};
27use serde::Serialize;
28use std::path::Path;
29use std::time::Duration;
30
31type ProgrammaticResult<T> = Result<T, ProgrammaticError>;
32
33pub fn serialize_combined_programmatic_json(
39 output: CombinedProgrammaticOutput,
40) -> ProgrammaticResult<serde_json::Value> {
41 let CombinedProgrammaticOutput {
42 dead_code,
43 duplication,
44 health,
45 root,
46 elapsed,
47 explain,
48 next_steps,
49 envelope_mode,
50 telemetry_analysis_run_id,
51 } = output;
52 let workspace_diagnostics =
53 combined_workspace_diagnostics(dead_code.as_ref(), health.as_ref(), duplication.as_ref());
54 crate::serialize_combined_json(crate::CombinedJsonOutputInput {
55 gate_outcomes: None,
56 check: dead_code
57 .as_ref()
58 .map(|dead_code| crate::CombinedCheckJsonSection {
59 results: &dead_code.output.results,
60 root: &dead_code.root,
61 elapsed: Duration::from_millis(dead_code.output.elapsed_ms.0),
62 config_fixable: dead_code.config_fixable,
63 extras: crate::CheckJsonExtraOutputs::default(),
64 }),
65 dupes: duplication
66 .as_ref()
67 .map(|duplication| &duplication.output.report),
68 health: health.as_ref().map(|health| &health.report),
69 root: &root,
70 elapsed,
71 explain,
72 type_aware: None,
73 workspace_diagnostics,
74 next_steps,
75 envelope_mode,
76 telemetry_analysis_run_id: telemetry_analysis_run_id.as_deref(),
77 })
78 .map_err(|err| {
79 ProgrammaticError::new(format!("failed to serialize combined report: {err}"), 2)
80 .with_code("FALLOW_SERIALIZE_COMBINED_REPORT")
81 .with_context("combined")
82 })
83}
84
85fn combined_workspace_diagnostics(
100 dead_code: Option<&DeadCodeProgrammaticOutput>,
101 health: Option<&HealthProgrammaticOutput>,
102 duplication: Option<&DuplicationProgrammaticOutput>,
103) -> Vec<WorkspaceDiagnostic> {
104 let merged = merge_workspace_diagnostics(
105 dead_code.map_or_else(Vec::new, |dead_code| {
106 dead_code.output.workspace_diagnostics.clone()
107 }),
108 health.map_or_else(Vec::new, |health| health.workspace_diagnostics.clone()),
109 );
110 merge_workspace_diagnostics(
111 merged,
112 duplication.map_or_else(Vec::new, |duplication| {
113 duplication.output.workspace_diagnostics.clone()
114 }),
115 )
116}
117
118pub fn serialize_decision_surface_programmatic_json(
124 output: DecisionSurfaceProgrammaticOutput,
125) -> ProgrammaticResult<serde_json::Value> {
126 let DecisionSurfaceProgrammaticOutput {
127 surface,
128 elapsed: _,
129 envelope_mode,
130 telemetry_analysis_run_id,
131 } = output;
132 let payload = build_decision_surface_output(&surface);
133 serialize_decision_surface_json_output(
134 payload,
135 envelope_mode,
136 telemetry_analysis_run_id.as_deref(),
137 )
138 .map_err(|err| {
139 ProgrammaticError::new(format!("failed to serialize decision surface: {err}"), 2)
140 .with_code("FALLOW_SERIALIZE_DECISION_SURFACE")
141 .with_context("decision-surface")
142 })
143}
144
145pub fn serialize_audit_programmatic_json(
151 output: AuditProgrammaticOutput,
152) -> ProgrammaticResult<serde_json::Value> {
153 let base_snapshot = output.base_snapshot.as_ref();
154 let dead_code = output
155 .dead_code
156 .as_ref()
157 .map(|dead_code| serialize_audit_dead_code(dead_code, base_snapshot))
158 .transpose()?;
159 let duplication = output
160 .duplication
161 .as_ref()
162 .map(|duplication| serialize_audit_duplication(duplication, base_snapshot))
163 .transpose()?;
164 let complexity = output
165 .complexity
166 .as_ref()
167 .map(|complexity| serialize_audit_complexity(complexity, base_snapshot))
168 .transpose()?;
169
170 crate::serialize_audit_json(
171 crate::AuditJsonOutputInput {
172 gate_outcomes: None,
173 header: crate::AuditJsonHeaderInput {
174 schema_version: SchemaVersion(AUDIT_SCHEMA_VERSION),
175 version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
176 verdict: output.verdict,
177 changed_files_count: u32::try_from(output.changed_files_count).unwrap_or(u32::MAX),
178 base_ref: output.base_ref,
179 base_description: output.base_description,
180 head_sha: output.head_sha,
181 elapsed_ms: ElapsedMs(
182 u64::try_from(output.elapsed.as_millis()).unwrap_or(u64::MAX),
183 ),
184 base_snapshot_skipped: output.base_snapshot_skipped,
185 summary: output.summary,
186 attribution: output.attribution,
187 },
188 meta: None,
189 dead_code,
190 duplication,
191 complexity,
192 next_steps: output.next_steps,
193 },
194 output.envelope_mode,
195 output.telemetry_analysis_run_id.as_deref(),
196 )
197 .map_err(|err| {
198 ProgrammaticError::new(format!("failed to serialize audit report: {err}"), 2)
199 .with_code("FALLOW_SERIALIZE_AUDIT_REPORT")
200 .with_context("audit")
201 })
202}
203
204fn serialize_audit_dead_code(
215 output: &DeadCodeProgrammaticOutput,
216 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
217) -> ProgrammaticResult<serde_json::Value> {
218 let mut json = crate::serialize_check_json_payload(crate::CheckJsonPayloadInput {
219 results: &output.output.results,
220 root: &output.root,
221 elapsed: Duration::from_millis(output.output.elapsed_ms.0),
222 config_fixable: output.config_fixable,
223 extras: crate::CheckJsonExtraOutputs::default(),
224 workspace_diagnostics: output.output.workspace_diagnostics.clone(),
225 })
226 .map_err(|err| {
227 ProgrammaticError::new(format!("failed to serialize audit dead-code: {err}"), 2)
228 .with_code("FALLOW_SERIALIZE_AUDIT_DEAD_CODE")
229 .with_context("audit.deadCode")
230 })?;
231 if let Some(base) = base_snapshot {
232 if has_persisted_introduced_flags(&json) {
233 crate::audit_keys::annotate_stale_suppressions_json(
234 &mut json,
235 &output.output.results,
236 &output.root,
237 &base.dead_code,
238 );
239 } else {
240 crate::audit_keys::annotate_dead_code_json(
241 &mut json,
242 &output.output.results,
243 &output.root,
244 &base.dead_code,
245 );
246 }
247 }
248 Ok(json)
249}
250
251fn serialize_audit_duplication(
252 output: &DuplicationProgrammaticOutput,
253 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
254) -> ProgrammaticResult<serde_json::Value> {
255 let mut json = serde_json::to_value(&output.output.report).map_err(|err| {
256 ProgrammaticError::new(format!("failed to serialize audit duplication: {err}"), 2)
257 .with_code("FALLOW_SERIALIZE_AUDIT_DUPLICATION")
258 .with_context("audit.duplication")
259 })?;
260 let root_prefix = format!("{}/", output.root.display());
261 strip_root_prefix(&mut json, &root_prefix);
262 if let Some(base) = base_snapshot
263 && !has_persisted_introduced_flags(&json)
264 {
265 annotate_audit_duplication_json(&mut json, output, &base.dupes);
266 }
267 Ok(json)
268}
269
270fn serialize_audit_complexity(
271 output: &HealthProgrammaticOutput,
272 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
273) -> ProgrammaticResult<serde_json::Value> {
274 let mut json = serde_json::to_value(&output.report).map_err(|err| {
275 ProgrammaticError::new(format!("failed to serialize audit complexity: {err}"), 2)
276 .with_code("FALLOW_SERIALIZE_AUDIT_COMPLEXITY")
277 .with_context("audit.complexity")
278 })?;
279 let root_prefix = format!("{}/", output.root.display());
280 strip_root_prefix(&mut json, &root_prefix);
281 if let Some(base) = base_snapshot {
282 crate::audit_keys::annotate_health_json(
283 &mut json,
284 &output.report,
285 &output.root,
286 &base.health,
287 );
288 }
289 Ok(json)
290}
291
292fn has_persisted_introduced_flags(json: &serde_json::Value) -> bool {
293 json.as_object().is_some_and(|object| {
294 object.values().any(|value| {
295 value
296 .as_array()
297 .is_some_and(|items| items.iter().any(|item| item.get("introduced").is_some()))
298 })
299 })
300}
301
302fn annotate_audit_duplication_json(
303 json: &mut serde_json::Value,
304 output: &DuplicationProgrammaticOutput,
305 base: &rustc_hash::FxHashSet<String>,
306) {
307 let Some(items) = json
308 .get_mut("clone_groups")
309 .and_then(serde_json::Value::as_array_mut)
310 else {
311 return;
312 };
313 for (item, group) in items.iter_mut().zip(&output.output.report.clone_groups) {
314 if let serde_json::Value::Object(map) = item {
315 let key = crate::audit_keys::dupe_group_key(&group.group, &output.root);
316 map.insert(
317 "introduced".to_string(),
318 serde_json::json!(!base.contains(&key)),
319 );
320 }
321 }
322}
323
324pub fn serialize_dead_code_programmatic_json(
330 output: DeadCodeProgrammaticOutput,
331) -> ProgrammaticResult<serde_json::Value> {
332 let DeadCodeProgrammaticOutput {
333 output,
334 root,
335 config_fixable: _,
336 envelope_mode,
337 telemetry_analysis_run_id,
338 } = output;
339 serialize_check_programmatic_output(
340 output,
341 &root,
342 envelope_mode,
343 telemetry_analysis_run_id.as_deref(),
344 "dead-code",
345 "FALLOW_SERIALIZE_DEAD_CODE_REPORT",
346 )
347}
348
349pub fn serialize_circular_dependencies_programmatic_json(
355 output: CircularDependenciesProgrammaticOutput,
356) -> ProgrammaticResult<serde_json::Value> {
357 let CircularDependenciesProgrammaticOutput {
358 output,
359 root,
360 envelope_mode,
361 telemetry_analysis_run_id,
362 } = output;
363 serialize_check_programmatic_output(
364 output,
365 &root,
366 envelope_mode,
367 telemetry_analysis_run_id.as_deref(),
368 "circular-dependencies",
369 "FALLOW_SERIALIZE_CIRCULAR_DEPENDENCIES_REPORT",
370 )
371}
372
373pub fn serialize_boundary_violations_programmatic_json(
379 output: BoundaryViolationsProgrammaticOutput,
380) -> ProgrammaticResult<serde_json::Value> {
381 let BoundaryViolationsProgrammaticOutput {
382 output,
383 root,
384 envelope_mode,
385 telemetry_analysis_run_id,
386 } = output;
387 serialize_check_programmatic_output(
388 output,
389 &root,
390 envelope_mode,
391 telemetry_analysis_run_id.as_deref(),
392 "boundary-violations",
393 "FALLOW_SERIALIZE_BOUNDARY_VIOLATIONS_REPORT",
394 )
395}
396
397fn serialize_check_programmatic_output(
398 output: CheckOutput,
399 root: &Path,
400 envelope_mode: RootEnvelopeMode,
401 telemetry_analysis_run_id: Option<&str>,
402 context: &'static str,
403 code: &'static str,
404) -> ProgrammaticResult<serde_json::Value> {
405 let mut json = serialize_check_json_output(output, envelope_mode, telemetry_analysis_run_id)
406 .map_err(|err| {
407 ProgrammaticError::new(format!("failed to serialize {context} report: {err}"), 2)
408 .with_code(code)
409 .with_context(context)
410 })?;
411 let root_prefix = format!("{}/", root.display());
412 strip_root_prefix(&mut json, &root_prefix);
413 Ok(json)
414}
415
416pub fn serialize_duplication_programmatic_json(
422 output: DuplicationProgrammaticOutput,
423) -> ProgrammaticResult<serde_json::Value> {
424 let DuplicationProgrammaticOutput {
425 output,
426 root,
427 threshold: _,
428 envelope_mode,
429 telemetry_analysis_run_id,
430 } = output;
431 let mut json =
432 serialize_dupes_json_output(output, envelope_mode, telemetry_analysis_run_id.as_deref())
433 .map_err(|err| {
434 ProgrammaticError::new(format!("failed to serialize duplication report: {err}"), 2)
435 .with_code("FALLOW_SERIALIZE_DUPLICATION_REPORT")
436 .with_context("dupes")
437 })?;
438 let root_prefix = format!("{}/", root.display());
439 strip_root_prefix(&mut json, &root_prefix);
440 Ok(json)
441}
442
443pub fn serialize_feature_flags_programmatic_json(
449 output: FeatureFlagsProgrammaticOutput,
450) -> ProgrammaticResult<serde_json::Value> {
451 serialize_feature_flags_json_output(
452 output.output,
453 output.envelope_mode,
454 output.telemetry_analysis_run_id.as_deref(),
455 )
456 .map_err(|err| {
457 ProgrammaticError::new(
458 format!("failed to serialize feature flags report: {err}"),
459 2,
460 )
461 .with_code("FALLOW_SERIALIZE_FEATURE_FLAGS_REPORT")
462 .with_context("feature-flags")
463 })
464}
465
466pub fn serialize_trace_export_programmatic_json(
472 output: TraceExportProgrammaticOutput,
473) -> ProgrammaticResult<serde_json::Value> {
474 serialize_trace_programmatic_output(
475 output.output,
476 "export trace",
477 "FALLOW_SERIALIZE_TRACE_EXPORT",
478 "trace_export",
479 )
480}
481
482pub fn serialize_trace_file_programmatic_json(
488 output: TraceFileProgrammaticOutput,
489) -> ProgrammaticResult<serde_json::Value> {
490 serialize_trace_programmatic_output(
491 output.output,
492 "file trace",
493 "FALLOW_SERIALIZE_TRACE_FILE",
494 "trace_file",
495 )
496}
497
498pub fn serialize_trace_import_path_programmatic_json(
504 output: TraceImportPathProgrammaticOutput,
505) -> ProgrammaticResult<serde_json::Value> {
506 serialize_trace_programmatic_output(
507 output.output,
508 "import path trace",
509 "FALLOW_SERIALIZE_TRACE_IMPORT_PATH",
510 "trace_import_path",
511 )
512}
513
514pub fn serialize_trace_dependency_programmatic_json(
520 output: TraceDependencyProgrammaticOutput,
521) -> ProgrammaticResult<serde_json::Value> {
522 serialize_trace_programmatic_output(
523 output.output,
524 "dependency trace",
525 "FALLOW_SERIALIZE_TRACE_DEPENDENCY",
526 "trace_dependency",
527 )
528}
529
530pub fn serialize_trace_error_programmatic_json(
536 output: TraceErrorProgrammaticOutput,
537) -> ProgrammaticResult<serde_json::Value> {
538 serialize_trace_programmatic_output(
539 output.output,
540 "stack-trace resolution",
541 "FALLOW_SERIALIZE_TRACE_ERROR",
542 "trace_error",
543 )
544}
545
546pub fn serialize_trace_clone_programmatic_json(
552 output: TraceCloneProgrammaticOutput,
553) -> ProgrammaticResult<serde_json::Value> {
554 serialize_trace_programmatic_output(
555 output.output,
556 "clone trace",
557 "FALLOW_SERIALIZE_TRACE_CLONE",
558 "trace_clone",
559 )
560}
561
562fn serialize_trace_programmatic_output<T: Serialize>(
563 output: T,
564 context: &'static str,
565 code: &'static str,
566 error_context: &'static str,
567) -> ProgrammaticResult<serde_json::Value> {
568 serde_json::to_value(output).map_err(|err| {
569 ProgrammaticError::new(format!("failed to serialize {context}: {err}"), 2)
570 .with_code(code)
571 .with_context(error_context)
572 })
573}
574
575pub fn serialize_health_programmatic_json(
581 output: HealthProgrammaticOutput,
582) -> ProgrammaticResult<serde_json::Value> {
583 let HealthProgrammaticOutput {
584 report,
585 grouping,
586 root,
587 elapsed,
588 explain,
589 workspace_diagnostics,
590 next_steps,
591 envelope_mode,
592 telemetry_analysis_run_id,
593 } = output;
594 let (grouped_by, groups) = grouping.map_or((None, None), |grouping| {
595 (
596 group_by_mode_from_label(grouping.mode),
597 Some(grouping.groups),
598 )
599 });
600 serialize_health_report_json(HealthJsonReportInput {
601 gate_outcomes: None,
602 report,
603 root: &root,
604 elapsed,
605 explain,
606 type_aware: None,
607 grouped_by,
608 groups,
609 workspace_diagnostics,
610 next_steps,
611 envelope_mode,
612 telemetry_analysis_run_id: telemetry_analysis_run_id.as_deref(),
613 })
614 .map_err(|err| {
615 ProgrammaticError::new(format!("failed to serialize health report: {err}"), 2)
616 .with_code("FALLOW_SERIALIZE_HEALTH_REPORT")
617 .with_context("health")
618 })
619}
620
621fn group_by_mode_from_label(label: &str) -> Option<GroupByMode> {
622 match label {
623 "owner" => Some(GroupByMode::Owner),
624 "directory" => Some(GroupByMode::Directory),
625 "package" => Some(GroupByMode::Package),
626 "section" => Some(GroupByMode::Section),
627 _ => None,
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use super::{
634 RootEnvelopeMode, serialize_audit_dead_code, serialize_combined_programmatic_json,
635 };
636 use crate::DupesReportPayload;
637 use crate::runtime::{
638 CombinedProgrammaticOutput, DeadCodeProgrammaticOutput, DuplicationProgrammaticOutput,
639 HealthProgrammaticOutput,
640 };
641 use fallow_output::{
642 CHECK_SCHEMA_VERSION, CheckOutputInput, DUPES_SCHEMA_VERSION, DupesOutputInput,
643 HealthReport, build_check_output, build_dupes_output,
644 };
645 use fallow_types::duplicates::DuplicationReport;
646 use fallow_types::results::AnalysisResults;
647 use fallow_types::workspace::{WorkspaceDiagnostic, WorkspaceDiagnosticKind};
648 use std::path::Path;
649 use std::time::Duration;
650
651 fn dead_code_output(
652 root: &Path,
653 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
654 ) -> DeadCodeProgrammaticOutput {
655 DeadCodeProgrammaticOutput {
656 output: build_check_output(CheckOutputInput {
657 schema_version: CHECK_SCHEMA_VERSION,
658 version: "0.0.0-test".to_owned(),
659 elapsed: Duration::ZERO,
660 results: AnalysisResults::default(),
661 config_fixable: false,
662 meta: None,
663 workspace_diagnostics,
664 next_steps: Vec::new(),
665 }),
666 root: root.to_path_buf(),
667 config_fixable: false,
668 envelope_mode: RootEnvelopeMode::Tagged,
669 telemetry_analysis_run_id: None,
670 }
671 }
672
673 #[test]
677 fn audit_dead_code_section_carries_workspace_diagnostics_root_relative_or_omits_them() {
678 let root = Path::new("/project");
679 let carried = serialize_audit_dead_code(
680 &dead_code_output(
681 root,
682 vec![WorkspaceDiagnostic::new(
683 root,
684 root.join("package.json"),
685 WorkspaceDiagnosticKind::BunLockbOverrideResolutionSkipped,
686 )],
687 ),
688 None,
689 )
690 .expect("audit dead-code JSON");
691 assert_eq!(
692 carried["workspace_diagnostics"][0]["kind"],
693 "bun-lockb-override-resolution-skipped"
694 );
695 assert_eq!(carried["workspace_diagnostics"][0]["path"], "package.json");
696
697 let empty = serialize_audit_dead_code(&dead_code_output(root, Vec::new()), None)
698 .expect("audit dead-code JSON");
699 assert!(
700 empty.get("workspace_diagnostics").is_none(),
701 "an empty list is omitted from the audit dead-code section: {empty}"
702 );
703 }
704
705 fn health_output(
706 root: &Path,
707 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
708 ) -> HealthProgrammaticOutput {
709 HealthProgrammaticOutput {
710 report: HealthReport::default(),
711 grouping: None,
712 root: root.to_path_buf(),
713 elapsed: Duration::ZERO,
714 explain: false,
715 workspace_diagnostics,
716 next_steps: Vec::new(),
717 envelope_mode: RootEnvelopeMode::Tagged,
718 telemetry_analysis_run_id: None,
719 }
720 }
721
722 fn duplication_output(
723 root: &Path,
724 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
725 ) -> DuplicationProgrammaticOutput {
726 DuplicationProgrammaticOutput {
727 output: build_dupes_output(DupesOutputInput {
728 gate_outcomes: None,
729 baseline_staleness: None,
730 schema_version: DUPES_SCHEMA_VERSION,
731 version: "0.0.0-test".to_owned(),
732 elapsed: Duration::ZERO,
733 report: DupesReportPayload::from_report(&DuplicationReport::default()),
734 clone_groups_shown: 0,
735 clone_groups_omitted: 0,
736 clone_families_shown: 0,
737 clone_families_omitted: 0,
738 grouped_by: None,
739 total_issues: None,
740 groups: None,
741 meta: None,
742 workspace_diagnostics,
743 next_steps: Vec::new(),
744 }),
745 root: root.to_path_buf(),
746 threshold: 0.0,
747 envelope_mode: RootEnvelopeMode::Tagged,
748 telemetry_analysis_run_id: None,
749 }
750 }
751
752 fn combined_output(
753 root: &Path,
754 dead_code: Option<DeadCodeProgrammaticOutput>,
755 health: Option<HealthProgrammaticOutput>,
756 ) -> CombinedProgrammaticOutput {
757 combined_output_with_duplication(root, dead_code, health, None)
758 }
759
760 fn combined_output_with_duplication(
761 root: &Path,
762 dead_code: Option<DeadCodeProgrammaticOutput>,
763 health: Option<HealthProgrammaticOutput>,
764 duplication: Option<DuplicationProgrammaticOutput>,
765 ) -> CombinedProgrammaticOutput {
766 CombinedProgrammaticOutput {
767 dead_code,
768 duplication,
769 health,
770 root: root.to_path_buf(),
771 elapsed: Duration::ZERO,
772 explain: false,
773 next_steps: Vec::new(),
774 envelope_mode: RootEnvelopeMode::Tagged,
775 telemetry_analysis_run_id: None,
776 }
777 }
778
779 fn bun_lockb_diagnostic(root: &Path) -> WorkspaceDiagnostic {
780 WorkspaceDiagnostic::new(
781 root,
782 root.join("package.json"),
783 WorkspaceDiagnosticKind::BunLockbOverrideResolutionSkipped,
784 )
785 }
786
787 fn large_file_diagnostic(root: &Path, relative: &str) -> WorkspaceDiagnostic {
788 WorkspaceDiagnostic::new(
789 root,
790 root.join(relative),
791 WorkspaceDiagnosticKind::SkippedLargeFile {
792 size_bytes: 6_000_000,
793 },
794 )
795 }
796
797 fn root_kinds(document: &serde_json::Value) -> Vec<String> {
798 document["workspace_diagnostics"]
799 .as_array()
800 .cloned()
801 .unwrap_or_default()
802 .iter()
803 .map(|diagnostic| diagnostic["kind"].as_str().unwrap_or_default().to_owned())
804 .collect()
805 }
806
807 #[test]
811 fn combined_programmatic_root_carries_workspace_diagnostics_or_omits_them() {
812 let root = Path::new("/project");
813 let carried = serialize_combined_programmatic_json(combined_output(
814 root,
815 Some(dead_code_output(root, vec![bun_lockb_diagnostic(root)])),
816 None,
817 ))
818 .expect("combined JSON");
819 assert_eq!(
820 carried["workspace_diagnostics"][0]["kind"],
821 "bun-lockb-override-resolution-skipped"
822 );
823 assert_eq!(carried["workspace_diagnostics"][0]["path"], "package.json");
824 assert!(
825 carried["check"].is_object(),
826 "the check section is present, so the absence check below is not vacuous: {carried}"
827 );
828 assert!(
829 carried["check"].get("workspace_diagnostics").is_none(),
830 "the check section is not a second carrier: {carried}"
831 );
832
833 let empty = serialize_combined_programmatic_json(combined_output(
834 root,
835 Some(dead_code_output(root, Vec::new())),
836 None,
837 ))
838 .expect("combined JSON");
839 assert!(
840 empty.get("workspace_diagnostics").is_none(),
841 "an empty list is omitted from the combined root: {empty}"
842 );
843 }
844
845 #[test]
849 fn combined_programmatic_root_carries_workspace_diagnostics_without_a_dead_code_section() {
850 let root = Path::new("/project");
851 let carried = serialize_combined_programmatic_json(combined_output(
852 root,
853 None,
854 Some(health_output(root, vec![bun_lockb_diagnostic(root)])),
855 ))
856 .expect("combined JSON");
857 assert!(
858 carried.get("check").is_none(),
859 "this run has no check section: {carried}"
860 );
861 assert_eq!(
862 carried["workspace_diagnostics"][0]["kind"],
863 "bun-lockb-override-resolution-skipped"
864 );
865 assert_eq!(carried["workspace_diagnostics"][0]["path"], "package.json");
866 }
867
868 #[test]
872 fn combined_programmatic_root_carries_workspace_diagnostics_from_a_duplication_only_run() {
873 let root = Path::new("/project");
874 let carried = serialize_combined_programmatic_json(combined_output_with_duplication(
875 root,
876 None,
877 None,
878 Some(duplication_output(
879 root,
880 vec![large_file_diagnostic(root, "src/generated.ts")],
881 )),
882 ))
883 .expect("combined JSON");
884 assert!(
885 carried.get("check").is_none() && carried.get("health").is_none(),
886 "only the dupes section ran: {carried}"
887 );
888 assert_eq!(root_kinds(&carried), ["skipped-large-file"]);
889 assert_eq!(
890 carried["workspace_diagnostics"][0]["path"],
891 "src/generated.ts"
892 );
893 }
894
895 #[test]
903 fn combined_programmatic_root_unions_sections_that_recorded_different_diagnostics() {
904 let root = Path::new("/project");
905 let shared = bun_lockb_diagnostic(root);
906 let carried = serialize_combined_programmatic_json(combined_output_with_duplication(
907 root,
908 Some(dead_code_output(
909 root,
910 vec![shared.clone(), large_file_diagnostic(root, "src/big.ts")],
911 )),
912 Some(health_output(root, vec![shared.clone()])),
913 Some(duplication_output(
914 root,
915 vec![shared, large_file_diagnostic(root, "src/other.ts")],
916 )),
917 ))
918 .expect("combined JSON");
919 assert_eq!(
920 root_kinds(&carried),
921 [
922 "bun-lockb-override-resolution-skipped",
923 "skipped-large-file",
924 "skipped-large-file",
925 ],
926 "the union keeps dead-code order first and drops the repeats: {}",
927 carried["workspace_diagnostics"]
928 );
929 let paths: Vec<&str> = carried["workspace_diagnostics"]
930 .as_array()
931 .expect("array")
932 .iter()
933 .map(|diagnostic| diagnostic["path"].as_str().unwrap_or_default())
934 .collect();
935 assert_eq!(paths, ["package.json", "src/big.ts", "src/other.ts"]);
936 }
937
938 #[test]
943 fn combined_programmatic_root_keeps_diagnostics_an_empty_dead_code_section_missed() {
944 let root = Path::new("/project");
945 let carried = serialize_combined_programmatic_json(combined_output_with_duplication(
946 root,
947 Some(dead_code_output(root, Vec::new())),
948 Some(health_output(
949 root,
950 vec![large_file_diagnostic(root, "src/big.test.ts")],
951 )),
952 None,
953 ))
954 .expect("combined JSON");
955 assert_eq!(root_kinds(&carried), ["skipped-large-file"]);
956 assert_eq!(
957 carried["workspace_diagnostics"][0]["path"],
958 "src/big.test.ts"
959 );
960 }
961}