1use std::{
2 collections::HashSet,
3 env,
4 io::{BufRead, BufReader, Cursor},
5 path::Path,
6 process::Stdio,
7};
8
9use cargo_util::paths;
10use clap::Parser;
11use indexmap::{IndexMap, IndexSet};
12use rustfix::{collect_suggestions, CodeFix, Suggestion};
13use tracing::{trace, warn};
14
15use crate::{
16 core::shell,
17 ops::check::{BuildUnit, CheckOutput, Message},
18 util::{cli::CheckFlags, package::format_package_id, vcs::VcsOpts},
19 CargoResult,
20};
21
22#[derive(Debug, Parser)]
23pub struct FixitArgs {
24 #[arg(long)]
26 clippy: bool,
27
28 #[command(flatten)]
29 vcs_opts: VcsOpts,
30
31 #[command(flatten)]
32 check_flags: CheckFlags,
33}
34
35impl FixitArgs {
36 pub fn exec(self) -> CargoResult<()> {
37 exec(self)
38 }
39}
40
41#[derive(Debug, Default)]
42struct File {
43 fixes: u32,
44}
45
46#[tracing::instrument(skip_all)]
47fn exec(args: FixitArgs) -> CargoResult<()> {
48 args.vcs_opts.valid_vcs()?;
49
50 let mut files = IndexMap::new();
51
52 let max_iterations: usize = env::var("CARGO_FIX_MAX_RETRIES")
53 .ok()
54 .and_then(|i| i.parse().ok())
55 .unwrap_or(4);
56 let mut iteration = 0;
57
58 let mut last_errors;
59
60 let mut current_target = None;
61 let mut seen = HashSet::new();
62
63 loop {
64 trace!("iteration={iteration}");
65 trace!("current_target={current_target:?}");
66 let (messages, _exit_code) = check(&args)?;
67
68 let (mut errors, build_unit_map) = collect_errors(messages, &seen);
69
70 let mut made_changes = false;
71
72 for (build_unit, file_map) in build_unit_map {
73 if current_target.is_none() && file_map.is_empty() {
74 if seen.iter().all(|b| b.package_id != build_unit.package_id) {
75 shell::status("Checking", format_package_id(&build_unit.package_id)?)?;
76 }
77 seen.insert(build_unit);
78 } else if !file_map.is_empty()
79 && current_target.get_or_insert(build_unit.clone()) == &build_unit
80 && fix_errors(&mut files, file_map, &mut errors)?
81 {
82 made_changes = true;
83 break;
84 }
85 }
86
87 trace!("made_changes={made_changes:?}");
88 trace!("current_target={current_target:?}");
89
90 last_errors = errors;
91 iteration += 1;
92
93 if !made_changes || iteration >= max_iterations {
94 if let Some(pkg) = current_target {
95 if seen.iter().all(|b| b.package_id != pkg.package_id) {
96 shell::status("Fixed", format_package_id(&pkg.package_id)?)?;
97 }
98 seen.insert(pkg);
99 current_target = None;
100 iteration = 0;
101 } else {
102 break;
103 }
104 }
105 }
106 for (name, file) in files {
107 shell::fixed(name, file.fixes)?;
108 }
109
110 for e in last_errors {
111 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
112 }
113
114 Ok(())
115}
116
117fn check(args: &FixitArgs) -> CargoResult<(impl Iterator<Item = CheckOutput>, Option<i32>)> {
118 let cmd = if args.clippy { "clippy" } else { "check" };
119 let command = std::process::Command::new(env!("CARGO"))
120 .args([cmd, "--message-format", "json-diagnostic-rendered-ansi"])
121 .args(args.check_flags.to_flags())
122 .stderr(Stdio::piped())
123 .stdout(Stdio::piped())
124 .output()?;
125
126 let buf = BufReader::new(Cursor::new(command.stdout));
127
128 Ok((
129 buf.lines()
130 .map_while(|l| l.ok())
131 .filter_map(|l| serde_json::from_str(&l).ok()),
132 command.status.code(),
133 ))
134}
135
136#[tracing::instrument(skip_all)]
137#[allow(clippy::type_complexity)]
138fn collect_errors(
139 messages: impl Iterator<Item = CheckOutput>,
140 seen: &HashSet<BuildUnit>,
141) -> (
142 IndexSet<String>,
143 IndexMap<BuildUnit, IndexMap<String, IndexSet<(Suggestion, Option<String>)>>>,
144) {
145 let only = HashSet::new();
146 let mut build_unit_map = IndexMap::new();
147
148 let mut errors = IndexSet::new();
149
150 for message in messages {
151 let Message {
152 build_unit,
153 message: diagnostic,
154 } = match message {
155 CheckOutput::Message(m) => m,
156 CheckOutput::Artifact(a) => {
157 if !seen.contains(&a.build_unit) && !a.fresh {
158 build_unit_map
159 .entry(a.build_unit.clone())
160 .or_insert(IndexMap::new());
161 }
162 continue;
163 }
164 };
165
166 if seen.contains(&build_unit) {
167 trace!("rejecting build unit `{:?}` already seen", build_unit);
168 if let Some(rendered) = diagnostic.rendered {
169 errors.insert(rendered);
170 }
171 continue;
172 }
173
174 let file_map = build_unit_map
175 .entry(build_unit.clone())
176 .or_insert(IndexMap::new());
177
178 let filter = if env::var("__CARGO_FIX_YOLO").is_ok() {
179 rustfix::Filter::Everything
180 } else {
181 rustfix::Filter::MachineApplicableOnly
182 };
183
184 let Some(suggestion) = collect_suggestions(&diagnostic, &only, filter) else {
185 trace!("rejecting as not a MachineApplicable diagnosis: {diagnostic:?}");
186 if let Some(rendered) = diagnostic.rendered {
187 errors.insert(rendered);
188 }
189 continue;
190 };
191
192 let mut file_names = suggestion
193 .solutions
194 .iter()
195 .flat_map(|s| s.replacements.iter())
196 .map(|r| &r.snippet.file_name);
197
198 let Some(file_name) = file_names.next() else {
199 trace!("rejecting as it has no solutions {:?}", suggestion);
200 if let Some(rendered) = diagnostic.rendered {
201 errors.insert(rendered);
202 }
203 continue;
204 };
205
206 if !file_names.all(|f| f == file_name) {
207 trace!("rejecting as it changes multiple files: {:?}", suggestion);
208 if let Some(rendered) = diagnostic.rendered {
209 errors.insert(rendered);
210 }
211 continue;
212 }
213
214 let file_path = Path::new(&file_name);
215 if let Ok(home) = env::var("CARGO_HOME") {
217 if file_path.starts_with(home) {
218 continue;
219 }
220 }
221
222 file_map
223 .entry(file_name.to_owned())
224 .or_insert_with(IndexSet::new)
225 .insert((suggestion, diagnostic.rendered));
226 }
227
228 (errors, build_unit_map)
229}
230
231#[tracing::instrument(skip_all)]
232fn fix_errors(
233 files: &mut IndexMap<String, File>,
234 file_map: IndexMap<String, IndexSet<(Suggestion, Option<String>)>>,
235 errors: &mut IndexSet<String>,
236) -> CargoResult<bool> {
237 let mut made_changes = false;
238 for (file, suggestions) in file_map {
239 let code = match paths::read(file.as_ref()) {
240 Ok(s) => s,
241 Err(e) => {
242 warn!("failed to read `{}`: {}", file, e);
243 errors.extend(suggestions.iter().filter_map(|(_, e)| e.clone()));
244 continue;
245 }
246 };
247
248 let mut fixed = CodeFix::new(&code);
249 let mut num_fixes = 0;
250
251 for (suggestion, rendered) in suggestions.iter().rev() {
252 match fixed.apply(suggestion) {
253 Ok(()) => num_fixes += 1,
254 Err(rustfix::Error::AlreadyReplaced {
255 is_identical: true, ..
256 }) => {}
257 Err(e) => {
258 if let Some(rendered) = rendered {
259 errors.insert(rendered.to_owned());
260 }
261 warn!("{e:?}");
262 }
263 }
264 }
265 if fixed.modified() {
266 let new_code = fixed.finish()?;
267 paths::write(&file, new_code)?;
268 made_changes = true;
269 files.entry(file).or_default().fixes += num_fixes;
270 }
271 }
272
273 Ok(made_changes)
274}