1use serde::Serialize;
4use serde_json::Value;
5
6#[derive(Clone, Copy)]
8pub struct FixJsonOutputInput<'a> {
9 pub dry_run: bool,
10 pub fixes: &'a [Value],
11 pub skipped_content_changed: usize,
12 pub skipped_mixed_line_endings: usize,
13 pub skipped_low_confidence_exports: usize,
14}
15
16#[derive(Debug, Clone, Serialize)]
18#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
19#[cfg_attr(feature = "schema", schemars(title = "fallow fix --format json"))]
20pub struct FixJsonOutput<'a> {
21 pub dry_run: bool,
22 pub fixes: &'a [Value],
23 pub total_fixed: usize,
24 pub skipped: usize,
25 pub skipped_content_changed: usize,
26 pub skipped_mixed_line_endings: usize,
27 pub skipped_low_confidence_exports: usize,
28}
29
30#[must_use]
32pub fn count_applied_fixes(fixes: &[Value]) -> usize {
33 fixes
34 .iter()
35 .filter(|fix| fix.get("applied").and_then(Value::as_bool).unwrap_or(false))
36 .count()
37}
38
39#[must_use]
41pub fn count_reported_fix_skips(fixes: &[Value]) -> usize {
42 fixes
43 .iter()
44 .filter(|fix| {
45 let is_skipped = fix.get("skipped").and_then(Value::as_bool).unwrap_or(false);
46 let reason = fix.get("skip_reason").and_then(Value::as_str);
47 let is_plan_skip = matches!(
48 reason,
49 Some(
50 "content_changed"
51 | "mixed_line_endings"
52 | "low_confidence_off_graph"
53 | "low_confidence_unresolved_imports"
54 )
55 );
56 is_skipped && !is_plan_skip
57 })
58 .count()
59}
60
61#[must_use]
63pub fn build_fix_json_output(input: FixJsonOutputInput<'_>) -> FixJsonOutput<'_> {
64 FixJsonOutput {
65 dry_run: input.dry_run,
66 fixes: input.fixes,
67 total_fixed: count_applied_fixes(input.fixes),
68 skipped: count_reported_fix_skips(input.fixes),
69 skipped_content_changed: input.skipped_content_changed,
70 skipped_mixed_line_endings: input.skipped_mixed_line_endings,
71 skipped_low_confidence_exports: input.skipped_low_confidence_exports,
72 }
73}
74
75pub fn serialize_fix_json_output(
81 input: FixJsonOutputInput<'_>,
82) -> Result<Value, serde_json::Error> {
83 serde_json::to_value(build_fix_json_output(input))
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use serde_json::json;
90
91 #[test]
92 fn fix_output_counts_applied_and_user_skips() {
93 let fixes = vec![
94 json!({"applied": true}),
95 json!({"applied": false, "skipped": true, "skip_reason": "manual"}),
96 json!({"skipped": true, "skip_reason": "content_changed"}),
97 json!({"skipped": true, "skip_reason": "low_confidence_unresolved_imports"}),
98 ];
99
100 let output = build_fix_json_output(FixJsonOutputInput {
101 dry_run: true,
102 fixes: &fixes,
103 skipped_content_changed: 1,
104 skipped_mixed_line_endings: 2,
105 skipped_low_confidence_exports: 3,
106 });
107
108 assert!(output.dry_run);
109 assert_eq!(output.total_fixed, 1);
110 assert_eq!(output.skipped, 1);
111 assert_eq!(output.skipped_content_changed, 1);
112 assert_eq!(output.skipped_mixed_line_endings, 2);
113 assert_eq!(output.skipped_low_confidence_exports, 3);
114 }
115
116 #[test]
117 fn fix_output_serializes_expected_root_keys() {
118 let fixes = vec![json!({"type": "unused-export", "applied": true})];
119 let value = serialize_fix_json_output(FixJsonOutputInput {
120 dry_run: false,
121 fixes: &fixes,
122 skipped_content_changed: 0,
123 skipped_mixed_line_endings: 0,
124 skipped_low_confidence_exports: 0,
125 })
126 .expect("fix output serializes");
127
128 assert_eq!(value["dry_run"], false);
129 assert_eq!(value["total_fixed"], 1);
130 assert_eq!(value["skipped"], 0);
131 assert_eq!(value["fixes"][0]["type"], "unused-export");
132 }
133}