1use crate::{
8 ProgrammaticError,
9 runtime::{
10 AuditProgrammaticOutput, BoundaryViolationsProgrammaticOutput,
11 CircularDependenciesProgrammaticOutput, CombinedProgrammaticOutput,
12 DeadCodeProgrammaticOutput, DecisionSurfaceProgrammaticOutput,
13 DuplicationProgrammaticOutput, FeatureFlagsProgrammaticOutput, HealthJsonReportInput,
14 HealthProgrammaticOutput, TraceCloneProgrammaticOutput, TraceDependencyProgrammaticOutput,
15 TraceExportProgrammaticOutput, TraceFileProgrammaticOutput, serialize_health_report_json,
16 },
17};
18use fallow_output::{
19 CHECK_SCHEMA_VERSION, CheckOutput, GroupByMode, RootEnvelopeMode,
20 build_decision_surface_output, serialize_check_json_output,
21 serialize_decision_surface_json_output, serialize_dupes_json_output,
22 serialize_feature_flags_json_output, strip_root_prefix,
23};
24use fallow_types::envelope::{ElapsedMs, SchemaVersion, ToolVersion};
25use serde::Serialize;
26use std::path::Path;
27use std::time::Duration;
28
29type ProgrammaticResult<T> = Result<T, ProgrammaticError>;
30
31pub fn serialize_combined_programmatic_json(
37 output: CombinedProgrammaticOutput,
38) -> ProgrammaticResult<serde_json::Value> {
39 let CombinedProgrammaticOutput {
40 dead_code,
41 duplication,
42 health,
43 root,
44 elapsed,
45 explain,
46 next_steps,
47 envelope_mode,
48 telemetry_analysis_run_id,
49 } = output;
50 crate::serialize_combined_json(crate::CombinedJsonOutputInput {
51 check: dead_code
52 .as_ref()
53 .map(|dead_code| crate::CombinedCheckJsonSection {
54 results: &dead_code.output.results,
55 root: &dead_code.root,
56 elapsed: Duration::from_millis(dead_code.output.elapsed_ms.0),
57 config_fixable: dead_code.config_fixable,
58 extras: crate::CheckJsonExtraOutputs::default(),
59 }),
60 dupes: duplication
61 .as_ref()
62 .map(|duplication| &duplication.output.report),
63 health: health.as_ref().map(|health| &health.report),
64 root: &root,
65 elapsed,
66 explain,
67 next_steps,
68 envelope_mode,
69 telemetry_analysis_run_id: telemetry_analysis_run_id.as_deref(),
70 })
71 .map_err(|err| {
72 ProgrammaticError::new(format!("failed to serialize combined report: {err}"), 2)
73 .with_code("FALLOW_SERIALIZE_COMBINED_REPORT")
74 .with_context("combined")
75 })
76}
77
78pub fn serialize_decision_surface_programmatic_json(
84 output: DecisionSurfaceProgrammaticOutput,
85) -> ProgrammaticResult<serde_json::Value> {
86 let DecisionSurfaceProgrammaticOutput {
87 surface,
88 elapsed: _,
89 envelope_mode,
90 telemetry_analysis_run_id,
91 } = output;
92 let payload = build_decision_surface_output(&surface);
93 serialize_decision_surface_json_output(
94 payload,
95 envelope_mode,
96 telemetry_analysis_run_id.as_deref(),
97 )
98 .map_err(|err| {
99 ProgrammaticError::new(format!("failed to serialize decision surface: {err}"), 2)
100 .with_code("FALLOW_SERIALIZE_DECISION_SURFACE")
101 .with_context("decision-surface")
102 })
103}
104
105pub fn serialize_audit_programmatic_json(
111 output: AuditProgrammaticOutput,
112) -> ProgrammaticResult<serde_json::Value> {
113 let base_snapshot = output.base_snapshot.as_ref();
114 let dead_code = output
115 .dead_code
116 .as_ref()
117 .map(|dead_code| serialize_audit_dead_code(dead_code, base_snapshot))
118 .transpose()?;
119 let duplication = output
120 .duplication
121 .as_ref()
122 .map(|duplication| serialize_audit_duplication(duplication, base_snapshot))
123 .transpose()?;
124 let complexity = output
125 .complexity
126 .as_ref()
127 .map(|complexity| serialize_audit_complexity(complexity, base_snapshot))
128 .transpose()?;
129
130 crate::serialize_audit_json(
131 crate::AuditJsonOutputInput {
132 header: crate::AuditJsonHeaderInput {
133 schema_version: SchemaVersion(CHECK_SCHEMA_VERSION),
134 version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
135 verdict: output.verdict,
136 changed_files_count: u32::try_from(output.changed_files_count).unwrap_or(u32::MAX),
137 base_ref: output.base_ref,
138 base_description: output.base_description,
139 head_sha: output.head_sha,
140 elapsed_ms: ElapsedMs(
141 u64::try_from(output.elapsed.as_millis()).unwrap_or(u64::MAX),
142 ),
143 base_snapshot_skipped: output.base_snapshot_skipped,
144 summary: output.summary,
145 attribution: output.attribution,
146 },
147 dead_code,
148 duplication,
149 complexity,
150 next_steps: output.next_steps,
151 },
152 output.envelope_mode,
153 output.telemetry_analysis_run_id.as_deref(),
154 )
155 .map_err(|err| {
156 ProgrammaticError::new(format!("failed to serialize audit report: {err}"), 2)
157 .with_code("FALLOW_SERIALIZE_AUDIT_REPORT")
158 .with_context("audit")
159 })
160}
161
162fn serialize_audit_dead_code(
163 output: &DeadCodeProgrammaticOutput,
164 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
165) -> ProgrammaticResult<serde_json::Value> {
166 let mut json = crate::serialize_check_json_payload(crate::CheckJsonPayloadInput {
167 results: &output.output.results,
168 root: &output.root,
169 elapsed: Duration::from_millis(output.output.elapsed_ms.0),
170 config_fixable: output.config_fixable,
171 extras: crate::CheckJsonExtraOutputs::default(),
172 workspace_diagnostics: Vec::new(),
173 })
174 .map_err(|err| {
175 ProgrammaticError::new(format!("failed to serialize audit dead-code: {err}"), 2)
176 .with_code("FALLOW_SERIALIZE_AUDIT_DEAD_CODE")
177 .with_context("audit.deadCode")
178 })?;
179 if let Some(base) = base_snapshot {
180 if has_persisted_introduced_flags(&json) {
181 crate::audit_keys::annotate_stale_suppressions_json(
182 &mut json,
183 &output.output.results,
184 &output.root,
185 &base.dead_code,
186 );
187 } else {
188 crate::audit_keys::annotate_dead_code_json(
189 &mut json,
190 &output.output.results,
191 &output.root,
192 &base.dead_code,
193 );
194 }
195 }
196 Ok(json)
197}
198
199fn serialize_audit_duplication(
200 output: &DuplicationProgrammaticOutput,
201 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
202) -> ProgrammaticResult<serde_json::Value> {
203 let mut json = serde_json::to_value(&output.output.report).map_err(|err| {
204 ProgrammaticError::new(format!("failed to serialize audit duplication: {err}"), 2)
205 .with_code("FALLOW_SERIALIZE_AUDIT_DUPLICATION")
206 .with_context("audit.duplication")
207 })?;
208 let root_prefix = format!("{}/", output.root.display());
209 strip_root_prefix(&mut json, &root_prefix);
210 if let Some(base) = base_snapshot
211 && !has_persisted_introduced_flags(&json)
212 {
213 annotate_audit_duplication_json(&mut json, output, &base.dupes);
214 }
215 Ok(json)
216}
217
218fn serialize_audit_complexity(
219 output: &HealthProgrammaticOutput,
220 base_snapshot: Option<&crate::AuditProgrammaticKeySnapshot>,
221) -> ProgrammaticResult<serde_json::Value> {
222 let mut json = serde_json::to_value(&output.report).map_err(|err| {
223 ProgrammaticError::new(format!("failed to serialize audit complexity: {err}"), 2)
224 .with_code("FALLOW_SERIALIZE_AUDIT_COMPLEXITY")
225 .with_context("audit.complexity")
226 })?;
227 let root_prefix = format!("{}/", output.root.display());
228 strip_root_prefix(&mut json, &root_prefix);
229 if let Some(base) = base_snapshot {
230 crate::audit_keys::annotate_health_json(
231 &mut json,
232 &output.report,
233 &output.root,
234 &base.health,
235 );
236 }
237 Ok(json)
238}
239
240fn has_persisted_introduced_flags(json: &serde_json::Value) -> bool {
241 json.as_object().is_some_and(|object| {
242 object.values().any(|value| {
243 value
244 .as_array()
245 .is_some_and(|items| items.iter().any(|item| item.get("introduced").is_some()))
246 })
247 })
248}
249
250fn annotate_audit_duplication_json(
251 json: &mut serde_json::Value,
252 output: &DuplicationProgrammaticOutput,
253 base: &rustc_hash::FxHashSet<String>,
254) {
255 let Some(items) = json
256 .get_mut("clone_groups")
257 .and_then(serde_json::Value::as_array_mut)
258 else {
259 return;
260 };
261 for (item, group) in items.iter_mut().zip(&output.output.report.clone_groups) {
262 if let serde_json::Value::Object(map) = item {
263 let key = crate::audit_keys::dupe_group_key(&group.group, &output.root);
264 map.insert(
265 "introduced".to_string(),
266 serde_json::json!(!base.contains(&key)),
267 );
268 }
269 }
270}
271
272pub fn serialize_dead_code_programmatic_json(
278 output: DeadCodeProgrammaticOutput,
279) -> ProgrammaticResult<serde_json::Value> {
280 let DeadCodeProgrammaticOutput {
281 output,
282 root,
283 config_fixable: _,
284 envelope_mode,
285 telemetry_analysis_run_id,
286 } = output;
287 serialize_check_programmatic_output(
288 output,
289 &root,
290 envelope_mode,
291 telemetry_analysis_run_id.as_deref(),
292 "dead-code",
293 "FALLOW_SERIALIZE_DEAD_CODE_REPORT",
294 )
295}
296
297pub fn serialize_circular_dependencies_programmatic_json(
303 output: CircularDependenciesProgrammaticOutput,
304) -> ProgrammaticResult<serde_json::Value> {
305 let CircularDependenciesProgrammaticOutput {
306 output,
307 root,
308 envelope_mode,
309 telemetry_analysis_run_id,
310 } = output;
311 serialize_check_programmatic_output(
312 output,
313 &root,
314 envelope_mode,
315 telemetry_analysis_run_id.as_deref(),
316 "circular-dependencies",
317 "FALLOW_SERIALIZE_CIRCULAR_DEPENDENCIES_REPORT",
318 )
319}
320
321pub fn serialize_boundary_violations_programmatic_json(
327 output: BoundaryViolationsProgrammaticOutput,
328) -> ProgrammaticResult<serde_json::Value> {
329 let BoundaryViolationsProgrammaticOutput {
330 output,
331 root,
332 envelope_mode,
333 telemetry_analysis_run_id,
334 } = output;
335 serialize_check_programmatic_output(
336 output,
337 &root,
338 envelope_mode,
339 telemetry_analysis_run_id.as_deref(),
340 "boundary-violations",
341 "FALLOW_SERIALIZE_BOUNDARY_VIOLATIONS_REPORT",
342 )
343}
344
345fn serialize_check_programmatic_output(
346 output: CheckOutput,
347 root: &Path,
348 envelope_mode: RootEnvelopeMode,
349 telemetry_analysis_run_id: Option<&str>,
350 context: &'static str,
351 code: &'static str,
352) -> ProgrammaticResult<serde_json::Value> {
353 let mut json = serialize_check_json_output(output, envelope_mode, telemetry_analysis_run_id)
354 .map_err(|err| {
355 ProgrammaticError::new(format!("failed to serialize {context} report: {err}"), 2)
356 .with_code(code)
357 .with_context(context)
358 })?;
359 let root_prefix = format!("{}/", root.display());
360 strip_root_prefix(&mut json, &root_prefix);
361 Ok(json)
362}
363
364pub fn serialize_duplication_programmatic_json(
370 output: DuplicationProgrammaticOutput,
371) -> ProgrammaticResult<serde_json::Value> {
372 let DuplicationProgrammaticOutput {
373 output,
374 root,
375 threshold: _,
376 envelope_mode,
377 telemetry_analysis_run_id,
378 } = output;
379 let mut json =
380 serialize_dupes_json_output(output, envelope_mode, telemetry_analysis_run_id.as_deref())
381 .map_err(|err| {
382 ProgrammaticError::new(format!("failed to serialize duplication report: {err}"), 2)
383 .with_code("FALLOW_SERIALIZE_DUPLICATION_REPORT")
384 .with_context("dupes")
385 })?;
386 let root_prefix = format!("{}/", root.display());
387 strip_root_prefix(&mut json, &root_prefix);
388 Ok(json)
389}
390
391pub fn serialize_feature_flags_programmatic_json(
397 output: FeatureFlagsProgrammaticOutput,
398) -> ProgrammaticResult<serde_json::Value> {
399 serialize_feature_flags_json_output(
400 output.output,
401 output.envelope_mode,
402 output.telemetry_analysis_run_id.as_deref(),
403 )
404 .map_err(|err| {
405 ProgrammaticError::new(
406 format!("failed to serialize feature flags report: {err}"),
407 2,
408 )
409 .with_code("FALLOW_SERIALIZE_FEATURE_FLAGS_REPORT")
410 .with_context("feature-flags")
411 })
412}
413
414pub fn serialize_trace_export_programmatic_json(
420 output: TraceExportProgrammaticOutput,
421) -> ProgrammaticResult<serde_json::Value> {
422 serialize_trace_programmatic_output(
423 output.output,
424 "export trace",
425 "FALLOW_SERIALIZE_TRACE_EXPORT",
426 "trace_export",
427 )
428}
429
430pub fn serialize_trace_file_programmatic_json(
436 output: TraceFileProgrammaticOutput,
437) -> ProgrammaticResult<serde_json::Value> {
438 serialize_trace_programmatic_output(
439 output.output,
440 "file trace",
441 "FALLOW_SERIALIZE_TRACE_FILE",
442 "trace_file",
443 )
444}
445
446pub fn serialize_trace_dependency_programmatic_json(
452 output: TraceDependencyProgrammaticOutput,
453) -> ProgrammaticResult<serde_json::Value> {
454 serialize_trace_programmatic_output(
455 output.output,
456 "dependency trace",
457 "FALLOW_SERIALIZE_TRACE_DEPENDENCY",
458 "trace_dependency",
459 )
460}
461
462pub fn serialize_trace_clone_programmatic_json(
468 output: TraceCloneProgrammaticOutput,
469) -> ProgrammaticResult<serde_json::Value> {
470 serialize_trace_programmatic_output(
471 output.output,
472 "clone trace",
473 "FALLOW_SERIALIZE_TRACE_CLONE",
474 "trace_clone",
475 )
476}
477
478fn serialize_trace_programmatic_output<T: Serialize>(
479 output: T,
480 context: &'static str,
481 code: &'static str,
482 error_context: &'static str,
483) -> ProgrammaticResult<serde_json::Value> {
484 serde_json::to_value(output).map_err(|err| {
485 ProgrammaticError::new(format!("failed to serialize {context}: {err}"), 2)
486 .with_code(code)
487 .with_context(error_context)
488 })
489}
490
491pub fn serialize_health_programmatic_json(
497 output: HealthProgrammaticOutput,
498) -> ProgrammaticResult<serde_json::Value> {
499 let HealthProgrammaticOutput {
500 report,
501 grouping,
502 root,
503 elapsed,
504 explain,
505 workspace_diagnostics,
506 next_steps,
507 envelope_mode,
508 telemetry_analysis_run_id,
509 } = output;
510 let (grouped_by, groups) = grouping.map_or((None, None), |grouping| {
511 (
512 group_by_mode_from_label(grouping.mode),
513 Some(grouping.groups),
514 )
515 });
516 serialize_health_report_json(HealthJsonReportInput {
517 report,
518 root: &root,
519 elapsed,
520 explain,
521 grouped_by,
522 groups,
523 workspace_diagnostics,
524 next_steps,
525 envelope_mode,
526 telemetry_analysis_run_id: telemetry_analysis_run_id.as_deref(),
527 })
528 .map_err(|err| {
529 ProgrammaticError::new(format!("failed to serialize health report: {err}"), 2)
530 .with_code("FALLOW_SERIALIZE_HEALTH_REPORT")
531 .with_context("health")
532 })
533}
534
535fn group_by_mode_from_label(label: &str) -> Option<GroupByMode> {
536 match label {
537 "owner" => Some(GroupByMode::Owner),
538 "directory" => Some(GroupByMode::Directory),
539 "package" => Some(GroupByMode::Package),
540 "section" => Some(GroupByMode::Section),
541 _ => None,
542 }
543}