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