cargo_fixit/ops/
fixit.rs

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    /// Run `clippy` instead of `check`
28    #[arg(long)]
29    clippy: bool,
30
31    /// Fix code even if it already has compiler errors
32    #[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        // This allows `cargo fix` to work even if the crate has #[deny(warnings)].
258        .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        // Do not write into registry cache. See rust-lang/cargo#9857.
354        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}