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