Skip to main content

cargo_fixit/ops/
fixit.rs

1use std::collections::HashMap;
2use std::collections::HashSet;
3use std::env;
4use std::io::BufRead;
5use std::io::BufReader;
6use std::io::Cursor;
7use std::path::Path;
8use std::process::Command;
9use std::process::Stdio;
10
11use cargo_metadata::MetadataCommand;
12use cargo_util::paths;
13use clap::Parser;
14use indexmap::{IndexMap, IndexSet};
15use rustfix::{collect_suggestions, CodeFix, Suggestion};
16use tracing::{trace, warn};
17
18use crate::{
19    core::{shell, sysroot::get_sysroot},
20    ops::check::{BuildUnit, CheckOutput, DiagnosticLevel, Message, MessageDiagnostic},
21    util::{
22        cli::CheckFlags, messages::gen_please_report_this_bug_text, package::format_package_id,
23        vcs::VcsOpts,
24    },
25    CargoResult,
26};
27
28#[derive(Debug, Parser)]
29pub struct FixitArgs {
30    /// Run `clippy` instead of `check`
31    #[arg(long)]
32    clippy: bool,
33
34    /// Fix code even if it already has compiler errors
35    #[arg(long)]
36    broken_code: bool,
37
38    /// Fix all targets together, risking stale suggestions
39    #[arg(long = "Zdangerous-parallel-fixes")]
40    dangerous_parallel_fixes: bool,
41
42    #[command(flatten)]
43    color: colorchoice_clap::Color,
44
45    #[command(flatten)]
46    vcs_opts: VcsOpts,
47
48    #[command(flatten)]
49    check_flags: CheckFlags,
50}
51
52impl FixitArgs {
53    pub fn exec(self) -> CargoResult<()> {
54        exec(self)
55    }
56}
57
58#[derive(Debug, Default)]
59struct File {
60    fixes: u32,
61    original_source: String,
62}
63
64#[tracing::instrument(skip_all)]
65fn exec(args: FixitArgs) -> CargoResult<()> {
66    args.color.write_global();
67
68    args.vcs_opts.valid_vcs()?;
69
70    let mut active_targets = IndexMap::new();
71    match fix(&args, &mut active_targets) {
72        Ok(()) => Ok(()),
73        Err(error) => {
74            for (file, original) in active_targets.values().flat_map(|files| files.iter()) {
75                paths::write(file, &original.original_source)?;
76            }
77            Err(error)
78        }
79    }
80}
81
82fn fix(
83    args: &FixitArgs,
84    active_targets: &mut IndexMap<BuildUnit, IndexMap<String, File>>,
85) -> CargoResult<()> {
86    let max_iterations: usize = env::var("CARGO_FIX_MAX_RETRIES")
87        .ok()
88        .and_then(|i| i.parse().ok())
89        .unwrap_or(4);
90    let mut iteration = 0;
91    let mut lint_cap = false;
92
93    let mut last_errors = IndexMap::new();
94    let mut claimed_files: HashMap<same_file::Handle, BuildUnit> = HashMap::new();
95    let mut package_graph_cache: Option<Option<PackageGraph>> = None;
96    let mut seen = HashSet::new();
97
98    loop {
99        trace!("iteration={iteration}");
100        trace!("active_targets={active_targets:?}");
101        let (messages, exit_code) = check(args, &mut lint_cap)?;
102
103        if !args.broken_code && exit_code != Some(0) {
104            let mut out = String::new();
105
106            if !active_targets.is_empty() {
107                out.push_str(
108                    "failed to automatically apply fixes suggested by rustc\n\n\
109                    after fixes were automatically applied the \
110                    compiler reported errors within these files:\n\n",
111                );
112
113                for (
114                    file,
115                    File {
116                        fixes: _,
117                        original_source,
118                    },
119                ) in active_targets.values().flat_map(|files| files.iter())
120                {
121                    out.push_str(&format!("  * {file}\n"));
122                    shell::note(format!("reverting `{file}` to its original state"))?;
123                    paths::write(file, original_source)?;
124                }
125                active_targets.clear();
126                out.push('\n');
127
128                out.push_str(&gen_please_report_this_bug_text(args.clippy));
129
130                let mut errors = messages
131                    .into_iter()
132                    .filter_map(|e| match e {
133                        CheckOutput::Message(m) => m.message.diagnostic.rendered,
134                        _ => None,
135                    })
136                    .peekable();
137                if errors.peek().is_some() {
138                    out.push_str("The errors reported are:\n");
139                }
140
141                for e in errors {
142                    out.push_str(&format!("{}\n\n", e.trim_end()));
143                }
144
145                let (messages, _) = check(args, &mut lint_cap)?;
146                let mut errors = messages
147                    .into_iter()
148                    .filter_map(|e| match e {
149                        CheckOutput::Message(m) => m.message.diagnostic.rendered,
150                        _ => None,
151                    })
152                    .peekable();
153
154                if errors.peek().is_some() {
155                    out.push_str("The original errors are:\n");
156                }
157
158                for e in errors {
159                    out.push_str(&format!("{}\n\n", e.trim_end()));
160                }
161
162                shell::warn(out)?;
163            } else {
164                for e in messages.into_iter().filter_map(|e| match e {
165                    CheckOutput::Message(m) => m.message.diagnostic.rendered,
166                    _ => None,
167                }) {
168                    shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
169                }
170            }
171
172            shell::note("try using `--broken-code` to fix errors")?;
173            anyhow::bail!("could not compile");
174        }
175
176        let (mut errors, mut build_unit_map) = collect_errors(messages.into_iter(), &seen);
177
178        if iteration >= max_iterations {
179            if active_targets.is_empty() {
180                break;
181            }
182            let targets: Vec<_> = active_targets.keys().cloned().collect();
183            for target in targets {
184                if let Some(file_map) = build_unit_map.get(&target) {
185                    let target_errors = errors.entry(target.clone()).or_default();
186                    target_errors.extend(
187                        file_map
188                            .values()
189                            .flatten()
190                            .filter_map(|(_, diagnostic)| diagnostic.clone()),
191                    );
192                }
193                finish_target(target, active_targets, &mut errors, &mut seen)?;
194            }
195            claimed_files.clear();
196            iteration = 0;
197        }
198
199        let mut finalized_targets = false;
200        if !active_targets.is_empty()
201            && active_targets
202                .keys()
203                .all(|target| build_unit_map.get(target).is_none_or(IndexMap::is_empty))
204        {
205            let targets: Vec<_> = active_targets.keys().cloned().collect();
206            for target in targets {
207                build_unit_map.shift_remove(&target);
208                finish_target(target, active_targets, &mut errors, &mut seen)?;
209            }
210            debug_assert!(active_targets.is_empty());
211            claimed_files.clear();
212            iteration = 0;
213            finalized_targets = true;
214        }
215
216        let mut made_changes = false;
217        // Admit build units from one compiler snapshot only when their packages are independent.
218        // Once a batch is active, recheck and finish it before considering additional units.
219        let continuing_batch = !active_targets.is_empty();
220
221        for (build_unit, file_map) in build_unit_map {
222            if seen.contains(&build_unit) {
223                continue;
224            }
225
226            let build_unit_errors = errors
227                .entry(build_unit.clone())
228                .or_insert_with(IndexSet::new);
229
230            if active_targets.is_empty() && file_map.is_empty() {
231                if finalized_targets && build_unit_errors.is_empty() {
232                    continue;
233                }
234                if seen.iter().all(|b| b.package_id != build_unit.package_id) {
235                    shell::status("Checking", format_package_id(&build_unit.package_id)?)?;
236                }
237                for e in build_unit_errors.iter() {
238                    shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
239                }
240                errors.shift_remove(&build_unit);
241
242                seen.insert(build_unit);
243            } else if !file_map.is_empty() {
244                let was_active = active_targets.contains_key(&build_unit);
245                if continuing_batch && !was_active {
246                    continue;
247                }
248
249                if !args.dangerous_parallel_fixes && !was_active && !active_targets.is_empty() {
250                    if active_targets
251                        .keys()
252                        .any(|active| active.package_id == build_unit.package_id)
253                    {
254                        continue;
255                    }
256
257                    if package_graph_cache.is_none() {
258                        package_graph_cache = Some(PackageGraph::load(&args.check_flags));
259                    }
260                    let Some(Some(graph)) = package_graph_cache.as_mut() else {
261                        continue;
262                    };
263
264                    let mut independent = true;
265                    for active in active_targets.keys() {
266                        if !graph
267                            .packages_are_independent(&active.package_id, &build_unit.package_id)
268                        {
269                            independent = false;
270                            break;
271                        }
272                    }
273                    if !independent {
274                        continue;
275                    }
276                }
277
278                let handles = file_map
279                    .keys()
280                    .map(same_file::Handle::from_path)
281                    .collect::<Result<Vec<_>, _>>()
282                    .ok();
283                let serialize_target = handles.is_none();
284                if serialize_target && !was_active && !active_targets.is_empty() {
285                    continue;
286                }
287                if handles.as_ref().is_some_and(|handles| {
288                    handles.iter().any(|handle| {
289                        claimed_files
290                            .get(handle)
291                            .is_some_and(|owner| owner != &build_unit)
292                    })
293                }) {
294                    continue;
295                }
296
297                let target_files = active_targets.entry(build_unit.clone()).or_default();
298                let changed = fix_errors(target_files, file_map, build_unit_errors)?;
299                if !changed && !was_active {
300                    active_targets.shift_remove(&build_unit);
301                }
302                if changed {
303                    if let Some(handles) = handles {
304                        for handle in handles {
305                            claimed_files.entry(handle).or_insert(build_unit.clone());
306                        }
307                    }
308                    made_changes = true;
309                    if serialize_target {
310                        break;
311                    }
312                }
313            }
314        }
315
316        trace!("made_changes={made_changes:?}");
317        trace!("active_targets={active_targets:?}");
318
319        last_errors = errors;
320        iteration += 1;
321
322        if !made_changes {
323            if active_targets.is_empty() {
324                break;
325            }
326            let targets: Vec<_> = active_targets.keys().cloned().collect();
327            for target in targets {
328                finish_target(target, active_targets, &mut last_errors, &mut seen)?;
329            }
330            claimed_files.clear();
331            iteration = 0;
332            continue;
333        }
334    }
335
336    for files in active_targets.values() {
337        for (name, file) in files {
338            shell::fixed(name, file.fixes)?;
339        }
340    }
341
342    for e in last_errors.iter().flat_map(|(_, e)| e) {
343        shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
344    }
345
346    active_targets.clear();
347    Ok(())
348}
349
350/// Package dependencies used to batch only transitively unrelated packages.
351#[derive(Debug)]
352struct PackageGraph {
353    dependencies: HashMap<String, Vec<String>>,
354    reachable: HashMap<String, HashSet<String>>,
355}
356
357impl PackageGraph {
358    /// Loads the package graph, returning `None` when batching must remain serial.
359    fn load(flags: &CheckFlags) -> Option<Self> {
360        let mut command = MetadataCommand::new();
361        command.no_deps();
362        command.other_options(flags.to_metadata_flags());
363
364        let metadata = match command.exec() {
365            Ok(metadata) => metadata,
366            Err(error) => {
367                warn!("failed to run `cargo metadata`: {error}");
368                return None;
369            }
370        };
371
372        // A script is identified by its manifest path, not its containing directory.
373        if metadata
374            .packages
375            .iter()
376            .any(|package| package.manifest_path.file_name() != Some("Cargo.toml"))
377        {
378            return Self::load_resolved(flags);
379        }
380
381        let package_ids_by_path: HashMap<_, _> = metadata
382            .packages
383            .iter()
384            .filter_map(|package| {
385                package
386                    .manifest_path
387                    .parent()
388                    .map(|path| (path, package.id.repr.as_str()))
389            })
390            .collect();
391        if package_ids_by_path.len() != metadata.packages.len() {
392            return Self::load_resolved(flags);
393        }
394
395        let package_names: HashSet<_> = metadata
396            .packages
397            .iter()
398            .map(|package| package.name.as_ref())
399            .collect();
400        let mut dependencies = HashMap::with_capacity(metadata.packages.len());
401        for package in &metadata.packages {
402            let mut package_dependencies = Vec::new();
403            for dependency in &package.dependencies {
404                if let Some(path) = &dependency.path {
405                    let Some(dependency_id) = package_ids_by_path.get(path.as_path()) else {
406                        return Self::load_resolved(flags);
407                    };
408                    package_dependencies.push((*dependency_id).to_owned());
409                } else if package_names.contains(dependency.name.as_str()) {
410                    return Self::load_resolved(flags);
411                }
412            }
413            dependencies.insert(package.id.repr.clone(), package_dependencies);
414        }
415
416        Some(Self {
417            dependencies,
418            reachable: HashMap::new(),
419        })
420    }
421
422    /// Resolves external packages when workspace metadata cannot prove independence.
423    fn load_resolved(flags: &CheckFlags) -> Option<Self> {
424        let mut command = MetadataCommand::new();
425        command.other_options(flags.to_metadata_flags());
426
427        let metadata = match command.exec() {
428            Ok(metadata) => metadata,
429            Err(error) => {
430                warn!("failed to run `cargo metadata`: {error}");
431                return None;
432            }
433        };
434        let Some(resolve) = metadata.resolve else {
435            warn!("`cargo metadata` did not return a dependency graph");
436            return None;
437        };
438        let dependencies = resolve
439            .nodes
440            .into_iter()
441            .map(|node| {
442                (
443                    node.id.repr,
444                    node.dependencies
445                        .into_iter()
446                        .map(|dependency| dependency.repr)
447                        .collect(),
448                )
449            })
450            .collect();
451
452        Some(Self {
453            dependencies,
454            reachable: HashMap::new(),
455        })
456    }
457
458    /// Returns whether both packages are known and transitively unrelated.
459    fn packages_are_independent(&mut self, left: &str, right: &str) -> bool {
460        left != right && !self.depends_on(left, right) && !self.depends_on(right, left)
461    }
462
463    /// Returns whether `package` transitively depends on `target`.
464    fn depends_on(&mut self, package: &str, target: &str) -> bool {
465        if !self.reachable.contains_key(package) {
466            let Some(reachable) = self.collect_reachable(package) else {
467                return true;
468            };
469            self.reachable.insert(package.to_owned(), reachable);
470        }
471
472        self.reachable
473            .get(package)
474            .is_none_or(|reachable| reachable.contains(target))
475    }
476
477    /// Collects the packages transitively reachable from `root`.
478    fn collect_reachable(&self, root: &str) -> Option<HashSet<String>> {
479        let mut reachable = HashSet::new();
480        let mut pending = vec![root];
481
482        while let Some(package) = pending.pop() {
483            if !reachable.insert(package.to_owned()) {
484                continue;
485            }
486            let dependencies = self.dependencies.get(package)?;
487            pending.extend(dependencies.iter().map(String::as_str));
488        }
489
490        reachable.remove(root);
491        Some(reachable)
492    }
493}
494
495/// Marks a target complete after reporting its fixes and remaining diagnostics.
496fn finish_target(
497    target: BuildUnit,
498    active_targets: &mut IndexMap<BuildUnit, IndexMap<String, File>>,
499    errors: &mut IndexMap<BuildUnit, IndexSet<String>>,
500    seen: &mut HashSet<BuildUnit>,
501) -> CargoResult<()> {
502    if seen
503        .iter()
504        .all(|build_unit| build_unit.package_id != target.package_id)
505    {
506        shell::status("Checking", format_package_id(&target.package_id)?)?;
507    }
508
509    if let Some(files) = active_targets.get(&target) {
510        for (name, file) in files {
511            shell::fixed(name, file.fixes)?;
512        }
513    }
514
515    for error in errors.get(&target).into_iter().flatten() {
516        shell::print_ansi_stderr(format!("{}\n\n", error.trim_end()).as_bytes())?;
517    }
518
519    active_targets.shift_remove(&target);
520    errors.shift_remove(&target);
521    seen.insert(target);
522    Ok(())
523}
524
525fn check(args: &FixitArgs, lint_cap: &mut bool) -> CargoResult<(Vec<CheckOutput>, Option<i32>)> {
526    let cmd = if args.clippy { "clippy" } else { "check" };
527    let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
528    let mut command = Command::new(cargo);
529    command
530        .args([cmd, "--message-format", "json-diagnostic-rendered-ansi"])
531        .args(args.check_flags.to_flags())
532        .stderr(Stdio::piped())
533        .stdout(Stdio::piped());
534    if *lint_cap {
535        cap_lints(&mut command);
536    }
537    let output = command.output()?;
538    let mut output = to_check_output(output);
539
540    if output.1 != Some(0) && !*lint_cap && denied_lint(&output.0) {
541        *lint_cap = true;
542        cap_lints(&mut command);
543        output = to_check_output(command.output()?);
544    }
545
546    Ok(output)
547}
548
549/// Applies the original lint cap while preserving existing compiler flags.
550fn cap_lints(command: &mut Command) {
551    if let Ok(flags) = env::var("CARGO_ENCODED_RUSTFLAGS") {
552        let separator = if flags.is_empty() { "" } else { "\u{1f}" };
553        command.env(
554            "CARGO_ENCODED_RUSTFLAGS",
555            format!("{flags}{separator}--cap-lints=warn"),
556        );
557    } else {
558        command.env(
559            "RUSTFLAGS",
560            format!(
561                "--cap-lints=warn {}",
562                env::var("RUSTFLAGS").unwrap_or("".to_owned())
563            ),
564        );
565    }
566}
567
568fn denied_lint(messages: &[CheckOutput]) -> bool {
569    messages.iter().any(|message| {
570        matches!(&message, CheckOutput::Message(message)
571                if message.message.level == DiagnosticLevel::Error
572                    && message.message.diagnostic.code.is_some())
573    })
574}
575
576fn to_check_output(output: std::process::Output) -> (Vec<CheckOutput>, Option<i32>) {
577    let buf = BufReader::new(Cursor::new(output.stdout));
578    (
579        buf.lines()
580            .map_while(|l| l.ok())
581            .filter_map(|l| serde_json::from_str(&l).ok())
582            .collect(),
583        output.status.code(),
584    )
585}
586
587#[tracing::instrument(skip_all)]
588#[allow(clippy::type_complexity)]
589fn collect_errors(
590    messages: impl Iterator<Item = CheckOutput>,
591    seen: &HashSet<BuildUnit>,
592) -> (
593    IndexMap<BuildUnit, IndexSet<String>>,
594    IndexMap<BuildUnit, IndexMap<String, IndexSet<(Suggestion, Option<String>)>>>,
595) {
596    let only = HashSet::new();
597    let mut build_unit_map = IndexMap::new();
598
599    let mut errors = IndexMap::new();
600
601    for message in messages {
602        let Message {
603            build_unit,
604            message: MessageDiagnostic { diagnostic, .. },
605        } = match message {
606            CheckOutput::Message(m) => m,
607            CheckOutput::Artifact(a) => {
608                if !seen.contains(&a.build_unit) && !a.fresh {
609                    build_unit_map
610                        .entry(a.build_unit.clone())
611                        .or_insert(IndexMap::new());
612                }
613                continue;
614            }
615        };
616
617        let errors = errors
618            .entry(build_unit.clone())
619            .or_insert_with(IndexSet::new);
620
621        if seen.contains(&build_unit) {
622            trace!("rejecting build unit `{:?}` already seen", build_unit);
623            continue;
624        }
625
626        let file_map = build_unit_map
627            .entry(build_unit.clone())
628            .or_insert(IndexMap::new());
629
630        let filter = if env::var("__CARGO_FIX_YOLO").is_ok() {
631            rustfix::Filter::Everything
632        } else {
633            rustfix::Filter::MachineApplicableOnly
634        };
635
636        let Some(suggestion) = collect_suggestions(&diagnostic, &only, filter) else {
637            trace!("rejecting as not a MachineApplicable diagnosis: {diagnostic:?}");
638            if let Some(rendered) = diagnostic.rendered {
639                errors.insert(rendered);
640            }
641            continue;
642        };
643
644        let mut file_names = suggestion
645            .solutions
646            .iter()
647            .flat_map(|s| s.replacements.iter())
648            .map(|r| &r.snippet.file_name);
649
650        let Some(file_name) = file_names.next() else {
651            trace!("rejecting as it has no solutions {:?}", suggestion);
652            if let Some(rendered) = diagnostic.rendered {
653                errors.insert(rendered);
654            }
655            continue;
656        };
657
658        if !file_names.all(|f| f == file_name) {
659            trace!("rejecting as it changes multiple files: {:?}", suggestion);
660            if let Some(rendered) = diagnostic.rendered {
661                errors.insert(rendered);
662            }
663            continue;
664        }
665
666        let file_path = Path::new(&file_name);
667        // Do not write into registry cache. See rust-lang/cargo#9857.
668        if let Ok(home) = env::var("CARGO_HOME") {
669            if file_path.starts_with(home) {
670                if let Some(rendered) = diagnostic.rendered {
671                    errors.insert(rendered);
672                }
673                continue;
674            }
675        }
676
677        if file_path.is_absolute() {
678            if let Some(sysroot) = get_sysroot() {
679                if file_path.starts_with(sysroot) {
680                    if let Some(rendered) = diagnostic.rendered {
681                        errors.insert(rendered);
682                    }
683                    continue;
684                }
685            }
686        }
687
688        file_map
689            .entry(file_name.to_owned())
690            .or_insert_with(IndexSet::new)
691            .insert((suggestion, diagnostic.rendered));
692    }
693
694    (errors, build_unit_map)
695}
696
697#[tracing::instrument(skip_all)]
698fn fix_errors(
699    files: &mut IndexMap<String, File>,
700    file_map: IndexMap<String, IndexSet<(Suggestion, Option<String>)>>,
701    errors: &mut IndexSet<String>,
702) -> CargoResult<bool> {
703    let mut made_changes = false;
704    for (file, suggestions) in file_map {
705        let source = match paths::read(file.as_ref()) {
706            Ok(s) => s,
707            Err(e) => {
708                warn!("failed to read `{}`: {}", file, e);
709                errors.extend(suggestions.iter().filter_map(|(_, e)| e.clone()));
710                continue;
711            }
712        };
713
714        let mut fixed = CodeFix::new(&source);
715        let mut num_fixes = 0;
716
717        for (suggestion, rendered) in suggestions.iter().rev() {
718            match fixed.apply(suggestion) {
719                Ok(()) => num_fixes += 1,
720                Err(rustfix::Error::AlreadyReplaced {
721                    is_identical: true, ..
722                }) => {}
723                Err(e) => {
724                    if let Some(rendered) = rendered {
725                        errors.insert(rendered.to_owned());
726                    }
727                    warn!("{e:?}");
728                }
729            }
730        }
731        if fixed.modified() {
732            let new_source = fixed.finish()?;
733            let file_state = files.entry(file.clone()).or_insert(File {
734                fixes: 0,
735                original_source: source,
736            });
737            paths::write(&file, new_source)?;
738            made_changes = true;
739            file_state.fixes += num_fixes;
740        }
741    }
742
743    Ok(made_changes)
744}