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