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<String, File> = 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 = IndexMap::new();
59 let mut current_target: Option<BuildUnit> = None;
60 let mut seen = HashSet::new();
61
62 loop {
63 trace!("iteration={iteration}");
64 trace!("current_target={current_target:?}");
65 let (messages, _exit_code) = check(&args)?;
66
67 let (mut errors, build_unit_map) = collect_errors(messages, &seen);
68
69 if iteration >= max_iterations {
70 if let Some(target) = current_target {
71 if seen.iter().all(|b| b.package_id != target.package_id) {
72 shell::status("Checking", format_package_id(&target.package_id)?)?;
73 }
74
75 for (name, file) in files {
76 shell::fixed(name, file.fixes)?;
77 }
78 files = IndexMap::new();
79
80 let mut errors = errors.shift_remove(&target).unwrap_or_else(IndexSet::new);
81
82 if let Some(e) = build_unit_map.get(&target) {
83 for (_, e) in e.iter().flat_map(|(_, s)| s) {
84 let Some(e) = e else {
85 continue;
86 };
87 errors.insert(e.to_owned());
88 }
89 }
90 for e in errors {
91 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
92 }
93
94 seen.insert(target);
95 current_target = None;
96 iteration = 0;
97 } else {
98 break;
99 }
100 }
101
102 let mut made_changes = false;
103
104 for (build_unit, file_map) in build_unit_map {
105 if seen.contains(&build_unit) {
106 continue;
107 }
108
109 let build_unit_errors = errors
110 .entry(build_unit.clone())
111 .or_insert_with(IndexSet::new);
112
113 if current_target.is_none() && file_map.is_empty() {
114 if seen.iter().all(|b| b.package_id != build_unit.package_id) {
115 shell::status("Checking", format_package_id(&build_unit.package_id)?)?;
116 }
117 for e in build_unit_errors.iter() {
118 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
119 }
120 errors.shift_remove(&build_unit);
121
122 seen.insert(build_unit);
123 } else if !file_map.is_empty()
124 && current_target.get_or_insert(build_unit.clone()) == &build_unit
125 && fix_errors(&mut files, file_map, build_unit_errors)?
126 {
127 made_changes = true;
128 break;
129 }
130 }
131
132 trace!("made_changes={made_changes:?}");
133 trace!("current_target={current_target:?}");
134
135 last_errors = errors;
136 iteration += 1;
137
138 if !made_changes {
139 if let Some(pkg) = current_target {
140 if seen.iter().all(|b| b.package_id != pkg.package_id) {
141 shell::status("Checking", format_package_id(&pkg.package_id)?)?;
142 }
143
144 for (name, file) in files {
145 shell::fixed(name, file.fixes)?;
146 }
147 files = IndexMap::new();
148
149 let errors = last_errors.shift_remove(&pkg).unwrap_or_else(IndexSet::new);
150 for e in errors {
151 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
152 }
153
154 seen.insert(pkg);
155 current_target = None;
156 iteration = 0;
157 } else {
158 break;
159 }
160 }
161 }
162
163 for (name, file) in files {
164 shell::fixed(name, file.fixes)?;
165 }
166
167 for e in last_errors.iter().flat_map(|(_, e)| e) {
168 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
169 }
170
171 Ok(())
172}
173
174fn check(args: &FixitArgs) -> CargoResult<(impl Iterator<Item = CheckOutput>, Option<i32>)> {
175 let cmd = if args.clippy { "clippy" } else { "check" };
176 let command = std::process::Command::new(env!("CARGO"))
177 .args([cmd, "--message-format", "json-diagnostic-rendered-ansi"])
178 .args(args.check_flags.to_flags())
179 .env("RUSTFLAGS", "--cap-lints=warn")
181 .stderr(Stdio::piped())
182 .stdout(Stdio::piped())
183 .output()?;
184
185 let buf = BufReader::new(Cursor::new(command.stdout));
186
187 Ok((
188 buf.lines()
189 .map_while(|l| l.ok())
190 .filter_map(|l| serde_json::from_str(&l).ok()),
191 command.status.code(),
192 ))
193}
194
195#[tracing::instrument(skip_all)]
196#[allow(clippy::type_complexity)]
197fn collect_errors(
198 messages: impl Iterator<Item = CheckOutput>,
199 seen: &HashSet<BuildUnit>,
200) -> (
201 IndexMap<BuildUnit, IndexSet<String>>,
202 IndexMap<BuildUnit, IndexMap<String, IndexSet<(Suggestion, Option<String>)>>>,
203) {
204 let only = HashSet::new();
205 let mut build_unit_map = IndexMap::new();
206
207 let mut errors = IndexMap::new();
208
209 for message in messages {
210 let Message {
211 build_unit,
212 message: diagnostic,
213 } = match message {
214 CheckOutput::Message(m) => m,
215 CheckOutput::Artifact(a) => {
216 if !seen.contains(&a.build_unit) && !a.fresh {
217 build_unit_map
218 .entry(a.build_unit.clone())
219 .or_insert(IndexMap::new());
220 }
221 continue;
222 }
223 };
224
225 let errors = errors
226 .entry(build_unit.clone())
227 .or_insert_with(IndexSet::new);
228
229 if seen.contains(&build_unit) {
230 trace!("rejecting build unit `{:?}` already seen", build_unit);
231 continue;
232 }
233
234 let file_map = build_unit_map
235 .entry(build_unit.clone())
236 .or_insert(IndexMap::new());
237
238 let filter = if env::var("__CARGO_FIX_YOLO").is_ok() {
239 rustfix::Filter::Everything
240 } else {
241 rustfix::Filter::MachineApplicableOnly
242 };
243
244 let Some(suggestion) = collect_suggestions(&diagnostic, &only, filter) else {
245 trace!("rejecting as not a MachineApplicable diagnosis: {diagnostic:?}");
246 if let Some(rendered) = diagnostic.rendered {
247 errors.insert(rendered);
248 }
249 continue;
250 };
251
252 let mut file_names = suggestion
253 .solutions
254 .iter()
255 .flat_map(|s| s.replacements.iter())
256 .map(|r| &r.snippet.file_name);
257
258 let Some(file_name) = file_names.next() else {
259 trace!("rejecting as it has no solutions {:?}", suggestion);
260 if let Some(rendered) = diagnostic.rendered {
261 errors.insert(rendered);
262 }
263 continue;
264 };
265
266 if !file_names.all(|f| f == file_name) {
267 trace!("rejecting as it changes multiple files: {:?}", suggestion);
268 if let Some(rendered) = diagnostic.rendered {
269 errors.insert(rendered);
270 }
271 continue;
272 }
273
274 let file_path = Path::new(&file_name);
275 if let Ok(home) = env::var("CARGO_HOME") {
277 if file_path.starts_with(home) {
278 continue;
279 }
280 }
281
282 file_map
283 .entry(file_name.to_owned())
284 .or_insert_with(IndexSet::new)
285 .insert((suggestion, diagnostic.rendered));
286 }
287
288 (errors, build_unit_map)
289}
290
291#[tracing::instrument(skip_all)]
292fn fix_errors(
293 files: &mut IndexMap<String, File>,
294 file_map: IndexMap<String, IndexSet<(Suggestion, Option<String>)>>,
295 errors: &mut IndexSet<String>,
296) -> CargoResult<bool> {
297 let mut made_changes = false;
298 for (file, suggestions) in file_map {
299 let code = match paths::read(file.as_ref()) {
300 Ok(s) => s,
301 Err(e) => {
302 warn!("failed to read `{}`: {}", file, e);
303 errors.extend(suggestions.iter().filter_map(|(_, e)| e.clone()));
304 continue;
305 }
306 };
307
308 let mut fixed = CodeFix::new(&code);
309 let mut num_fixes = 0;
310
311 for (suggestion, rendered) in suggestions.iter().rev() {
312 match fixed.apply(suggestion) {
313 Ok(()) => num_fixes += 1,
314 Err(rustfix::Error::AlreadyReplaced {
315 is_identical: true, ..
316 }) => {}
317 Err(e) => {
318 if let Some(rendered) = rendered {
319 errors.insert(rendered.to_owned());
320 }
321 warn!("{e:?}");
322 }
323 }
324 }
325 if fixed.modified() {
326 let new_code = fixed.finish()?;
327 paths::write(&file, new_code)?;
328 made_changes = true;
329 files.entry(file).or_default().fixes += num_fixes;
330 }
331 }
332
333 Ok(made_changes)
334}