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 meta: Option<Meta>,
126 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
128 pub next_steps: Vec<NextStep>,
130 pub envelope_mode: RootEnvelopeMode,
132 pub telemetry_analysis_run_id: Option<&'a str>,
134}
135
136pub struct GroupedDuplicationJsonOutputInput<'a> {
138 pub report: &'a DuplicationReport,
140 pub grouping: &'a DuplicationGrouping,
142 pub root: &'a Path,
144 pub elapsed: Duration,
146 pub meta: Option<Meta>,
148 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
150 pub next_steps: Vec<NextStep>,
152 pub envelope_mode: RootEnvelopeMode,
154 pub telemetry_analysis_run_id: Option<&'a str>,
156}
157
158pub fn serialize_check_json(
164 input: CheckJsonOutputInput<'_>,
165) -> Result<serde_json::Value, serde_json::Error> {
166 let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
167 results: input.results,
168 elapsed: input.elapsed,
169 config_fixable: input.config_fixable,
170 meta: input.meta,
171 extras: input.extras,
172 workspace_diagnostics: input.workspace_diagnostics,
173 next_steps: input.next_steps,
174 });
175 let mut output = fallow_output::serialize_check_json_output(
176 envelope,
177 input.envelope_mode,
178 input.telemetry_analysis_run_id,
179 )?;
180 strip_json_root_prefix(&mut output, input.root);
181 Ok(output)
182}
183
184pub fn serialize_check_json_payload(
190 input: CheckJsonPayloadInput<'_>,
191) -> Result<serde_json::Value, serde_json::Error> {
192 let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
193 results: input.results,
194 elapsed: input.elapsed,
195 config_fixable: input.config_fixable,
196 meta: None,
197 extras: input.extras,
198 workspace_diagnostics: input.workspace_diagnostics,
199 next_steps: Vec::new(),
200 });
201 let mut output = serde_json::to_value(envelope)?;
202 strip_json_root_prefix(&mut output, input.root);
203 Ok(output)
204}
205
206pub fn serialize_grouped_check_json(
212 input: GroupedCheckJsonOutputInput<'_>,
213) -> Result<serde_json::Value, serde_json::Error> {
214 let entries = input
215 .groups
216 .iter()
217 .map(|group| {
218 let mut results = group.results.clone();
219 apply_config_fixable_to_duplicate_exports(&mut results, input.config_fixable);
220 harmonize_typed_suppress_line_actions(&mut results);
221 CheckGroupedEntry {
222 key: group.key.clone(),
223 owners: group.owners.clone(),
224 total_issues: results.total_issues(),
225 results,
226 }
227 })
228 .collect();
229
230 let envelope = CheckGroupedOutput {
231 schema_version: SchemaVersion(CHECK_SCHEMA_VERSION),
232 version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
233 elapsed_ms: ElapsedMs(input.elapsed.as_millis() as u64),
234 grouped_by: input.grouped_by,
235 total_issues: input.original.total_issues(),
236 groups: entries,
237 meta: input.meta,
238 workspace_diagnostics: input.workspace_diagnostics,
239 next_steps: input.next_steps,
240 };
241
242 let mut output = fallow_output::serialize_check_grouped_json_output(
243 envelope,
244 input.envelope_mode,
245 input.telemetry_analysis_run_id,
246 )?;
247 strip_json_root_prefix(&mut output, input.root);
248 Ok(output)
249}
250
251pub fn serialize_duplication_json(
257 input: DuplicationJsonOutputInput<'_>,
258) -> Result<serde_json::Value, serde_json::Error> {
259 let payload = DupesReportPayload::from_report(input.report);
260 let envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
261 build_dupes_output(DupesOutputInput {
262 schema_version: DUPES_SCHEMA_VERSION,
263 version: env!("CARGO_PKG_VERSION").to_string(),
264 elapsed: input.elapsed,
265 report: payload,
266 grouped_by: None,
267 total_issues: None,
268 groups: None,
269 meta: input.meta,
270 workspace_diagnostics: input.workspace_diagnostics,
271 next_steps: input.next_steps,
272 });
273 let mut output = fallow_output::serialize_dupes_json_output(
274 envelope,
275 input.envelope_mode,
276 input.telemetry_analysis_run_id,
277 )?;
278 let root_prefix = format!("{}/", input.root.display());
279 strip_root_prefix(&mut output, &root_prefix);
280 Ok(output)
281}
282
283pub fn serialize_grouped_duplication_json(
289 input: GroupedDuplicationJsonOutputInput<'_>,
290) -> Result<serde_json::Value, serde_json::Error> {
291 let root_prefix = format!("{}/", input.root.display());
292 let payload = DupesReportPayload::from_report(input.report);
293 let envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
294 build_dupes_output(DupesOutputInput {
295 schema_version: DUPES_SCHEMA_VERSION,
296 version: env!("CARGO_PKG_VERSION").to_string(),
297 elapsed: input.elapsed,
298 report: payload,
299 grouped_by: Some(group_by_mode_from_label(input.grouping.mode)),
300 total_issues: Some(input.report.clone_groups.len()),
301 groups: None,
302 meta: input.meta,
303 workspace_diagnostics: input.workspace_diagnostics,
304 next_steps: input.next_steps,
305 });
306 let mut output = fallow_output::serialize_dupes_json_output(
307 envelope,
308 input.envelope_mode,
309 input.telemetry_analysis_run_id,
310 )?;
311 strip_root_prefix(&mut output, &root_prefix);
312
313 let group_values = input
314 .grouping
315 .groups
316 .iter()
317 .map(|group| {
318 let mut value = serde_json::to_value(group)?;
319 strip_root_prefix(&mut value, &root_prefix);
320 Ok(value)
321 })
322 .collect::<Result<Vec<_>, serde_json::Error>>()?;
323
324 if let serde_json::Value::Object(ref mut map) = output {
325 map.insert("groups".to_string(), serde_json::Value::Array(group_values));
326 }
327
328 Ok(output)
329}
330
331fn build_check_json_envelope(input: CheckJsonEnvelopeInput<'_>) -> CheckOutput {
332 let mut output = build_check_output(CheckOutputInput {
333 schema_version: CHECK_SCHEMA_VERSION,
334 version: env!("CARGO_PKG_VERSION").to_string(),
335 elapsed: input.elapsed,
336 results: input.results.clone(),
337 config_fixable: input.config_fixable,
338 meta: input.meta,
339 workspace_diagnostics: input.workspace_diagnostics,
340 next_steps: input.next_steps,
341 });
342 output.baseline_deltas = input.extras.baseline_deltas;
343 output.baseline = input.extras.baseline;
344 output.regression = input.extras.regression;
345 output
346}
347
348fn strip_json_root_prefix(output: &mut serde_json::Value, root: &Path) {
349 let root_prefix = format!("{}/", root.display());
350 strip_root_prefix(output, &root_prefix);
351}
352
353fn group_by_mode_from_label(label: &str) -> GroupByMode {
354 match label {
355 "directory" => GroupByMode::Directory,
356 "package" => GroupByMode::Package,
357 "section" => GroupByMode::Section,
358 _ => GroupByMode::Owner,
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use fallow_types::workspace::WorkspaceDiagnosticKind;
366
367 #[test]
368 fn grouped_check_json_carries_workspace_diagnostics_with_relative_paths() {
369 let root = Path::new("/project");
370 let output = serialize_grouped_check_json(GroupedCheckJsonOutputInput {
371 groups: &[],
372 original: &AnalysisResults::default(),
373 root,
374 elapsed: Duration::ZERO,
375 grouped_by: GroupByMode::Directory,
376 config_fixable: false,
377 meta: None,
378 workspace_diagnostics: vec![WorkspaceDiagnostic::new(
379 root,
380 root.join("src/unreadable.ts"),
381 WorkspaceDiagnosticKind::SourceReadFailure {
382 error: "permission denied".to_string(),
383 },
384 )],
385 next_steps: Vec::new(),
386 envelope_mode: RootEnvelopeMode::Tagged,
387 telemetry_analysis_run_id: None,
388 })
389 .expect("grouped check JSON serializes");
390
391 assert_eq!(
392 output["workspace_diagnostics"][0]["path"],
393 "src/unreadable.ts"
394 );
395 assert_eq!(
396 output["workspace_diagnostics"][0]["kind"],
397 "source-read-failure"
398 );
399 }
400}