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