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, sysroot::get_sysroot},
17 ops::check::{BuildUnit, CheckOutput, Message},
18 util::{
19 cli::CheckFlags, messages::gen_please_report_this_bug_text, package::format_package_id,
20 vcs::VcsOpts,
21 },
22 CargoResult,
23};
24
25#[derive(Debug, Parser)]
26pub struct FixitArgs {
27 #[arg(long)]
29 clippy: bool,
30
31 #[arg(long)]
33 broken_code: bool,
34
35 #[command(flatten)]
36 color: colorchoice_clap::Color,
37
38 #[command(flatten)]
39 vcs_opts: VcsOpts,
40
41 #[command(flatten)]
42 check_flags: CheckFlags,
43}
44
45impl FixitArgs {
46 pub fn exec(self) -> CargoResult<()> {
47 exec(self)
48 }
49}
50
51#[derive(Debug, Default)]
52struct File {
53 fixes: u32,
54 original_source: String,
55}
56
57#[tracing::instrument(skip_all)]
58fn exec(args: FixitArgs) -> CargoResult<()> {
59 args.color.write_global();
60
61 args.vcs_opts.valid_vcs()?;
62
63 let mut files: IndexMap<String, File> = IndexMap::new();
64
65 let max_iterations: usize = env::var("CARGO_FIX_MAX_RETRIES")
66 .ok()
67 .and_then(|i| i.parse().ok())
68 .unwrap_or(4);
69 let mut iteration = 0;
70
71 let mut last_errors = IndexMap::new();
72 let mut current_target: Option<BuildUnit> = None;
73 let mut seen = HashSet::new();
74
75 loop {
76 trace!("iteration={iteration}");
77 trace!("current_target={current_target:?}");
78 let (messages, exit_code) = check(&args)?;
79
80 if !args.broken_code && exit_code != Some(0) {
81 let mut out = String::new();
82
83 if current_target.is_some() {
84 out.push_str(
85 "failed to automatically apply fixes suggested by rustc\n\n\
86 after fixes were automatically applied the \
87 compiler reported errors within these files:\n\n",
88 );
89
90 for (
91 file,
92 File {
93 fixes: _,
94 original_source,
95 },
96 ) in &files
97 {
98 out.push_str(&format!(" * {file}\n"));
99 shell::note(format!("reverting `{file}` to its original state"))?;
100 paths::write(file, original_source)?;
101 }
102 out.push('\n');
103
104 out.push_str(&gen_please_report_this_bug_text(args.clippy));
105
106 let mut errors = messages
107 .filter_map(|e| match e {
108 CheckOutput::Message(m) => m.message.rendered,
109 _ => None,
110 })
111 .peekable();
112 if errors.peek().is_some() {
113 out.push_str("The errors reported are:\n");
114 }
115
116 for e in errors {
117 out.push_str(&format!("{}\n\n", e.trim_end()));
118 }
119
120 let (messages, _) = check(&args)?;
121 let mut errors = messages
122 .filter_map(|e| match e {
123 CheckOutput::Message(m) => m.message.rendered,
124 _ => None,
125 })
126 .peekable();
127
128 if errors.peek().is_some() {
129 out.push_str("The original errors are:\n");
130 }
131
132 for e in errors {
133 out.push_str(&format!("{}\n\n", e.trim_end()));
134 }
135
136 shell::warn(out)?;
137 } else {
138 for e in messages.filter_map(|e| match e {
139 CheckOutput::Message(m) => m.message.rendered,
140 _ => None,
141 }) {
142 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
143 }
144 }
145
146 shell::note("try using `--broken-code` to fix errors")?;
147 anyhow::bail!("could not compile");
148 }
149
150 let (mut errors, build_unit_map) = collect_errors(messages, &seen);
151
152 if iteration >= max_iterations {
153 if let Some(target) = current_target {
154 if seen.iter().all(|b| b.package_id != target.package_id) {
155 shell::status("Checking", format_package_id(&target.package_id)?)?;
156 }
157
158 for (name, file) in files {
159 shell::fixed(name, file.fixes)?;
160 }
161 files = IndexMap::new();
162
163 let mut errors = errors.shift_remove(&target).unwrap_or_else(IndexSet::new);
164
165 if let Some(e) = build_unit_map.get(&target) {
166 for (_, e) in e.iter().flat_map(|(_, s)| s) {
167 let Some(e) = e else {
168 continue;
169 };
170 errors.insert(e.to_owned());
171 }
172 }
173 for e in errors {
174 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
175 }
176
177 seen.insert(target);
178 current_target = None;
179 iteration = 0;
180 } else {
181 break;
182 }
183 }
184
185 let mut made_changes = false;
186
187 for (build_unit, file_map) in build_unit_map {
188 if seen.contains(&build_unit) {
189 continue;
190 }
191
192 let build_unit_errors = errors
193 .entry(build_unit.clone())
194 .or_insert_with(IndexSet::new);
195
196 if current_target.is_none() && file_map.is_empty() {
197 if seen.iter().all(|b| b.package_id != build_unit.package_id) {
198 shell::status("Checking", format_package_id(&build_unit.package_id)?)?;
199 }
200 for e in build_unit_errors.iter() {
201 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
202 }
203 errors.shift_remove(&build_unit);
204
205 seen.insert(build_unit);
206 } else if !file_map.is_empty()
207 && current_target.get_or_insert(build_unit.clone()) == &build_unit
208 && fix_errors(&mut files, file_map, build_unit_errors)?
209 {
210 made_changes = true;
211 break;
212 }
213 }
214
215 trace!("made_changes={made_changes:?}");
216 trace!("current_target={current_target:?}");
217
218 last_errors = errors;
219 iteration += 1;
220
221 if !made_changes {
222 if let Some(pkg) = current_target {
223 if seen.iter().all(|b| b.package_id != pkg.package_id) {
224 shell::status("Checking", format_package_id(&pkg.package_id)?)?;
225 }
226
227 for (name, file) in files {
228 shell::fixed(name, file.fixes)?;
229 }
230 files = IndexMap::new();
231
232 let errors = last_errors.shift_remove(&pkg).unwrap_or_else(IndexSet::new);
233 for e in errors {
234 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
235 }
236
237 seen.insert(pkg);
238 current_target = None;
239 iteration = 0;
240 } else {
241 break;
242 }
243 }
244 }
245
246 for (name, file) in files {
247 shell::fixed(name, file.fixes)?;
248 }
249
250 for e in last_errors.iter().flat_map(|(_, e)| e) {
251 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
252 }
253
254 Ok(())
255}
256
257fn check(args: &FixitArgs) -> CargoResult<(impl Iterator<Item = CheckOutput>, Option<i32>)> {
258 let cmd = if args.clippy { "clippy" } else { "check" };
259 let command = std::process::Command::new(env!("CARGO"))
260 .args([cmd, "--message-format", "json-diagnostic-rendered-ansi"])
261 .args(args.check_flags.to_flags())
262 .env(
264 "RUSTFLAGS",
265 format!(
266 "--cap-lints=warn {}",
267 env::var("RUSTFLAGS").unwrap_or("".to_owned())
268 ),
269 )
270 .stderr(Stdio::piped())
271 .stdout(Stdio::piped())
272 .output()?;
273
274 let buf = BufReader::new(Cursor::new(command.stdout));
275
276 Ok((
277 buf.lines()
278 .map_while(|l| l.ok())
279 .filter_map(|l| serde_json::from_str(&l).ok()),
280 command.status.code(),
281 ))
282}
283
284#[tracing::instrument(skip_all)]
285#[allow(clippy::type_complexity)]
286fn collect_errors(
287 messages: impl Iterator<Item = CheckOutput>,
288 seen: &HashSet<BuildUnit>,
289) -> (
290 IndexMap<BuildUnit, IndexSet<String>>,
291 IndexMap<BuildUnit, IndexMap<String, IndexSet<(Suggestion, Option<String>)>>>,
292) {
293 let only = HashSet::new();
294 let mut build_unit_map = IndexMap::new();
295
296 let mut errors = IndexMap::new();
297
298 for message in messages {
299 let Message {
300 build_unit,
301 message: diagnostic,
302 } = match message {
303 CheckOutput::Message(m) => m,
304 CheckOutput::Artifact(a) => {
305 if !seen.contains(&a.build_unit) && !a.fresh {
306 build_unit_map
307 .entry(a.build_unit.clone())
308 .or_insert(IndexMap::new());
309 }
310 continue;
311 }
312 };
313
314 let errors = errors
315 .entry(build_unit.clone())
316 .or_insert_with(IndexSet::new);
317
318 if seen.contains(&build_unit) {
319 trace!("rejecting build unit `{:?}` already seen", build_unit);
320 continue;
321 }
322
323 let file_map = build_unit_map
324 .entry(build_unit.clone())
325 .or_insert(IndexMap::new());
326
327 let filter = if env::var("__CARGO_FIX_YOLO").is_ok() {
328 rustfix::Filter::Everything
329 } else {
330 rustfix::Filter::MachineApplicableOnly
331 };
332
333 let Some(suggestion) = collect_suggestions(&diagnostic, &only, filter) else {
334 trace!("rejecting as not a MachineApplicable diagnosis: {diagnostic:?}");
335 if let Some(rendered) = diagnostic.rendered {
336 errors.insert(rendered);
337 }
338 continue;
339 };
340
341 let mut file_names = suggestion
342 .solutions
343 .iter()
344 .flat_map(|s| s.replacements.iter())
345 .map(|r| &r.snippet.file_name);
346
347 let Some(file_name) = file_names.next() else {
348 trace!("rejecting as it has no solutions {:?}", suggestion);
349 if let Some(rendered) = diagnostic.rendered {
350 errors.insert(rendered);
351 }
352 continue;
353 };
354
355 if !file_names.all(|f| f == file_name) {
356 trace!("rejecting as it changes multiple files: {:?}", suggestion);
357 if let Some(rendered) = diagnostic.rendered {
358 errors.insert(rendered);
359 }
360 continue;
361 }
362
363 let file_path = Path::new(&file_name);
364 if let Ok(home) = env::var("CARGO_HOME") {
366 if file_path.starts_with(home) {
367 if let Some(rendered) = diagnostic.rendered {
368 errors.insert(rendered);
369 }
370 continue;
371 }
372 }
373
374 if let Some(sysroot) = get_sysroot() {
375 if file_path.starts_with(sysroot) {
376 if let Some(rendered) = diagnostic.rendered {
377 errors.insert(rendered);
378 }
379 continue;
380 }
381 }
382
383 file_map
384 .entry(file_name.to_owned())
385 .or_insert_with(IndexSet::new)
386 .insert((suggestion, diagnostic.rendered));
387 }
388
389 (errors, build_unit_map)
390}
391
392#[tracing::instrument(skip_all)]
393fn fix_errors(
394 files: &mut IndexMap<String, File>,
395 file_map: IndexMap<String, IndexSet<(Suggestion, Option<String>)>>,
396 errors: &mut IndexSet<String>,
397) -> CargoResult<bool> {
398 let mut made_changes = false;
399 for (file, suggestions) in file_map {
400 let source = match paths::read(file.as_ref()) {
401 Ok(s) => s,
402 Err(e) => {
403 warn!("failed to read `{}`: {}", file, e);
404 errors.extend(suggestions.iter().filter_map(|(_, e)| e.clone()));
405 continue;
406 }
407 };
408
409 let mut fixed = CodeFix::new(&source);
410 let mut num_fixes = 0;
411
412 for (suggestion, rendered) in suggestions.iter().rev() {
413 match fixed.apply(suggestion) {
414 Ok(()) => num_fixes += 1,
415 Err(rustfix::Error::AlreadyReplaced {
416 is_identical: true, ..
417 }) => {}
418 Err(e) => {
419 if let Some(rendered) = rendered {
420 errors.insert(rendered.to_owned());
421 }
422 warn!("{e:?}");
423 }
424 }
425 }
426 if fixed.modified() {
427 let new_source = fixed.finish()?;
428 paths::write(&file, new_source)?;
429 made_changes = true;
430 files
431 .entry(file)
432 .or_insert(File {
433 fixes: 0,
434 original_source: source,
435 })
436 .fixes += num_fixes;
437 }
438 }
439
440 Ok(made_changes)
441}