1use std::path::Path;
4use std::time::Duration;
5
6use fallow_output::{
7 CHECK_SCHEMA_VERSION, CheckEnvelope, CheckGroupedEntry, CheckGroupedOutput, CheckOutput,
8 CheckOutputInput, DUPES_SCHEMA_VERSION, DupesOutput, DupesOutputInput, GroupByMode,
9 apply_config_fixable_to_duplicate_exports, build_check_output, build_dupes_output,
10 harmonize_multi_kind_suppress_line_actions as harmonize_typed_suppress_line_actions,
11 strip_root_prefix,
12};
13use fallow_types::duplicates::DuplicationReport;
14use fallow_types::envelope::{
15 BaselineDeltas, BaselineMatch, ElapsedMs, Meta, RegressionResult, SchemaVersion, ToolVersion,
16};
17use fallow_types::output::NextStep;
18use fallow_types::results::AnalysisResults;
19use fallow_types::workspace::WorkspaceDiagnostic;
20
21use crate::{DupesReportPayload, DuplicationGroup, DuplicationGrouping, ResultGroup};
22
23pub struct CheckJsonOutputInput<'a> {
26 pub envelope: CheckEnvelope,
28 pub results: &'a AnalysisResults,
30 pub root: &'a Path,
32 pub elapsed: Duration,
34 pub config_fixable: bool,
37 pub meta: Option<Meta>,
39 pub extras: CheckJsonExtraOutputs,
41 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
43 pub next_steps: Vec<NextStep>,
45 pub telemetry_analysis_run_id: Option<&'a str>,
47}
48
49pub struct CheckJsonPayloadInput<'a> {
51 pub results: &'a AnalysisResults,
53 pub root: &'a Path,
55 pub elapsed: Duration,
57 pub config_fixable: bool,
60 pub extras: CheckJsonExtraOutputs,
62 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
64}
65
66#[derive(Debug, Clone, Default)]
71pub struct CheckJsonExtraOutputs {
72 pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
74 pub baseline_deltas: Option<BaselineDeltas>,
76 pub baseline: Option<BaselineMatch>,
78 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
81 pub regression: Option<RegressionResult>,
83 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
88 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
93 pub finding_id_query: Option<fallow_output::FindingIdQuery>,
96}
97
98struct CheckJsonEnvelopeInput<'a> {
99 schema_version: u32,
100 results: &'a AnalysisResults,
101 elapsed: Duration,
102 config_fixable: bool,
103 meta: Option<Meta>,
104 extras: CheckJsonExtraOutputs,
105 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
106 next_steps: Vec<NextStep>,
107}
108
109pub struct GroupedCheckJsonOutputInput<'a> {
111 pub envelope: CheckEnvelope,
113 pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
115 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
117 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
122 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
127 pub finding_id_query: Option<fallow_output::FindingIdQuery>,
130
131 pub groups: &'a [ResultGroup],
133 pub original: &'a AnalysisResults,
135 pub root: &'a Path,
137 pub elapsed: Duration,
139 pub grouped_by: GroupByMode,
141 pub config_fixable: bool,
144 pub meta: Option<Meta>,
146 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
148 pub next_steps: Vec<NextStep>,
150 pub telemetry_analysis_run_id: Option<&'a str>,
152}
153
154pub struct DuplicationJsonOutputInput<'a> {
156 pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
158 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
160 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
165 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
170
171 pub report: &'a DuplicationReport,
173 pub root: &'a Path,
175 pub elapsed: Duration,
177 pub include_fragments: bool,
179 pub meta: Option<Meta>,
181 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
183 pub next_steps: Vec<NextStep>,
185 pub telemetry_analysis_run_id: Option<&'a str>,
187}
188
189pub struct GroupedDuplicationJsonOutputInput<'a> {
191 pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
193 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
195 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
200 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
205
206 pub report: &'a DuplicationReport,
208 pub grouping: &'a DuplicationGrouping,
210 pub root: &'a Path,
212 pub elapsed: Duration,
214 pub include_fragments: bool,
216 pub meta: Option<Meta>,
218 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
220 pub next_steps: Vec<NextStep>,
222 pub telemetry_analysis_run_id: Option<&'a str>,
224}
225
226pub fn serialize_check_json(
232 input: CheckJsonOutputInput<'_>,
233) -> Result<serde_json::Value, serde_json::Error> {
234 let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
235 schema_version: input.envelope.schema_version(),
236 results: input.results,
237 elapsed: input.elapsed,
238 config_fixable: input.config_fixable,
239 meta: input.meta,
240 extras: input.extras,
241 workspace_diagnostics: input.workspace_diagnostics,
242 next_steps: input.next_steps,
243 });
244 let run_id = input.telemetry_analysis_run_id;
245 let mut output = match input.envelope {
246 CheckEnvelope::DeadCode => fallow_output::serialize_check_json_output(envelope, run_id)?,
247 CheckEnvelope::Architecture => {
248 fallow_output::serialize_architecture_json_output(envelope, run_id)?
249 }
250 };
251 strip_json_root_prefix(&mut output, input.root);
252 Ok(output)
253}
254
255pub fn serialize_check_json_payload(
261 input: CheckJsonPayloadInput<'_>,
262) -> Result<serde_json::Value, serde_json::Error> {
263 let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
264 schema_version: CHECK_SCHEMA_VERSION,
265 results: input.results,
266 elapsed: input.elapsed,
267 config_fixable: input.config_fixable,
268 meta: None,
269 extras: input.extras,
270 workspace_diagnostics: input.workspace_diagnostics,
271 next_steps: Vec::new(),
272 });
273 let mut output = serde_json::to_value(envelope)?;
274 strip_json_root_prefix(&mut output, input.root);
275 Ok(output)
276}
277
278pub fn serialize_grouped_check_json(
284 input: GroupedCheckJsonOutputInput<'_>,
285) -> Result<serde_json::Value, serde_json::Error> {
286 let entries = input
287 .groups
288 .iter()
289 .map(|group| {
290 let mut results = group.results.clone();
291 apply_config_fixable_to_duplicate_exports(&mut results, input.config_fixable);
292 harmonize_typed_suppress_line_actions(&mut results);
293 CheckGroupedEntry {
294 key: group.key.clone(),
295 owners: group.owners.clone(),
296 total_issues: results.total_issues(),
297 results,
298 }
299 })
300 .collect();
301
302 let envelope = CheckGroupedOutput {
303 package_baselines: input.package_baselines,
304 request_outcomes: input.request_outcomes,
305 schema_version: SchemaVersion(input.envelope.schema_version()),
306 version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
307 elapsed_ms: ElapsedMs(input.elapsed.as_millis() as u64),
308 grouped_by: input.grouped_by,
309 total_issues: input.original.total_issues(),
310 groups: entries,
311 unused_load_data_keys_global_abstain: input.original.unused_load_data_keys_global_abstain,
312 cascade_hidden: input.original.cascade_hidden,
313 baseline_staleness: input.baseline_staleness,
314 finding_id_query: input.finding_id_query,
315 gate_outcomes: input.gate_outcomes,
316 meta: input.meta,
317 workspace_diagnostics: input.workspace_diagnostics,
318 next_steps: input.next_steps,
319 };
320
321 let run_id = input.telemetry_analysis_run_id;
322 let mut output = match input.envelope {
323 CheckEnvelope::DeadCode => {
324 fallow_output::serialize_check_grouped_json_output(envelope, run_id)?
325 }
326 CheckEnvelope::Architecture => {
327 fallow_output::serialize_architecture_grouped_json_output(envelope, run_id)?
328 }
329 };
330 strip_json_root_prefix(&mut output, input.root);
331 Ok(output)
332}
333
334pub fn serialize_duplication_json(
340 input: DuplicationJsonOutputInput<'_>,
341) -> Result<serde_json::Value, serde_json::Error> {
342 let payload =
343 DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
344 let mut envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
345 build_dupes_output(DupesOutputInput {
346 gate_outcomes: input.gate_outcomes,
347 request_outcomes: input.request_outcomes,
348 schema_version: DUPES_SCHEMA_VERSION,
349 version: env!("CARGO_PKG_VERSION").to_string(),
350 elapsed: input.elapsed,
351 report: payload,
352 clone_groups_shown: input.report.clone_groups_shown(),
353 clone_groups_omitted: input.report.clone_groups_omitted(),
354 clone_families_shown: input.report.clone_families_shown(),
355 clone_families_omitted: input.report.clone_families_omitted(),
356 grouped_by: None,
357 total_issues: None,
358 groups: None,
359 baseline_staleness: input.baseline_staleness,
360 meta: input.meta,
361 workspace_diagnostics: input.workspace_diagnostics,
362 next_steps: input.next_steps,
363 });
364 envelope.package_baselines = input.package_baselines;
365 let mut output =
366 fallow_output::serialize_dupes_json_output(envelope, input.telemetry_analysis_run_id)?;
367 let root_prefix = format!("{}/", input.root.display());
368 strip_root_prefix(&mut output, &root_prefix);
369 Ok(output)
370}
371
372pub fn serialize_grouped_duplication_json(
378 input: GroupedDuplicationJsonOutputInput<'_>,
379) -> Result<serde_json::Value, serde_json::Error> {
380 let root_prefix = format!("{}/", input.root.display());
381 let payload =
382 DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
383 let mut envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
384 build_dupes_output(DupesOutputInput {
385 gate_outcomes: input.gate_outcomes,
386 request_outcomes: input.request_outcomes,
387 schema_version: DUPES_SCHEMA_VERSION,
388 version: env!("CARGO_PKG_VERSION").to_string(),
389 elapsed: input.elapsed,
390 report: payload,
391 clone_groups_shown: input.report.clone_groups_shown(),
392 clone_groups_omitted: input.report.clone_groups_omitted(),
393 clone_families_shown: input.report.clone_families_shown(),
394 clone_families_omitted: input.report.clone_families_omitted(),
395 grouped_by: Some(group_by_mode_from_label(input.grouping.mode)),
396 total_issues: Some(input.report.clone_groups.len()),
397 groups: None,
398 baseline_staleness: input.baseline_staleness,
399 meta: input.meta,
400 workspace_diagnostics: input.workspace_diagnostics,
401 next_steps: input.next_steps,
402 });
403 envelope.package_baselines = input.package_baselines;
404 let mut output =
405 fallow_output::serialize_dupes_json_output(envelope, input.telemetry_analysis_run_id)?;
406 strip_root_prefix(&mut output, &root_prefix);
407
408 let group_values = input
409 .grouping
410 .groups
411 .iter()
412 .map(|group| {
413 let mut value = if input.include_fragments {
414 serde_json::to_value(group)?
415 } else {
416 let mut stripped = group.clone();
417 stripped.strip_fragments();
418 serde_json::to_value(&stripped)?
419 };
420 strip_root_prefix(&mut value, &root_prefix);
421 Ok(value)
422 })
423 .collect::<Result<Vec<_>, serde_json::Error>>()?;
424
425 if let serde_json::Value::Object(ref mut map) = output {
426 map.insert("groups".to_string(), serde_json::Value::Array(group_values));
427 }
428
429 Ok(output)
430}
431
432fn build_check_json_envelope(input: CheckJsonEnvelopeInput<'_>) -> CheckOutput {
433 let mut output = build_check_output(CheckOutputInput {
434 schema_version: input.schema_version,
435 version: env!("CARGO_PKG_VERSION").to_string(),
436 elapsed: input.elapsed,
437 results: input.results.clone(),
438 config_fixable: input.config_fixable,
439 meta: input.meta,
440 workspace_diagnostics: input.workspace_diagnostics,
441 next_steps: input.next_steps,
442 });
443 output.baseline_deltas = input.extras.baseline_deltas;
444 output.baseline = input.extras.baseline;
445 output.baseline_staleness = input.extras.baseline_staleness;
446 output.regression = input.extras.regression;
447 output.gate_outcomes = input.extras.gate_outcomes;
448 output.request_outcomes = input.extras.request_outcomes;
449 output.package_baselines = input.extras.package_baselines;
450 output.finding_id_query = input.extras.finding_id_query;
451 output
452}
453
454fn strip_json_root_prefix(output: &mut serde_json::Value, root: &Path) {
455 let root_prefix = format!("{}/", root.display());
456 strip_root_prefix(output, &root_prefix);
457}
458
459fn group_by_mode_from_label(label: &str) -> GroupByMode {
460 match label {
461 "directory" => GroupByMode::Directory,
462 "package" => GroupByMode::Package,
463 "section" => GroupByMode::Section,
464 _ => GroupByMode::Owner,
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471 use fallow_types::workspace::WorkspaceDiagnosticKind;
472
473 #[test]
474 fn grouped_check_json_carries_workspace_diagnostics_with_relative_paths() {
475 let root = Path::new("/project");
476 let output = serialize_grouped_check_json(GroupedCheckJsonOutputInput {
477 envelope: CheckEnvelope::DeadCode,
478 package_baselines: Vec::new(),
479 gate_outcomes: None,
480 request_outcomes: None,
481 finding_id_query: None,
482 baseline_staleness: None,
483 groups: &[],
484 original: &AnalysisResults::default(),
485 root,
486 elapsed: Duration::ZERO,
487 grouped_by: GroupByMode::Directory,
488 config_fixable: false,
489 meta: None,
490 workspace_diagnostics: vec![WorkspaceDiagnostic::new(
491 root,
492 root.join("src/unreadable.ts"),
493 WorkspaceDiagnosticKind::SourceReadFailure {
494 error: "permission denied".to_string(),
495 },
496 )],
497 next_steps: Vec::new(),
498 telemetry_analysis_run_id: None,
499 })
500 .expect("grouped check JSON serializes");
501
502 assert_eq!(
503 output["workspace_diagnostics"][0]["path"],
504 "src/unreadable.ts"
505 );
506 assert_eq!(
507 output["workspace_diagnostics"][0]["kind"],
508 "source-read-failure"
509 );
510 assert_eq!(output["kind"], "dead-code-grouped");
511 assert_eq!(output["schema_version"], CHECK_SCHEMA_VERSION);
512 }
513
514 #[test]
515 fn architecture_envelope_sets_kind_and_schema_version() {
516 let root = Path::new("/project");
517 let results = AnalysisResults::default();
518 let flat = serialize_check_json(CheckJsonOutputInput {
519 envelope: CheckEnvelope::Architecture,
520 results: &results,
521 root,
522 elapsed: Duration::ZERO,
523 config_fixable: false,
524 meta: None,
525 extras: CheckJsonExtraOutputs::default(),
526 workspace_diagnostics: Vec::new(),
527 next_steps: Vec::new(),
528 telemetry_analysis_run_id: None,
529 })
530 .expect("architecture JSON serializes");
531 assert_eq!(flat["kind"], "architecture");
532 assert_eq!(
533 flat["schema_version"],
534 fallow_output::ARCHITECTURE_SCHEMA_VERSION
535 );
536
537 let grouped = serialize_grouped_check_json(GroupedCheckJsonOutputInput {
538 envelope: CheckEnvelope::Architecture,
539 package_baselines: Vec::new(),
540 gate_outcomes: None,
541 request_outcomes: None,
542 finding_id_query: None,
543 baseline_staleness: None,
544 groups: &[],
545 original: &results,
546 root,
547 elapsed: Duration::ZERO,
548 grouped_by: GroupByMode::Directory,
549 config_fixable: false,
550 meta: None,
551 workspace_diagnostics: Vec::new(),
552 next_steps: Vec::new(),
553 telemetry_analysis_run_id: None,
554 })
555 .expect("grouped architecture JSON serializes");
556 assert_eq!(grouped["kind"], "architecture-grouped");
557 assert_eq!(
558 grouped["schema_version"],
559 fallow_output::ARCHITECTURE_SCHEMA_VERSION
560 );
561
562 let payload = serialize_check_json_payload(CheckJsonPayloadInput {
563 results: &results,
564 root,
565 elapsed: Duration::ZERO,
566 config_fixable: false,
567 extras: CheckJsonExtraOutputs::default(),
568 workspace_diagnostics: Vec::new(),
569 })
570 .expect("embedded payload serializes");
571 assert_eq!(payload["schema_version"], CHECK_SCHEMA_VERSION);
572 assert!(payload.get("kind").is_none());
573 }
574}