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}
86
87struct CheckJsonEnvelopeInput<'a> {
88 results: &'a AnalysisResults,
89 elapsed: Duration,
90 config_fixable: bool,
91 meta: Option<Meta>,
92 extras: CheckJsonExtraOutputs,
93 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
94 next_steps: Vec<NextStep>,
95}
96
97pub struct GroupedCheckJsonOutputInput<'a> {
99 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
101 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
106
107 pub groups: &'a [ResultGroup],
109 pub original: &'a AnalysisResults,
111 pub root: &'a Path,
113 pub elapsed: Duration,
115 pub grouped_by: GroupByMode,
117 pub config_fixable: bool,
120 pub meta: Option<Meta>,
122 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
124 pub next_steps: Vec<NextStep>,
126 pub envelope_mode: RootEnvelopeMode,
128 pub telemetry_analysis_run_id: Option<&'a str>,
130}
131
132pub struct DuplicationJsonOutputInput<'a> {
134 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
136 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
141
142 pub report: &'a DuplicationReport,
144 pub root: &'a Path,
146 pub elapsed: Duration,
148 pub include_fragments: bool,
150 pub meta: Option<Meta>,
152 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
154 pub next_steps: Vec<NextStep>,
156 pub envelope_mode: RootEnvelopeMode,
158 pub telemetry_analysis_run_id: Option<&'a str>,
160}
161
162pub struct GroupedDuplicationJsonOutputInput<'a> {
164 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
166 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
171
172 pub report: &'a DuplicationReport,
174 pub grouping: &'a DuplicationGrouping,
176 pub root: &'a Path,
178 pub elapsed: Duration,
180 pub include_fragments: bool,
182 pub meta: Option<Meta>,
184 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
186 pub next_steps: Vec<NextStep>,
188 pub envelope_mode: RootEnvelopeMode,
190 pub telemetry_analysis_run_id: Option<&'a str>,
192}
193
194pub fn serialize_check_json(
200 input: CheckJsonOutputInput<'_>,
201) -> Result<serde_json::Value, serde_json::Error> {
202 let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
203 results: input.results,
204 elapsed: input.elapsed,
205 config_fixable: input.config_fixable,
206 meta: input.meta,
207 extras: input.extras,
208 workspace_diagnostics: input.workspace_diagnostics,
209 next_steps: input.next_steps,
210 });
211 let mut output = fallow_output::serialize_check_json_output(
212 envelope,
213 input.envelope_mode,
214 input.telemetry_analysis_run_id,
215 )?;
216 strip_json_root_prefix(&mut output, input.root);
217 Ok(output)
218}
219
220pub fn serialize_check_json_payload(
226 input: CheckJsonPayloadInput<'_>,
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: None,
233 extras: input.extras,
234 workspace_diagnostics: input.workspace_diagnostics,
235 next_steps: Vec::new(),
236 });
237 let mut output = serde_json::to_value(envelope)?;
238 strip_json_root_prefix(&mut output, input.root);
239 Ok(output)
240}
241
242pub fn serialize_grouped_check_json(
248 input: GroupedCheckJsonOutputInput<'_>,
249) -> Result<serde_json::Value, serde_json::Error> {
250 let entries = input
251 .groups
252 .iter()
253 .map(|group| {
254 let mut results = group.results.clone();
255 apply_config_fixable_to_duplicate_exports(&mut results, input.config_fixable);
256 harmonize_typed_suppress_line_actions(&mut results);
257 CheckGroupedEntry {
258 key: group.key.clone(),
259 owners: group.owners.clone(),
260 total_issues: results.total_issues(),
261 results,
262 }
263 })
264 .collect();
265
266 let envelope = CheckGroupedOutput {
267 schema_version: SchemaVersion(CHECK_SCHEMA_VERSION),
268 version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
269 elapsed_ms: ElapsedMs(input.elapsed.as_millis() as u64),
270 grouped_by: input.grouped_by,
271 total_issues: input.original.total_issues(),
272 groups: entries,
273 baseline_staleness: input.baseline_staleness,
274 gate_outcomes: input.gate_outcomes,
275 meta: input.meta,
276 workspace_diagnostics: input.workspace_diagnostics,
277 next_steps: input.next_steps,
278 };
279
280 let mut output = fallow_output::serialize_check_grouped_json_output(
281 envelope,
282 input.envelope_mode,
283 input.telemetry_analysis_run_id,
284 )?;
285 strip_json_root_prefix(&mut output, input.root);
286 Ok(output)
287}
288
289pub fn serialize_duplication_json(
295 input: DuplicationJsonOutputInput<'_>,
296) -> Result<serde_json::Value, serde_json::Error> {
297 let payload =
298 DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
299 let envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
300 build_dupes_output(DupesOutputInput {
301 gate_outcomes: input.gate_outcomes,
302 schema_version: DUPES_SCHEMA_VERSION,
303 version: env!("CARGO_PKG_VERSION").to_string(),
304 elapsed: input.elapsed,
305 report: payload,
306 clone_groups_shown: input.report.clone_groups_shown(),
307 clone_groups_omitted: input.report.clone_groups_omitted(),
308 clone_families_shown: input.report.clone_families_shown(),
309 clone_families_omitted: input.report.clone_families_omitted(),
310 grouped_by: None,
311 total_issues: None,
312 groups: None,
313 baseline_staleness: input.baseline_staleness,
314 meta: input.meta,
315 workspace_diagnostics: input.workspace_diagnostics,
316 next_steps: input.next_steps,
317 });
318 let mut output = fallow_output::serialize_dupes_json_output(
319 envelope,
320 input.envelope_mode,
321 input.telemetry_analysis_run_id,
322 )?;
323 let root_prefix = format!("{}/", input.root.display());
324 strip_root_prefix(&mut output, &root_prefix);
325 Ok(output)
326}
327
328pub fn serialize_grouped_duplication_json(
334 input: GroupedDuplicationJsonOutputInput<'_>,
335) -> Result<serde_json::Value, serde_json::Error> {
336 let root_prefix = format!("{}/", input.root.display());
337 let payload =
338 DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
339 let envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
340 build_dupes_output(DupesOutputInput {
341 gate_outcomes: input.gate_outcomes,
342 schema_version: DUPES_SCHEMA_VERSION,
343 version: env!("CARGO_PKG_VERSION").to_string(),
344 elapsed: input.elapsed,
345 report: payload,
346 clone_groups_shown: input.report.clone_groups_shown(),
347 clone_groups_omitted: input.report.clone_groups_omitted(),
348 clone_families_shown: input.report.clone_families_shown(),
349 clone_families_omitted: input.report.clone_families_omitted(),
350 grouped_by: Some(group_by_mode_from_label(input.grouping.mode)),
351 total_issues: Some(input.report.clone_groups.len()),
352 groups: None,
353 baseline_staleness: input.baseline_staleness,
354 meta: input.meta,
355 workspace_diagnostics: input.workspace_diagnostics,
356 next_steps: input.next_steps,
357 });
358 let mut output = fallow_output::serialize_dupes_json_output(
359 envelope,
360 input.envelope_mode,
361 input.telemetry_analysis_run_id,
362 )?;
363 strip_root_prefix(&mut output, &root_prefix);
364
365 let group_values = input
366 .grouping
367 .groups
368 .iter()
369 .map(|group| {
370 let mut value = if input.include_fragments {
371 serde_json::to_value(group)?
372 } else {
373 let mut stripped = group.clone();
374 stripped.strip_fragments();
375 serde_json::to_value(&stripped)?
376 };
377 strip_root_prefix(&mut value, &root_prefix);
378 Ok(value)
379 })
380 .collect::<Result<Vec<_>, serde_json::Error>>()?;
381
382 if let serde_json::Value::Object(ref mut map) = output {
383 map.insert("groups".to_string(), serde_json::Value::Array(group_values));
384 }
385
386 Ok(output)
387}
388
389fn build_check_json_envelope(input: CheckJsonEnvelopeInput<'_>) -> CheckOutput {
390 let mut output = build_check_output(CheckOutputInput {
391 schema_version: CHECK_SCHEMA_VERSION,
392 version: env!("CARGO_PKG_VERSION").to_string(),
393 elapsed: input.elapsed,
394 results: input.results.clone(),
395 config_fixable: input.config_fixable,
396 meta: input.meta,
397 workspace_diagnostics: input.workspace_diagnostics,
398 next_steps: input.next_steps,
399 });
400 output.baseline_deltas = input.extras.baseline_deltas;
401 output.baseline = input.extras.baseline;
402 output.baseline_staleness = input.extras.baseline_staleness;
403 output.regression = input.extras.regression;
404 output.gate_outcomes = input.extras.gate_outcomes;
405 output
406}
407
408fn strip_json_root_prefix(output: &mut serde_json::Value, root: &Path) {
409 let root_prefix = format!("{}/", root.display());
410 strip_root_prefix(output, &root_prefix);
411}
412
413fn group_by_mode_from_label(label: &str) -> GroupByMode {
414 match label {
415 "directory" => GroupByMode::Directory,
416 "package" => GroupByMode::Package,
417 "section" => GroupByMode::Section,
418 _ => GroupByMode::Owner,
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use fallow_types::workspace::WorkspaceDiagnosticKind;
426
427 #[test]
428 fn grouped_check_json_carries_workspace_diagnostics_with_relative_paths() {
429 let root = Path::new("/project");
430 let output = serialize_grouped_check_json(GroupedCheckJsonOutputInput {
431 gate_outcomes: None,
432 baseline_staleness: None,
433 groups: &[],
434 original: &AnalysisResults::default(),
435 root,
436 elapsed: Duration::ZERO,
437 grouped_by: GroupByMode::Directory,
438 config_fixable: false,
439 meta: None,
440 workspace_diagnostics: vec![WorkspaceDiagnostic::new(
441 root,
442 root.join("src/unreadable.ts"),
443 WorkspaceDiagnosticKind::SourceReadFailure {
444 error: "permission denied".to_string(),
445 },
446 )],
447 next_steps: Vec::new(),
448 envelope_mode: RootEnvelopeMode::Tagged,
449 telemetry_analysis_run_id: None,
450 })
451 .expect("grouped check JSON serializes");
452
453 assert_eq!(
454 output["workspace_diagnostics"][0]["path"],
455 "src/unreadable.ts"
456 );
457 assert_eq!(
458 output["workspace_diagnostics"][0]["kind"],
459 "source-read-failure"
460 );
461 }
462}