1use std::path::Path;
4use std::time::Duration;
5
6use fallow_output::{
7 CHECK_SCHEMA_VERSION, CheckGroupedEntry, CheckGroupedOutput, CheckOutput, CheckOutputInput,
8 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> {
25 pub results: &'a AnalysisResults,
27 pub root: &'a Path,
29 pub elapsed: Duration,
31 pub config_fixable: bool,
34 pub meta: Option<Meta>,
36 pub extras: CheckJsonExtraOutputs,
38 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
40 pub next_steps: Vec<NextStep>,
42 pub telemetry_analysis_run_id: Option<&'a str>,
44}
45
46pub struct CheckJsonPayloadInput<'a> {
48 pub results: &'a AnalysisResults,
50 pub root: &'a Path,
52 pub elapsed: Duration,
54 pub config_fixable: bool,
57 pub extras: CheckJsonExtraOutputs,
59 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
61}
62
63#[derive(Debug, Clone, Default)]
68pub struct CheckJsonExtraOutputs {
69 pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
71 pub baseline_deltas: Option<BaselineDeltas>,
73 pub baseline: Option<BaselineMatch>,
75 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
78 pub regression: Option<RegressionResult>,
80 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
85 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
90 pub finding_id_query: Option<fallow_output::FindingIdQuery>,
93}
94
95struct CheckJsonEnvelopeInput<'a> {
96 results: &'a AnalysisResults,
97 elapsed: Duration,
98 config_fixable: bool,
99 meta: Option<Meta>,
100 extras: CheckJsonExtraOutputs,
101 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
102 next_steps: Vec<NextStep>,
103}
104
105pub struct GroupedCheckJsonOutputInput<'a> {
107 pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
109 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
111 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
116 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
121 pub finding_id_query: Option<fallow_output::FindingIdQuery>,
124
125 pub groups: &'a [ResultGroup],
127 pub original: &'a AnalysisResults,
129 pub root: &'a Path,
131 pub elapsed: Duration,
133 pub grouped_by: GroupByMode,
135 pub config_fixable: bool,
138 pub meta: Option<Meta>,
140 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
142 pub next_steps: Vec<NextStep>,
144 pub telemetry_analysis_run_id: Option<&'a str>,
146}
147
148pub struct DuplicationJsonOutputInput<'a> {
150 pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
152 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
154 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
159 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
164
165 pub report: &'a DuplicationReport,
167 pub root: &'a Path,
169 pub elapsed: Duration,
171 pub include_fragments: bool,
173 pub meta: Option<Meta>,
175 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
177 pub next_steps: Vec<NextStep>,
179 pub telemetry_analysis_run_id: Option<&'a str>,
181}
182
183pub struct GroupedDuplicationJsonOutputInput<'a> {
185 pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
187 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
189 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
194 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
199
200 pub report: &'a DuplicationReport,
202 pub grouping: &'a DuplicationGrouping,
204 pub root: &'a Path,
206 pub elapsed: Duration,
208 pub include_fragments: bool,
210 pub meta: Option<Meta>,
212 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
214 pub next_steps: Vec<NextStep>,
216 pub telemetry_analysis_run_id: Option<&'a str>,
218}
219
220pub fn serialize_check_json(
226 input: CheckJsonOutputInput<'_>,
227) -> Result<serde_json::Value, serde_json::Error> {
228 let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
229 results: input.results,
230 elapsed: input.elapsed,
231 config_fixable: input.config_fixable,
232 meta: input.meta,
233 extras: input.extras,
234 workspace_diagnostics: input.workspace_diagnostics,
235 next_steps: input.next_steps,
236 });
237 let mut output =
238 fallow_output::serialize_check_json_output(envelope, input.telemetry_analysis_run_id)?;
239 strip_json_root_prefix(&mut output, input.root);
240 Ok(output)
241}
242
243pub fn serialize_check_json_payload(
249 input: CheckJsonPayloadInput<'_>,
250) -> Result<serde_json::Value, serde_json::Error> {
251 let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
252 results: input.results,
253 elapsed: input.elapsed,
254 config_fixable: input.config_fixable,
255 meta: None,
256 extras: input.extras,
257 workspace_diagnostics: input.workspace_diagnostics,
258 next_steps: Vec::new(),
259 });
260 let mut output = serde_json::to_value(envelope)?;
261 strip_json_root_prefix(&mut output, input.root);
262 Ok(output)
263}
264
265pub fn serialize_grouped_check_json(
271 input: GroupedCheckJsonOutputInput<'_>,
272) -> Result<serde_json::Value, serde_json::Error> {
273 let entries = input
274 .groups
275 .iter()
276 .map(|group| {
277 let mut results = group.results.clone();
278 apply_config_fixable_to_duplicate_exports(&mut results, input.config_fixable);
279 harmonize_typed_suppress_line_actions(&mut results);
280 CheckGroupedEntry {
281 key: group.key.clone(),
282 owners: group.owners.clone(),
283 total_issues: results.total_issues(),
284 results,
285 }
286 })
287 .collect();
288
289 let envelope = CheckGroupedOutput {
290 package_baselines: input.package_baselines,
291 request_outcomes: input.request_outcomes,
292 schema_version: SchemaVersion(CHECK_SCHEMA_VERSION),
293 version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
294 elapsed_ms: ElapsedMs(input.elapsed.as_millis() as u64),
295 grouped_by: input.grouped_by,
296 total_issues: input.original.total_issues(),
297 groups: entries,
298 unused_load_data_keys_global_abstain: input.original.unused_load_data_keys_global_abstain,
299 baseline_staleness: input.baseline_staleness,
300 finding_id_query: input.finding_id_query,
301 gate_outcomes: input.gate_outcomes,
302 meta: input.meta,
303 workspace_diagnostics: input.workspace_diagnostics,
304 next_steps: input.next_steps,
305 };
306
307 let mut output = fallow_output::serialize_check_grouped_json_output(
308 envelope,
309 input.telemetry_analysis_run_id,
310 )?;
311 strip_json_root_prefix(&mut output, input.root);
312 Ok(output)
313}
314
315pub fn serialize_duplication_json(
321 input: DuplicationJsonOutputInput<'_>,
322) -> Result<serde_json::Value, serde_json::Error> {
323 let payload =
324 DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
325 let mut envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
326 build_dupes_output(DupesOutputInput {
327 gate_outcomes: input.gate_outcomes,
328 request_outcomes: input.request_outcomes,
329 schema_version: DUPES_SCHEMA_VERSION,
330 version: env!("CARGO_PKG_VERSION").to_string(),
331 elapsed: input.elapsed,
332 report: payload,
333 clone_groups_shown: input.report.clone_groups_shown(),
334 clone_groups_omitted: input.report.clone_groups_omitted(),
335 clone_families_shown: input.report.clone_families_shown(),
336 clone_families_omitted: input.report.clone_families_omitted(),
337 grouped_by: None,
338 total_issues: None,
339 groups: None,
340 baseline_staleness: input.baseline_staleness,
341 meta: input.meta,
342 workspace_diagnostics: input.workspace_diagnostics,
343 next_steps: input.next_steps,
344 });
345 envelope.package_baselines = input.package_baselines;
346 let mut output =
347 fallow_output::serialize_dupes_json_output(envelope, input.telemetry_analysis_run_id)?;
348 let root_prefix = format!("{}/", input.root.display());
349 strip_root_prefix(&mut output, &root_prefix);
350 Ok(output)
351}
352
353pub fn serialize_grouped_duplication_json(
359 input: GroupedDuplicationJsonOutputInput<'_>,
360) -> Result<serde_json::Value, serde_json::Error> {
361 let root_prefix = format!("{}/", input.root.display());
362 let payload =
363 DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
364 let mut envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
365 build_dupes_output(DupesOutputInput {
366 gate_outcomes: input.gate_outcomes,
367 request_outcomes: input.request_outcomes,
368 schema_version: DUPES_SCHEMA_VERSION,
369 version: env!("CARGO_PKG_VERSION").to_string(),
370 elapsed: input.elapsed,
371 report: payload,
372 clone_groups_shown: input.report.clone_groups_shown(),
373 clone_groups_omitted: input.report.clone_groups_omitted(),
374 clone_families_shown: input.report.clone_families_shown(),
375 clone_families_omitted: input.report.clone_families_omitted(),
376 grouped_by: Some(group_by_mode_from_label(input.grouping.mode)),
377 total_issues: Some(input.report.clone_groups.len()),
378 groups: None,
379 baseline_staleness: input.baseline_staleness,
380 meta: input.meta,
381 workspace_diagnostics: input.workspace_diagnostics,
382 next_steps: input.next_steps,
383 });
384 envelope.package_baselines = input.package_baselines;
385 let mut output =
386 fallow_output::serialize_dupes_json_output(envelope, input.telemetry_analysis_run_id)?;
387 strip_root_prefix(&mut output, &root_prefix);
388
389 let group_values = input
390 .grouping
391 .groups
392 .iter()
393 .map(|group| {
394 let mut value = if input.include_fragments {
395 serde_json::to_value(group)?
396 } else {
397 let mut stripped = group.clone();
398 stripped.strip_fragments();
399 serde_json::to_value(&stripped)?
400 };
401 strip_root_prefix(&mut value, &root_prefix);
402 Ok(value)
403 })
404 .collect::<Result<Vec<_>, serde_json::Error>>()?;
405
406 if let serde_json::Value::Object(ref mut map) = output {
407 map.insert("groups".to_string(), serde_json::Value::Array(group_values));
408 }
409
410 Ok(output)
411}
412
413fn build_check_json_envelope(input: CheckJsonEnvelopeInput<'_>) -> CheckOutput {
414 let mut output = build_check_output(CheckOutputInput {
415 schema_version: CHECK_SCHEMA_VERSION,
416 version: env!("CARGO_PKG_VERSION").to_string(),
417 elapsed: input.elapsed,
418 results: input.results.clone(),
419 config_fixable: input.config_fixable,
420 meta: input.meta,
421 workspace_diagnostics: input.workspace_diagnostics,
422 next_steps: input.next_steps,
423 });
424 output.baseline_deltas = input.extras.baseline_deltas;
425 output.baseline = input.extras.baseline;
426 output.baseline_staleness = input.extras.baseline_staleness;
427 output.regression = input.extras.regression;
428 output.gate_outcomes = input.extras.gate_outcomes;
429 output.request_outcomes = input.extras.request_outcomes;
430 output.package_baselines = input.extras.package_baselines;
431 output.finding_id_query = input.extras.finding_id_query;
432 output
433}
434
435fn strip_json_root_prefix(output: &mut serde_json::Value, root: &Path) {
436 let root_prefix = format!("{}/", root.display());
437 strip_root_prefix(output, &root_prefix);
438}
439
440fn group_by_mode_from_label(label: &str) -> GroupByMode {
441 match label {
442 "directory" => GroupByMode::Directory,
443 "package" => GroupByMode::Package,
444 "section" => GroupByMode::Section,
445 _ => GroupByMode::Owner,
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use fallow_types::workspace::WorkspaceDiagnosticKind;
453
454 #[test]
455 fn grouped_check_json_carries_workspace_diagnostics_with_relative_paths() {
456 let root = Path::new("/project");
457 let output = serialize_grouped_check_json(GroupedCheckJsonOutputInput {
458 package_baselines: Vec::new(),
459 gate_outcomes: None,
460 request_outcomes: None,
461 finding_id_query: None,
462 baseline_staleness: None,
463 groups: &[],
464 original: &AnalysisResults::default(),
465 root,
466 elapsed: Duration::ZERO,
467 grouped_by: GroupByMode::Directory,
468 config_fixable: false,
469 meta: None,
470 workspace_diagnostics: vec![WorkspaceDiagnostic::new(
471 root,
472 root.join("src/unreadable.ts"),
473 WorkspaceDiagnosticKind::SourceReadFailure {
474 error: "permission denied".to_string(),
475 },
476 )],
477 next_steps: Vec::new(),
478 telemetry_analysis_run_id: None,
479 })
480 .expect("grouped check JSON serializes");
481
482 assert_eq!(
483 output["workspace_diagnostics"][0]["path"],
484 "src/unreadable.ts"
485 );
486 assert_eq!(
487 output["workspace_diagnostics"][0]["kind"],
488 "source-read-failure"
489 );
490 }
491}