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 baseline_deltas: Option<BaselineDeltas>,
71 pub baseline: Option<BaselineMatch>,
73 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
76 pub regression: Option<RegressionResult>,
78 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
83 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
88}
89
90struct CheckJsonEnvelopeInput<'a> {
91 results: &'a AnalysisResults,
92 elapsed: Duration,
93 config_fixable: bool,
94 meta: Option<Meta>,
95 extras: CheckJsonExtraOutputs,
96 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
97 next_steps: Vec<NextStep>,
98}
99
100pub struct GroupedCheckJsonOutputInput<'a> {
102 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
104 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
109 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
114
115 pub groups: &'a [ResultGroup],
117 pub original: &'a AnalysisResults,
119 pub root: &'a Path,
121 pub elapsed: Duration,
123 pub grouped_by: GroupByMode,
125 pub config_fixable: bool,
128 pub meta: Option<Meta>,
130 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
132 pub next_steps: Vec<NextStep>,
134 pub telemetry_analysis_run_id: Option<&'a str>,
136}
137
138pub struct DuplicationJsonOutputInput<'a> {
140 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
142 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
147 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
152
153 pub report: &'a DuplicationReport,
155 pub root: &'a Path,
157 pub elapsed: Duration,
159 pub include_fragments: bool,
161 pub meta: Option<Meta>,
163 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
165 pub next_steps: Vec<NextStep>,
167 pub telemetry_analysis_run_id: Option<&'a str>,
169}
170
171pub struct GroupedDuplicationJsonOutputInput<'a> {
173 pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
175 pub gate_outcomes: Option<fallow_output::GateOutcomes>,
180 pub request_outcomes: Option<fallow_output::RequestOutcomes>,
185
186 pub report: &'a DuplicationReport,
188 pub grouping: &'a DuplicationGrouping,
190 pub root: &'a Path,
192 pub elapsed: Duration,
194 pub include_fragments: bool,
196 pub meta: Option<Meta>,
198 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
200 pub next_steps: Vec<NextStep>,
202 pub telemetry_analysis_run_id: Option<&'a str>,
204}
205
206pub fn serialize_check_json(
212 input: CheckJsonOutputInput<'_>,
213) -> Result<serde_json::Value, serde_json::Error> {
214 let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
215 results: input.results,
216 elapsed: input.elapsed,
217 config_fixable: input.config_fixable,
218 meta: input.meta,
219 extras: input.extras,
220 workspace_diagnostics: input.workspace_diagnostics,
221 next_steps: input.next_steps,
222 });
223 let mut output =
224 fallow_output::serialize_check_json_output(envelope, input.telemetry_analysis_run_id)?;
225 strip_json_root_prefix(&mut output, input.root);
226 Ok(output)
227}
228
229pub fn serialize_check_json_payload(
235 input: CheckJsonPayloadInput<'_>,
236) -> Result<serde_json::Value, serde_json::Error> {
237 let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
238 results: input.results,
239 elapsed: input.elapsed,
240 config_fixable: input.config_fixable,
241 meta: None,
242 extras: input.extras,
243 workspace_diagnostics: input.workspace_diagnostics,
244 next_steps: Vec::new(),
245 });
246 let mut output = serde_json::to_value(envelope)?;
247 strip_json_root_prefix(&mut output, input.root);
248 Ok(output)
249}
250
251pub fn serialize_grouped_check_json(
257 input: GroupedCheckJsonOutputInput<'_>,
258) -> Result<serde_json::Value, serde_json::Error> {
259 let entries = input
260 .groups
261 .iter()
262 .map(|group| {
263 let mut results = group.results.clone();
264 apply_config_fixable_to_duplicate_exports(&mut results, input.config_fixable);
265 harmonize_typed_suppress_line_actions(&mut results);
266 CheckGroupedEntry {
267 key: group.key.clone(),
268 owners: group.owners.clone(),
269 total_issues: results.total_issues(),
270 results,
271 }
272 })
273 .collect();
274
275 let envelope = CheckGroupedOutput {
276 request_outcomes: input.request_outcomes,
277 schema_version: SchemaVersion(CHECK_SCHEMA_VERSION),
278 version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
279 elapsed_ms: ElapsedMs(input.elapsed.as_millis() as u64),
280 grouped_by: input.grouped_by,
281 total_issues: input.original.total_issues(),
282 groups: entries,
283 baseline_staleness: input.baseline_staleness,
284 gate_outcomes: input.gate_outcomes,
285 meta: input.meta,
286 workspace_diagnostics: input.workspace_diagnostics,
287 next_steps: input.next_steps,
288 };
289
290 let mut output = fallow_output::serialize_check_grouped_json_output(
291 envelope,
292 input.telemetry_analysis_run_id,
293 )?;
294 strip_json_root_prefix(&mut output, input.root);
295 Ok(output)
296}
297
298pub fn serialize_duplication_json(
304 input: DuplicationJsonOutputInput<'_>,
305) -> Result<serde_json::Value, serde_json::Error> {
306 let payload =
307 DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
308 let envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
309 build_dupes_output(DupesOutputInput {
310 gate_outcomes: input.gate_outcomes,
311 request_outcomes: input.request_outcomes,
312 schema_version: DUPES_SCHEMA_VERSION,
313 version: env!("CARGO_PKG_VERSION").to_string(),
314 elapsed: input.elapsed,
315 report: payload,
316 clone_groups_shown: input.report.clone_groups_shown(),
317 clone_groups_omitted: input.report.clone_groups_omitted(),
318 clone_families_shown: input.report.clone_families_shown(),
319 clone_families_omitted: input.report.clone_families_omitted(),
320 grouped_by: None,
321 total_issues: None,
322 groups: None,
323 baseline_staleness: input.baseline_staleness,
324 meta: input.meta,
325 workspace_diagnostics: input.workspace_diagnostics,
326 next_steps: input.next_steps,
327 });
328 let mut output =
329 fallow_output::serialize_dupes_json_output(envelope, input.telemetry_analysis_run_id)?;
330 let root_prefix = format!("{}/", input.root.display());
331 strip_root_prefix(&mut output, &root_prefix);
332 Ok(output)
333}
334
335pub fn serialize_grouped_duplication_json(
341 input: GroupedDuplicationJsonOutputInput<'_>,
342) -> Result<serde_json::Value, serde_json::Error> {
343 let root_prefix = format!("{}/", input.root.display());
344 let payload =
345 DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
346 let envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
347 build_dupes_output(DupesOutputInput {
348 gate_outcomes: input.gate_outcomes,
349 request_outcomes: input.request_outcomes,
350 schema_version: DUPES_SCHEMA_VERSION,
351 version: env!("CARGO_PKG_VERSION").to_string(),
352 elapsed: input.elapsed,
353 report: payload,
354 clone_groups_shown: input.report.clone_groups_shown(),
355 clone_groups_omitted: input.report.clone_groups_omitted(),
356 clone_families_shown: input.report.clone_families_shown(),
357 clone_families_omitted: input.report.clone_families_omitted(),
358 grouped_by: Some(group_by_mode_from_label(input.grouping.mode)),
359 total_issues: Some(input.report.clone_groups.len()),
360 groups: None,
361 baseline_staleness: input.baseline_staleness,
362 meta: input.meta,
363 workspace_diagnostics: input.workspace_diagnostics,
364 next_steps: input.next_steps,
365 });
366 let mut output =
367 fallow_output::serialize_dupes_json_output(envelope, input.telemetry_analysis_run_id)?;
368 strip_root_prefix(&mut output, &root_prefix);
369
370 let group_values = input
371 .grouping
372 .groups
373 .iter()
374 .map(|group| {
375 let mut value = if input.include_fragments {
376 serde_json::to_value(group)?
377 } else {
378 let mut stripped = group.clone();
379 stripped.strip_fragments();
380 serde_json::to_value(&stripped)?
381 };
382 strip_root_prefix(&mut value, &root_prefix);
383 Ok(value)
384 })
385 .collect::<Result<Vec<_>, serde_json::Error>>()?;
386
387 if let serde_json::Value::Object(ref mut map) = output {
388 map.insert("groups".to_string(), serde_json::Value::Array(group_values));
389 }
390
391 Ok(output)
392}
393
394fn build_check_json_envelope(input: CheckJsonEnvelopeInput<'_>) -> CheckOutput {
395 let mut output = build_check_output(CheckOutputInput {
396 schema_version: CHECK_SCHEMA_VERSION,
397 version: env!("CARGO_PKG_VERSION").to_string(),
398 elapsed: input.elapsed,
399 results: input.results.clone(),
400 config_fixable: input.config_fixable,
401 meta: input.meta,
402 workspace_diagnostics: input.workspace_diagnostics,
403 next_steps: input.next_steps,
404 });
405 output.baseline_deltas = input.extras.baseline_deltas;
406 output.baseline = input.extras.baseline;
407 output.baseline_staleness = input.extras.baseline_staleness;
408 output.regression = input.extras.regression;
409 output.gate_outcomes = input.extras.gate_outcomes;
410 output.request_outcomes = input.extras.request_outcomes;
411 output
412}
413
414fn strip_json_root_prefix(output: &mut serde_json::Value, root: &Path) {
415 let root_prefix = format!("{}/", root.display());
416 strip_root_prefix(output, &root_prefix);
417}
418
419fn group_by_mode_from_label(label: &str) -> GroupByMode {
420 match label {
421 "directory" => GroupByMode::Directory,
422 "package" => GroupByMode::Package,
423 "section" => GroupByMode::Section,
424 _ => GroupByMode::Owner,
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431 use fallow_types::workspace::WorkspaceDiagnosticKind;
432
433 #[test]
434 fn grouped_check_json_carries_workspace_diagnostics_with_relative_paths() {
435 let root = Path::new("/project");
436 let output = serialize_grouped_check_json(GroupedCheckJsonOutputInput {
437 gate_outcomes: None,
438 request_outcomes: None,
439 baseline_staleness: None,
440 groups: &[],
441 original: &AnalysisResults::default(),
442 root,
443 elapsed: Duration::ZERO,
444 grouped_by: GroupByMode::Directory,
445 config_fixable: false,
446 meta: None,
447 workspace_diagnostics: vec![WorkspaceDiagnostic::new(
448 root,
449 root.join("src/unreadable.ts"),
450 WorkspaceDiagnosticKind::SourceReadFailure {
451 error: "permission denied".to_string(),
452 },
453 )],
454 next_steps: Vec::new(),
455 telemetry_analysis_run_id: None,
456 })
457 .expect("grouped check JSON serializes");
458
459 assert_eq!(
460 output["workspace_diagnostics"][0]["path"],
461 "src/unreadable.ts"
462 );
463 assert_eq!(
464 output["workspace_diagnostics"][0]["kind"],
465 "source-read-failure"
466 );
467 }
468}