1use serde::Serialize;
4use serde_json::Value;
5
6#[derive(Clone, Copy)]
8pub struct FixJsonOutputInput<'a> {
9 pub dry_run: bool,
11 pub fixes: &'a [Value],
13 pub skipped_content_changed: usize,
15 pub skipped_mixed_line_endings: usize,
17 pub skipped_low_confidence_exports: usize,
19 pub skipped_low_confidence_dependencies: usize,
22 pub skipped_low_confidence_members: usize,
25}
26
27#[derive(Debug, Clone, Serialize)]
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30#[cfg_attr(feature = "schema", schemars(title = "fallow fix --format json"))]
31pub struct FixJsonOutput<'a> {
32 pub dry_run: bool,
34 pub fixes: &'a [Value],
36 pub total_fixed: usize,
38 pub skipped: usize,
41 pub skipped_content_changed: usize,
43 pub skipped_mixed_line_endings: usize,
45 pub skipped_low_confidence_exports: usize,
47 pub skipped_low_confidence_dependencies: usize,
52 pub skipped_low_confidence_members: usize,
58}
59
60#[must_use]
62pub fn count_applied_fixes(fixes: &[Value]) -> usize {
63 fixes
64 .iter()
65 .filter(|fix| fix.get("applied").and_then(Value::as_bool).unwrap_or(false))
66 .count()
67}
68
69#[must_use]
71pub fn count_reported_fix_skips(fixes: &[Value]) -> usize {
72 fixes
73 .iter()
74 .filter(|fix| {
75 let is_skipped = fix.get("skipped").and_then(Value::as_bool).unwrap_or(false);
76 let reason = fix.get("skip_reason").and_then(Value::as_str);
77 let is_plan_skip = matches!(
78 reason,
79 Some(
80 "content_changed"
81 | "mixed_line_endings"
82 | "low_confidence_off_graph"
83 | "low_confidence_unresolved_imports"
84 | "low_confidence_incomplete_analysis"
85 )
86 );
87 is_skipped && !is_plan_skip
88 })
89 .count()
90}
91
92#[must_use]
94pub fn build_fix_json_output(input: FixJsonOutputInput<'_>) -> FixJsonOutput<'_> {
95 FixJsonOutput {
96 dry_run: input.dry_run,
97 fixes: input.fixes,
98 total_fixed: count_applied_fixes(input.fixes),
99 skipped: count_reported_fix_skips(input.fixes),
100 skipped_content_changed: input.skipped_content_changed,
101 skipped_mixed_line_endings: input.skipped_mixed_line_endings,
102 skipped_low_confidence_exports: input.skipped_low_confidence_exports,
103 skipped_low_confidence_dependencies: input.skipped_low_confidence_dependencies,
104 skipped_low_confidence_members: input.skipped_low_confidence_members,
105 }
106}
107
108pub fn serialize_fix_json_output(
114 input: FixJsonOutputInput<'_>,
115) -> Result<Value, serde_json::Error> {
116 serde_json::to_value(build_fix_json_output(input))
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use serde_json::json;
123
124 #[test]
125 fn fix_output_counts_applied_and_user_skips() {
126 let fixes = vec![
127 json!({"applied": true}),
128 json!({"applied": false, "skipped": true, "skip_reason": "manual"}),
129 json!({"skipped": true, "skip_reason": "content_changed"}),
130 json!({"skipped": true, "skip_reason": "low_confidence_unresolved_imports"}),
131 json!({
132 "type": "remove_dependency",
133 "skipped": true,
134 "skip_reason": "low_confidence_incomplete_analysis",
135 }),
136 json!({
137 "type": "remove_enum_member",
138 "skipped": true,
139 "skip_reason": "low_confidence_incomplete_analysis",
140 }),
141 ];
142
143 let output = build_fix_json_output(FixJsonOutputInput {
144 dry_run: true,
145 fixes: &fixes,
146 skipped_content_changed: 1,
147 skipped_mixed_line_endings: 2,
148 skipped_low_confidence_exports: 3,
149 skipped_low_confidence_dependencies: 4,
150 skipped_low_confidence_members: 5,
151 });
152
153 assert!(output.dry_run);
154 assert_eq!(output.total_fixed, 1);
155 assert_eq!(output.skipped, 1);
156 assert_eq!(output.skipped_content_changed, 1);
157 assert_eq!(output.skipped_mixed_line_endings, 2);
158 assert_eq!(output.skipped_low_confidence_exports, 3);
159 assert_eq!(
160 output.skipped, 1,
161 "a withheld dependency belongs to its own counter, not the generic skip count"
162 );
163 assert_eq!(output.skipped_low_confidence_dependencies, 4);
164 assert_eq!(
165 output.skipped, 1,
166 "a withheld enum member belongs to its own counter too"
167 );
168 assert_eq!(output.skipped_low_confidence_members, 5);
169 }
170
171 #[test]
172 fn fix_output_serializes_expected_root_keys() {
173 let fixes = vec![json!({"type": "unused-export", "applied": true})];
174 let value = serialize_fix_json_output(FixJsonOutputInput {
175 dry_run: false,
176 fixes: &fixes,
177 skipped_content_changed: 0,
178 skipped_mixed_line_endings: 0,
179 skipped_low_confidence_exports: 0,
180 skipped_low_confidence_dependencies: 0,
181 skipped_low_confidence_members: 0,
182 })
183 .expect("fix output serializes");
184
185 assert_eq!(value["dry_run"], false);
186 assert_eq!(value["total_fixed"], 1);
187 assert_eq!(value["skipped"], 0);
188 assert_eq!(value["fixes"][0]["type"], "unused-export");
189 }
190}