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, DiagnosticLevel, Message, MessageDiagnostic},
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 let mut lint_cap = false;
71
72 let mut last_errors = IndexMap::new();
73 let mut current_target: Option<BuildUnit> = None;
74 let mut seen = HashSet::new();
75
76 loop {
77 trace!("iteration={iteration}");
78 trace!("current_target={current_target:?}");
79 let (messages, exit_code) = check(&args, &mut lint_cap)?;
80
81 if !args.broken_code && exit_code != Some(0) {
82 let mut out = String::new();
83
84 if current_target.is_some() {
85 out.push_str(
86 "failed to automatically apply fixes suggested by rustc\n\n\
87 after fixes were automatically applied the \
88 compiler reported errors within these files:\n\n",
89 );
90
91 for (
92 file,
93 File {
94 fixes: _,
95 original_source,
96 },
97 ) in &files
98 {
99 out.push_str(&format!(" * {file}\n"));
100 shell::note(format!("reverting `{file}` to its original state"))?;
101 paths::write(file, original_source)?;
102 }
103 out.push('\n');
104
105 out.push_str(&gen_please_report_this_bug_text(args.clippy));
106
107 let mut errors = messages
108 .into_iter()
109 .filter_map(|e| match e {
110 CheckOutput::Message(m) => m.message.diagnostic.rendered,
111 _ => None,
112 })
113 .peekable();
114 if errors.peek().is_some() {
115 out.push_str("The errors reported are:\n");
116 }
117
118 for e in errors {
119 out.push_str(&format!("{}\n\n", e.trim_end()));
120 }
121
122 let (messages, _) = check(&args, &mut lint_cap)?;
123 let mut errors = messages
124 .into_iter()
125 .filter_map(|e| match e {
126 CheckOutput::Message(m) => m.message.diagnostic.rendered,
127 _ => None,
128 })
129 .peekable();
130
131 if errors.peek().is_some() {
132 out.push_str("The original errors are:\n");
133 }
134
135 for e in errors {
136 out.push_str(&format!("{}\n\n", e.trim_end()));
137 }
138
139 shell::warn(out)?;
140 } else {
141 for e in messages.into_iter().filter_map(|e| match e {
142 CheckOutput::Message(m) => m.message.diagnostic.rendered,
143 _ => None,
144 }) {
145 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
146 }
147 }
148
149 shell::note("try using `--broken-code` to fix errors")?;
150 anyhow::bail!("could not compile");
151 }
152
153 let (mut errors, mut build_unit_map) = collect_errors(messages.into_iter(), &seen);
154
155 if iteration >= max_iterations {
156 if let Some(target) = current_target {
157 if let Some(file_map) = build_unit_map.get(&target) {
158 let target_errors = errors.entry(target.clone()).or_default();
159 target_errors.extend(
160 file_map
161 .values()
162 .flatten()
163 .filter_map(|(_, diagnostic)| diagnostic.clone()),
164 );
165 }
166 finish_target(target, &mut files, &mut errors, &mut seen)?;
167 current_target = None;
168 iteration = 0;
169 } else {
170 break;
171 }
172 }
173
174 let mut finalized_target = false;
175 if let Some(target) = current_target.as_ref() {
176 if build_unit_map.get(target).is_none_or(IndexMap::is_empty) {
177 let target = current_target.take().expect("current target is present");
178 build_unit_map.shift_remove(&target);
179 finish_target(target, &mut files, &mut errors, &mut seen)?;
180 iteration = 0;
181 finalized_target = true;
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 finalized_target && build_unit_errors.is_empty() {
198 continue;
199 }
200 if seen.iter().all(|b| b.package_id != build_unit.package_id) {
201 shell::status("Checking", format_package_id(&build_unit.package_id)?)?;
202 }
203 for e in build_unit_errors.iter() {
204 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
205 }
206 errors.shift_remove(&build_unit);
207
208 seen.insert(build_unit);
209 } else if !file_map.is_empty()
210 && current_target.get_or_insert(build_unit.clone()) == &build_unit
211 && fix_errors(&mut files, file_map, build_unit_errors)?
212 {
213 made_changes = true;
214 break;
215 }
216 }
217
218 trace!("made_changes={made_changes:?}");
219 trace!("current_target={current_target:?}");
220
221 last_errors = errors;
222 iteration += 1;
223
224 if !made_changes {
225 if let Some(pkg) = current_target {
226 finish_target(pkg, &mut files, &mut last_errors, &mut seen)?;
227 current_target = None;
228 iteration = 0;
229 continue;
230 }
231 break;
232 }
233 }
234
235 for (name, file) in files {
236 shell::fixed(name, file.fixes)?;
237 }
238
239 for e in last_errors.iter().flat_map(|(_, e)| e) {
240 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
241 }
242
243 Ok(())
244}
245
246fn finish_target(
248 target: BuildUnit,
249 files: &mut IndexMap<String, File>,
250 errors: &mut IndexMap<BuildUnit, IndexSet<String>>,
251 seen: &mut HashSet<BuildUnit>,
252) -> CargoResult<()> {
253 if seen
254 .iter()
255 .all(|build_unit| build_unit.package_id != target.package_id)
256 {
257 shell::status("Checking", format_package_id(&target.package_id)?)?;
258 }
259
260 for (name, file) in std::mem::take(files) {
261 shell::fixed(name, file.fixes)?;
262 }
263
264 for error in errors.shift_remove(&target).unwrap_or_default() {
265 shell::print_ansi_stderr(format!("{}\n\n", error.trim_end()).as_bytes())?;
266 }
267
268 seen.insert(target);
269 Ok(())
270}
271
272fn check(args: &FixitArgs, lint_cap: &mut bool) -> CargoResult<(Vec<CheckOutput>, Option<i32>)> {
273 let cmd = if args.clippy { "clippy" } else { "check" };
274 let mut command = std::process::Command::new(env!("CARGO"));
275 command
276 .args([cmd, "--message-format", "json-diagnostic-rendered-ansi"])
277 .args(args.check_flags.to_flags())
278 .stderr(Stdio::piped())
279 .stdout(Stdio::piped());
280 if *lint_cap {
281 cap_lints(&mut command);
282 }
283 let output = command.output()?;
284 let mut output = to_check_output(output);
285
286 if output.1 != Some(0) && !*lint_cap && denied_lint(&output.0) {
287 *lint_cap = true;
288 cap_lints(&mut command);
289 output = to_check_output(command.output()?);
290 }
291
292 Ok(output)
293}
294
295fn cap_lints(command: &mut std::process::Command) {
297 if let Ok(flags) = env::var("CARGO_ENCODED_RUSTFLAGS") {
298 let separator = if flags.is_empty() { "" } else { "\u{1f}" };
299 command.env(
300 "CARGO_ENCODED_RUSTFLAGS",
301 format!("{flags}{separator}--cap-lints=warn"),
302 );
303 } else {
304 command.env(
305 "RUSTFLAGS",
306 format!(
307 "--cap-lints=warn {}",
308 env::var("RUSTFLAGS").unwrap_or("".to_owned())
309 ),
310 );
311 }
312}
313
314fn denied_lint(messages: &[CheckOutput]) -> bool {
315 messages.iter().any(|message| {
316 matches!(&message, CheckOutput::Message(message)
317 if message.message.level == DiagnosticLevel::Error
318 && message.message.diagnostic.code.is_some())
319 })
320}
321
322fn to_check_output(output: std::process::Output) -> (Vec<CheckOutput>, Option<i32>) {
323 let buf = BufReader::new(Cursor::new(output.stdout));
324 (
325 buf.lines()
326 .map_while(|l| l.ok())
327 .filter_map(|l| serde_json::from_str(&l).ok())
328 .collect(),
329 output.status.code(),
330 )
331}
332
333#[tracing::instrument(skip_all)]
334#[allow(clippy::type_complexity)]
335fn collect_errors(
336 messages: impl Iterator<Item = CheckOutput>,
337 seen: &HashSet<BuildUnit>,
338) -> (
339 IndexMap<BuildUnit, IndexSet<String>>,
340 IndexMap<BuildUnit, IndexMap<String, IndexSet<(Suggestion, Option<String>)>>>,
341) {
342 let only = HashSet::new();
343 let mut build_unit_map = IndexMap::new();
344
345 let mut errors = IndexMap::new();
346
347 for message in messages {
348 let Message {
349 build_unit,
350 message: MessageDiagnostic { diagnostic, .. },
351 } = match message {
352 CheckOutput::Message(m) => m,
353 CheckOutput::Artifact(a) => {
354 if !seen.contains(&a.build_unit) && !a.fresh {
355 build_unit_map
356 .entry(a.build_unit.clone())
357 .or_insert(IndexMap::new());
358 }
359 continue;
360 }
361 };
362
363 let errors = errors
364 .entry(build_unit.clone())
365 .or_insert_with(IndexSet::new);
366
367 if seen.contains(&build_unit) {
368 trace!("rejecting build unit `{:?}` already seen", build_unit);
369 continue;
370 }
371
372 let file_map = build_unit_map
373 .entry(build_unit.clone())
374 .or_insert(IndexMap::new());
375
376 let filter = if env::var("__CARGO_FIX_YOLO").is_ok() {
377 rustfix::Filter::Everything
378 } else {
379 rustfix::Filter::MachineApplicableOnly
380 };
381
382 let Some(suggestion) = collect_suggestions(&diagnostic, &only, filter) else {
383 trace!("rejecting as not a MachineApplicable diagnosis: {diagnostic:?}");
384 if let Some(rendered) = diagnostic.rendered {
385 errors.insert(rendered);
386 }
387 continue;
388 };
389
390 let mut file_names = suggestion
391 .solutions
392 .iter()
393 .flat_map(|s| s.replacements.iter())
394 .map(|r| &r.snippet.file_name);
395
396 let Some(file_name) = file_names.next() else {
397 trace!("rejecting as it has no solutions {:?}", suggestion);
398 if let Some(rendered) = diagnostic.rendered {
399 errors.insert(rendered);
400 }
401 continue;
402 };
403
404 if !file_names.all(|f| f == file_name) {
405 trace!("rejecting as it changes multiple files: {:?}", suggestion);
406 if let Some(rendered) = diagnostic.rendered {
407 errors.insert(rendered);
408 }
409 continue;
410 }
411
412 let file_path = Path::new(&file_name);
413 if let Ok(home) = env::var("CARGO_HOME") {
415 if file_path.starts_with(home) {
416 if let Some(rendered) = diagnostic.rendered {
417 errors.insert(rendered);
418 }
419 continue;
420 }
421 }
422
423 if file_path.is_absolute() {
424 if let Some(sysroot) = get_sysroot() {
425 if file_path.starts_with(sysroot) {
426 if let Some(rendered) = diagnostic.rendered {
427 errors.insert(rendered);
428 }
429 continue;
430 }
431 }
432 }
433
434 file_map
435 .entry(file_name.to_owned())
436 .or_insert_with(IndexSet::new)
437 .insert((suggestion, diagnostic.rendered));
438 }
439
440 (errors, build_unit_map)
441}
442
443#[tracing::instrument(skip_all)]
444fn fix_errors(
445 files: &mut IndexMap<String, File>,
446 file_map: IndexMap<String, IndexSet<(Suggestion, Option<String>)>>,
447 errors: &mut IndexSet<String>,
448) -> CargoResult<bool> {
449 let mut made_changes = false;
450 for (file, suggestions) in file_map {
451 let source = match paths::read(file.as_ref()) {
452 Ok(s) => s,
453 Err(e) => {
454 warn!("failed to read `{}`: {}", file, e);
455 errors.extend(suggestions.iter().filter_map(|(_, e)| e.clone()));
456 continue;
457 }
458 };
459
460 let mut fixed = CodeFix::new(&source);
461 let mut num_fixes = 0;
462
463 for (suggestion, rendered) in suggestions.iter().rev() {
464 match fixed.apply(suggestion) {
465 Ok(()) => num_fixes += 1,
466 Err(rustfix::Error::AlreadyReplaced {
467 is_identical: true, ..
468 }) => {}
469 Err(e) => {
470 if let Some(rendered) = rendered {
471 errors.insert(rendered.to_owned());
472 }
473 warn!("{e:?}");
474 }
475 }
476 }
477 if fixed.modified() {
478 let new_source = fixed.finish()?;
479 paths::write(&file, new_source)?;
480 made_changes = true;
481 files
482 .entry(file)
483 .or_insert(File {
484 fixes: 0,
485 original_source: source,
486 })
487 .fixes += num_fixes;
488 }
489 }
490
491 Ok(made_changes)
492}