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 regression: Option<RegressionResult>,
77}
78
79struct CheckJsonEnvelopeInput<'a> {
80 results: &'a AnalysisResults,
81 elapsed: Duration,
82 config_fixable: bool,
83 meta: Option<Meta>,
84 extras: CheckJsonExtraOutputs,
85 workspace_diagnostics: Vec<WorkspaceDiagnostic>,
86 next_steps: Vec<NextStep>,
87}
88
89pub struct GroupedCheckJsonOutputInput<'a> {
91 pub groups: &'a [ResultGroup],
93 pub original: &'a AnalysisResults,
95 pub root: &'a Path,
97 pub elapsed: Duration,
99 pub grouped_by: GroupByMode,
101 pub config_fixable: bool,
104 pub meta: Option<Meta>,
106 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
108 pub next_steps: Vec<NextStep>,
110 pub envelope_mode: RootEnvelopeMode,
112 pub telemetry_analysis_run_id: Option<&'a str>,
114}
115
116pub struct DuplicationJsonOutputInput<'a> {
118 pub report: &'a DuplicationReport,
120 pub root: &'a Path,
122 pub elapsed: Duration,
124 pub include_fragments: bool,
126 pub meta: Option<Meta>,
128 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
130 pub next_steps: Vec<NextStep>,
132 pub envelope_mode: RootEnvelopeMode,
134 pub telemetry_analysis_run_id: Option<&'a str>,
136}
137
138pub struct GroupedDuplicationJsonOutputInput<'a> {
140 pub report: &'a DuplicationReport,
142 pub grouping: &'a DuplicationGrouping,
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 fn serialize_check_json(
168 input: CheckJsonOutputInput<'_>,
169) -> Result<serde_json::Value, serde_json::Error> {
170 let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
171 results: input.results,
172 elapsed: input.elapsed,
173 config_fixable: input.config_fixable,
174 meta: input.meta,
175 extras: input.extras,
176 workspace_diagnostics: input.workspace_diagnostics,
177 next_steps: input.next_steps,
178 });
179 let mut output = fallow_output::serialize_check_json_output(
180 envelope,
181 input.envelope_mode,
182 input.telemetry_analysis_run_id,
183 )?;
184 strip_json_root_prefix(&mut output, input.root);
185 Ok(output)
186}
187
188pub fn serialize_check_json_payload(
194 input: CheckJsonPayloadInput<'_>,
195) -> Result<serde_json::Value, serde_json::Error> {
196 let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
197 results: input.results,
198 elapsed: input.elapsed,
199 config_fixable: input.config_fixable,
200 meta: None,
201 extras: input.extras,
202 workspace_diagnostics: input.workspace_diagnostics,
203 next_steps: Vec::new(),
204 });
205 let mut output = serde_json::to_value(envelope)?;
206 strip_json_root_prefix(&mut output, input.root);
207 Ok(output)
208}
209
210pub fn serialize_grouped_check_json(
216 input: GroupedCheckJsonOutputInput<'_>,
217) -> Result<serde_json::Value, serde_json::Error> {
218 let entries = input
219 .groups
220 .iter()
221 .map(|group| {
222 let mut results = group.results.clone();
223 apply_config_fixable_to_duplicate_exports(&mut results, input.config_fixable);
224 harmonize_typed_suppress_line_actions(&mut results);
225 CheckGroupedEntry {
226 key: group.key.clone(),
227 owners: group.owners.clone(),
228 total_issues: results.total_issues(),
229 results,
230 }
231 })
232 .collect();
233
234 let envelope = CheckGroupedOutput {
235 schema_version: SchemaVersion(CHECK_SCHEMA_VERSION),
236 version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
237 elapsed_ms: ElapsedMs(input.elapsed.as_millis() as u64),
238 grouped_by: input.grouped_by,
239 total_issues: input.original.total_issues(),
240 groups: entries,
241 meta: input.meta,
242 workspace_diagnostics: input.workspace_diagnostics,
243 next_steps: input.next_steps,
244 };
245
246 let mut output = fallow_output::serialize_check_grouped_json_output(
247 envelope,
248 input.envelope_mode,
249 input.telemetry_analysis_run_id,
250 )?;
251 strip_json_root_prefix(&mut output, input.root);
252 Ok(output)
253}
254
255pub fn serialize_duplication_json(
261 input: DuplicationJsonOutputInput<'_>,
262) -> Result<serde_json::Value, serde_json::Error> {
263 let payload =
264 DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
265 let envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
266 build_dupes_output(DupesOutputInput {
267 schema_version: DUPES_SCHEMA_VERSION,
268 version: env!("CARGO_PKG_VERSION").to_string(),
269 elapsed: input.elapsed,
270 report: payload,
271 clone_groups_shown: input.report.clone_groups_shown(),
272 clone_groups_omitted: input.report.clone_groups_omitted(),
273 clone_families_shown: input.report.clone_families_shown(),
274 clone_families_omitted: input.report.clone_families_omitted(),
275 grouped_by: None,
276 total_issues: None,
277 groups: None,
278 meta: input.meta,
279 workspace_diagnostics: input.workspace_diagnostics,
280 next_steps: input.next_steps,
281 });
282 let mut output = fallow_output::serialize_dupes_json_output(
283 envelope,
284 input.envelope_mode,
285 input.telemetry_analysis_run_id,
286 )?;
287 let root_prefix = format!("{}/", input.root.display());
288 strip_root_prefix(&mut output, &root_prefix);
289 Ok(output)
290}
291
292pub fn serialize_grouped_duplication_json(
298 input: GroupedDuplicationJsonOutputInput<'_>,
299) -> Result<serde_json::Value, serde_json::Error> {
300 let root_prefix = format!("{}/", input.root.display());
301 let payload =
302 DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
303 let envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
304 build_dupes_output(DupesOutputInput {
305 schema_version: DUPES_SCHEMA_VERSION,
306 version: env!("CARGO_PKG_VERSION").to_string(),
307 elapsed: input.elapsed,
308 report: payload,
309 clone_groups_shown: input.report.clone_groups_shown(),
310 clone_groups_omitted: input.report.clone_groups_omitted(),
311 clone_families_shown: input.report.clone_families_shown(),
312 clone_families_omitted: input.report.clone_families_omitted(),
313 grouped_by: Some(group_by_mode_from_label(input.grouping.mode)),
314 total_issues: Some(input.report.clone_groups.len()),
315 groups: None,
316 meta: input.meta,
317 workspace_diagnostics: input.workspace_diagnostics,
318 next_steps: input.next_steps,
319 });
320 let mut output = fallow_output::serialize_dupes_json_output(
321 envelope,
322 input.envelope_mode,
323 input.telemetry_analysis_run_id,
324 )?;
325 strip_root_prefix(&mut output, &root_prefix);
326
327 let group_values = input
328 .grouping
329 .groups
330 .iter()
331 .map(|group| {
332 let mut value = if input.include_fragments {
333 serde_json::to_value(group)?
334 } else {
335 let mut stripped = group.clone();
336 stripped.strip_fragments();
337 serde_json::to_value(&stripped)?
338 };
339 strip_root_prefix(&mut value, &root_prefix);
340 Ok(value)
341 })
342 .collect::<Result<Vec<_>, serde_json::Error>>()?;
343
344 if let serde_json::Value::Object(ref mut map) = output {
345 map.insert("groups".to_string(), serde_json::Value::Array(group_values));
346 }
347
348 Ok(output)
349}
350
351fn build_check_json_envelope(input: CheckJsonEnvelopeInput<'_>) -> CheckOutput {
352 let mut output = build_check_output(CheckOutputInput {
353 schema_version: CHECK_SCHEMA_VERSION,
354 version: env!("CARGO_PKG_VERSION").to_string(),
355 elapsed: input.elapsed,
356 results: input.results.clone(),
357 config_fixable: input.config_fixable,
358 meta: input.meta,
359 workspace_diagnostics: input.workspace_diagnostics,
360 next_steps: input.next_steps,
361 });
362 output.baseline_deltas = input.extras.baseline_deltas;
363 output.baseline = input.extras.baseline;
364 output.regression = input.extras.regression;
365 output
366}
367
368fn strip_json_root_prefix(output: &mut serde_json::Value, root: &Path) {
369 let root_prefix = format!("{}/", root.display());
370 strip_root_prefix(output, &root_prefix);
371}
372
373fn group_by_mode_from_label(label: &str) -> GroupByMode {
374 match label {
375 "directory" => GroupByMode::Directory,
376 "package" => GroupByMode::Package,
377 "section" => GroupByMode::Section,
378 _ => GroupByMode::Owner,
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use fallow_types::workspace::WorkspaceDiagnosticKind;
386
387 #[test]
388 fn grouped_check_json_carries_workspace_diagnostics_with_relative_paths() {
389 let root = Path::new("/project");
390 let output = serialize_grouped_check_json(GroupedCheckJsonOutputInput {
391 groups: &[],
392 original: &AnalysisResults::default(),
393 root,
394 elapsed: Duration::ZERO,
395 grouped_by: GroupByMode::Directory,
396 config_fixable: false,
397 meta: None,
398 workspace_diagnostics: vec![WorkspaceDiagnostic::new(
399 root,
400 root.join("src/unreadable.ts"),
401 WorkspaceDiagnosticKind::SourceReadFailure {
402 error: "permission denied".to_string(),
403 },
404 )],
405 next_steps: Vec::new(),
406 envelope_mode: RootEnvelopeMode::Tagged,
407 telemetry_analysis_run_id: None,
408 })
409 .expect("grouped check JSON serializes");
410
411 assert_eq!(
412 output["workspace_diagnostics"][0]["path"],
413 "src/unreadable.ts"
414 );
415 assert_eq!(
416 output["workspace_diagnostics"][0]["kind"],
417 "source-read-failure"
418 );
419 }
420}