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, RootEnvelopeMode,
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 envelope_mode: RootEnvelopeMode,
44 pub telemetry_analysis_run_id: Option<&'a str>,
46}
47
48pub struct CheckJsonPayloadInput<'a> {
50 pub results: &'a AnalysisResults,
52 pub root: &'a Path,
54 pub elapsed: Duration,
56 pub config_fixable: bool,
59 pub extras: CheckJsonExtraOutputs,
61 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
63}
64
65#[derive(Debug, Clone, Default)]
70pub struct CheckJsonExtraOutputs {
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}
91
92struct CheckJsonEnvelopeInput<'a> {
93 results: &'a AnalysisResults,
94 elapsed: Duration,
95 config_fixable: bool,
96 meta: Option<Meta>,
97 extras: CheckJsonExtraOutputs,
98 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
99 next_steps: Vec<NextStep>,
100}
101
102pub struct GroupedCheckJsonOutputInput<'a> {
104 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
106 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
111 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
116
117 pub groups: &'a [ResultGroup],
119 pub original: &'a AnalysisResults,
121 pub root: &'a Path,
123 pub elapsed: Duration,
125 pub grouped_by: GroupByMode,
127 pub config_fixable: bool,
130 pub meta: Option<Meta>,
132 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
134 pub next_steps: Vec<NextStep>,
136 pub envelope_mode: RootEnvelopeMode,
138 pub telemetry_analysis_run_id: Option<&'a str>,
140}
141
142pub struct DuplicationJsonOutputInput<'a> {
144 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
146 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
151 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
156
157 pub report: &'a DuplicationReport,
159 pub root: &'a Path,
161 pub elapsed: Duration,
163 pub include_fragments: bool,
165 pub meta: Option<Meta>,
167 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
169 pub next_steps: Vec<NextStep>,
171 pub envelope_mode: RootEnvelopeMode,
173 pub telemetry_analysis_run_id: Option<&'a str>,
175}
176
177pub struct GroupedDuplicationJsonOutputInput<'a> {
179 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
181 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
186 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
191
192 pub report: &'a DuplicationReport,
194 pub grouping: &'a DuplicationGrouping,
196 pub root: &'a Path,
198 pub elapsed: Duration,
200 pub include_fragments: bool,
202 pub meta: Option<Meta>,
204 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
206 pub next_steps: Vec<NextStep>,
208 pub envelope_mode: RootEnvelopeMode,
210 pub telemetry_analysis_run_id: Option<&'a str>,
212}
213
214pub fn serialize_check_json(
220 input: CheckJsonOutputInput<'_>,
221) -> Result<serde_json::Value, serde_json::Error> {
222 let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
223 results: input.results,
224 elapsed: input.elapsed,
225 config_fixable: input.config_fixable,
226 meta: input.meta,
227 extras: input.extras,
228 workspace_diagnostics: input.workspace_diagnostics,
229 next_steps: input.next_steps,
230 });
231 let mut output = fallow_output::serialize_check_json_output(
232 envelope,
233 input.envelope_mode,
234 input.telemetry_analysis_run_id,
235 )?;
236 strip_json_root_prefix(&mut output, input.root);
237 Ok(output)
238}
239
240pub fn serialize_check_json_payload(
246 input: CheckJsonPayloadInput<'_>,
247) -> Result<serde_json::Value, serde_json::Error> {
248 let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
249 results: input.results,
250 elapsed: input.elapsed,
251 config_fixable: input.config_fixable,
252 meta: None,
253 extras: input.extras,
254 workspace_diagnostics: input.workspace_diagnostics,
255 next_steps: Vec::new(),
256 });
257 let mut output = serde_json::to_value(envelope)?;
258 strip_json_root_prefix(&mut output, input.root);
259 Ok(output)
260}
261
262pub fn serialize_grouped_check_json(
268 input: GroupedCheckJsonOutputInput<'_>,
269) -> Result<serde_json::Value, serde_json::Error> {
270 let entries = input
271 .groups
272 .iter()
273 .map(|group| {
274 let mut results = group.results.clone();
275 apply_config_fixable_to_duplicate_exports(&mut results, input.config_fixable);
276 harmonize_typed_suppress_line_actions(&mut results);
277 CheckGroupedEntry {
278 key: group.key.clone(),
279 owners: group.owners.clone(),
280 total_issues: results.total_issues(),
281 results,
282 }
283 })
284 .collect();
285
286 let envelope = CheckGroupedOutput {
287 request_outcomes: input.request_outcomes,
288 schema_version: SchemaVersion(CHECK_SCHEMA_VERSION),
289 version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
290 elapsed_ms: ElapsedMs(input.elapsed.as_millis() as u64),
291 grouped_by: input.grouped_by,
292 total_issues: input.original.total_issues(),
293 groups: entries,
294 baseline_staleness: input.baseline_staleness,
295 gate_outcomes: input.gate_outcomes,
296 meta: input.meta,
297 workspace_diagnostics: input.workspace_diagnostics,
298 next_steps: input.next_steps,
299 };
300
301 let mut output = fallow_output::serialize_check_grouped_json_output(
302 envelope,
303 input.envelope_mode,
304 input.telemetry_analysis_run_id,
305 )?;
306 strip_json_root_prefix(&mut output, input.root);
307 Ok(output)
308}
309
310pub fn serialize_duplication_json(
316 input: DuplicationJsonOutputInput<'_>,
317) -> Result<serde_json::Value, serde_json::Error> {
318 let payload =
319 DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
320 let envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
321 build_dupes_output(DupesOutputInput {
322 gate_outcomes: input.gate_outcomes,
323 request_outcomes: input.request_outcomes,
324 schema_version: DUPES_SCHEMA_VERSION,
325 version: env!("CARGO_PKG_VERSION").to_string(),
326 elapsed: input.elapsed,
327 report: payload,
328 clone_groups_shown: input.report.clone_groups_shown(),
329 clone_groups_omitted: input.report.clone_groups_omitted(),
330 clone_families_shown: input.report.clone_families_shown(),
331 clone_families_omitted: input.report.clone_families_omitted(),
332 grouped_by: None,
333 total_issues: None,
334 groups: None,
335 baseline_staleness: input.baseline_staleness,
336 meta: input.meta,
337 workspace_diagnostics: input.workspace_diagnostics,
338 next_steps: input.next_steps,
339 });
340 let mut output = fallow_output::serialize_dupes_json_output(
341 envelope,
342 input.envelope_mode,
343 input.telemetry_analysis_run_id,
344 )?;
345 let root_prefix = format!("{}/", input.root.display());
346 strip_root_prefix(&mut output, &root_prefix);
347 Ok(output)
348}
349
350pub fn serialize_grouped_duplication_json(
356 input: GroupedDuplicationJsonOutputInput<'_>,
357) -> Result<serde_json::Value, serde_json::Error> {
358 let root_prefix = format!("{}/", input.root.display());
359 let payload =
360 DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
361 let envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
362 build_dupes_output(DupesOutputInput {
363 gate_outcomes: input.gate_outcomes,
364 request_outcomes: input.request_outcomes,
365 schema_version: DUPES_SCHEMA_VERSION,
366 version: env!("CARGO_PKG_VERSION").to_string(),
367 elapsed: input.elapsed,
368 report: payload,
369 clone_groups_shown: input.report.clone_groups_shown(),
370 clone_groups_omitted: input.report.clone_groups_omitted(),
371 clone_families_shown: input.report.clone_families_shown(),
372 clone_families_omitted: input.report.clone_families_omitted(),
373 grouped_by: Some(group_by_mode_from_label(input.grouping.mode)),
374 total_issues: Some(input.report.clone_groups.len()),
375 groups: None,
376 baseline_staleness: input.baseline_staleness,
377 meta: input.meta,
378 workspace_diagnostics: input.workspace_diagnostics,
379 next_steps: input.next_steps,
380 });
381 let mut output = fallow_output::serialize_dupes_json_output(
382 envelope,
383 input.envelope_mode,
384 input.telemetry_analysis_run_id,
385 )?;
386 strip_root_prefix(&mut output, &root_prefix);
387
388 let group_values = input
389 .grouping
390 .groups
391 .iter()
392 .map(|group| {
393 let mut value = if input.include_fragments {
394 serde_json::to_value(group)?
395 } else {
396 let mut stripped = group.clone();
397 stripped.strip_fragments();
398 serde_json::to_value(&stripped)?
399 };
400 strip_root_prefix(&mut value, &root_prefix);
401 Ok(value)
402 })
403 .collect::<Result<Vec<_>, serde_json::Error>>()?;
404
405 if let serde_json::Value::Object(ref mut map) = output {
406 map.insert("groups".to_string(), serde_json::Value::Array(group_values));
407 }
408
409 Ok(output)
410}
411
412fn build_check_json_envelope(input: CheckJsonEnvelopeInput<'_>) -> CheckOutput {
413 let mut output = build_check_output(CheckOutputInput {
414 schema_version: CHECK_SCHEMA_VERSION,
415 version: env!("CARGO_PKG_VERSION").to_string(),
416 elapsed: input.elapsed,
417 results: input.results.clone(),
418 config_fixable: input.config_fixable,
419 meta: input.meta,
420 workspace_diagnostics: input.workspace_diagnostics,
421 next_steps: input.next_steps,
422 });
423 output.baseline_deltas = input.extras.baseline_deltas;
424 output.baseline = input.extras.baseline;
425 output.baseline_staleness = input.extras.baseline_staleness;
426 output.regression = input.extras.regression;
427 output.gate_outcomes = input.extras.gate_outcomes;
428 output.request_outcomes = input.extras.request_outcomes;
429 output
430}
431
432fn strip_json_root_prefix(output: &mut serde_json::Value, root: &Path) {
433 let root_prefix = format!("{}/", root.display());
434 strip_root_prefix(output, &root_prefix);
435}
436
437fn group_by_mode_from_label(label: &str) -> GroupByMode {
438 match label {
439 "directory" => GroupByMode::Directory,
440 "package" => GroupByMode::Package,
441 "section" => GroupByMode::Section,
442 _ => GroupByMode::Owner,
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449 use fallow_types::workspace::WorkspaceDiagnosticKind;
450
451 #[test]
452 fn grouped_check_json_carries_workspace_diagnostics_with_relative_paths() {
453 let root = Path::new("/project");
454 let output = serialize_grouped_check_json(GroupedCheckJsonOutputInput {
455 gate_outcomes: None,
456 request_outcomes: None,
457 baseline_staleness: None,
458 groups: &[],
459 original: &AnalysisResults::default(),
460 root,
461 elapsed: Duration::ZERO,
462 grouped_by: GroupByMode::Directory,
463 config_fixable: false,
464 meta: None,
465 workspace_diagnostics: vec![WorkspaceDiagnostic::new(
466 root,
467 root.join("src/unreadable.ts"),
468 WorkspaceDiagnosticKind::SourceReadFailure {
469 error: "permission denied".to_string(),
470 },
471 )],
472 next_steps: Vec::new(),
473 envelope_mode: RootEnvelopeMode::Tagged,
474 telemetry_analysis_run_id: None,
475 })
476 .expect("grouped check JSON serializes");
477
478 assert_eq!(
479 output["workspace_diagnostics"][0]["path"],
480 "src/unreadable.ts"
481 );
482 assert_eq!(
483 output["workspace_diagnostics"][0]["kind"],
484 "source-read-failure"
485 );
486 }
487}