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