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/// Resolved 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.other_options(flags.to_metadata_flags());
362
363        let metadata = match command.exec() {
364            Ok(metadata) => metadata,
365            Err(error) => {
366                warn!("failed to run `cargo metadata`: {error}");
367                return None;
368            }
369        };
370        let Some(resolve) = metadata.resolve else {
371            warn!("`cargo metadata` did not return a dependency graph");
372            return None;
373        };
374        let dependencies = resolve
375            .nodes
376            .into_iter()
377            .map(|node| {
378                (
379                    node.id.repr,
380                    node.dependencies
381                        .into_iter()
382                        .map(|dependency| dependency.repr)
383                        .collect(),
384                )
385            })
386            .collect();
387
388        Some(Self {
389            dependencies,
390            reachable: HashMap::new(),
391        })
392    }
393
394    /// Returns whether both packages are known and transitively unrelated.
395    fn packages_are_independent(&mut self, left: &str, right: &str) -> bool {
396        left != right && !self.depends_on(left, right) && !self.depends_on(right, left)
397    }
398
399    /// Returns whether `package` transitively depends on `target`.
400    fn depends_on(&mut self, package: &str, target: &str) -> bool {
401        if !self.reachable.contains_key(package) {
402            let Some(reachable) = self.collect_reachable(package) else {
403                return true;
404            };
405            self.reachable.insert(package.to_owned(), reachable);
406        }
407
408        self.reachable
409            .get(package)
410            .is_none_or(|reachable| reachable.contains(target))
411    }
412
413    /// Collects the packages transitively reachable from `root`.
414    fn collect_reachable(&self, root: &str) -> Option<HashSet<String>> {
415        let mut reachable = HashSet::new();
416        let mut pending = vec![root];
417
418        while let Some(package) = pending.pop() {
419            if !reachable.insert(package.to_owned()) {
420                continue;
421            }
422            let dependencies = self.dependencies.get(package)?;
423            pending.extend(dependencies.iter().map(String::as_str));
424        }
425
426        reachable.remove(root);
427        Some(reachable)
428    }
429}
430
431/// Marks a target complete after reporting its fixes and remaining diagnostics.
432fn finish_target(
433    target: BuildUnit,
434    active_targets: &mut IndexMap<BuildUnit, IndexMap<String, File>>,
435    errors: &mut IndexMap<BuildUnit, IndexSet<String>>,
436    seen: &mut HashSet<BuildUnit>,
437) -> CargoResult<()> {
438    if seen
439        .iter()
440        .all(|build_unit| build_unit.package_id != target.package_id)
441    {
442        shell::status("Checking", format_package_id(&target.package_id)?)?;
443    }
444
445    if let Some(files) = active_targets.get(&target) {
446        for (name, file) in files {
447            shell::fixed(name, file.fixes)?;
448        }
449    }
450
451    for error in errors.get(&target).into_iter().flatten() {
452        shell::print_ansi_stderr(format!("{}\n\n", error.trim_end()).as_bytes())?;
453    }
454
455    active_targets.shift_remove(&target);
456    errors.shift_remove(&target);
457    seen.insert(target);
458    Ok(())
459}
460
461fn check(args: &FixitArgs, lint_cap: &mut bool) -> CargoResult<(Vec<CheckOutput>, Option<i32>)> {
462    let cmd = if args.clippy { "clippy" } else { "check" };
463    let mut command = Command::new(env!("CARGO"));
464    command
465        .args([cmd, "--message-format", "json-diagnostic-rendered-ansi"])
466        .args(args.check_flags.to_flags())
467        .stderr(Stdio::piped())
468        .stdout(Stdio::piped());
469    if *lint_cap {
470        cap_lints(&mut command);
471    }
472    let output = command.output()?;
473    let mut output = to_check_output(output);
474
475    if output.1 != Some(0) && !*lint_cap && denied_lint(&output.0) {
476        *lint_cap = true;
477        cap_lints(&mut command);
478        output = to_check_output(command.output()?);
479    }
480
481    Ok(output)
482}
483
484/// Applies the original lint cap while preserving existing compiler flags.
485fn cap_lints(command: &mut Command) {
486    if let Ok(flags) = env::var("CARGO_ENCODED_RUSTFLAGS") {
487        let separator = if flags.is_empty() { "" } else { "\u{1f}" };
488        command.env(
489            "CARGO_ENCODED_RUSTFLAGS",
490            format!("{flags}{separator}--cap-lints=warn"),
491        );
492    } else {
493        command.env(
494            "RUSTFLAGS",
495            format!(
496                "--cap-lints=warn {}",
497                env::var("RUSTFLAGS").unwrap_or("".to_owned())
498            ),
499        );
500    }
501}
502
503fn denied_lint(messages: &[CheckOutput]) -> bool {
504    messages.iter().any(|message| {
505        matches!(&message, CheckOutput::Message(message)
506                if message.message.level == DiagnosticLevel::Error
507                    && message.message.diagnostic.code.is_some())
508    })
509}
510
511fn to_check_output(output: std::process::Output) -> (Vec<CheckOutput>, Option<i32>) {
512    let buf = BufReader::new(Cursor::new(output.stdout));
513    (
514        buf.lines()
515            .map_while(|l| l.ok())
516            .filter_map(|l| serde_json::from_str(&l).ok())
517            .collect(),
518        output.status.code(),
519    )
520}
521
522#[tracing::instrument(skip_all)]
523#[allow(clippy::type_complexity)]
524fn collect_errors(
525    messages: impl Iterator<Item = CheckOutput>,
526    seen: &HashSet<BuildUnit>,
527) -> (
528    IndexMap<BuildUnit, IndexSet<String>>,
529    IndexMap<BuildUnit, IndexMap<String, IndexSet<(Suggestion, Option<String>)>>>,
530) {
531    let only = HashSet::new();
532    let mut build_unit_map = IndexMap::new();
533
534    let mut errors = IndexMap::new();
535
536    for message in messages {
537        let Message {
538            build_unit,
539            message: MessageDiagnostic { diagnostic, .. },
540        } = match message {
541            CheckOutput::Message(m) => m,
542            CheckOutput::Artifact(a) => {
543                if !seen.contains(&a.build_unit) && !a.fresh {
544                    build_unit_map
545                        .entry(a.build_unit.clone())
546                        .or_insert(IndexMap::new());
547                }
548                continue;
549            }
550        };
551
552        let errors = errors
553            .entry(build_unit.clone())
554            .or_insert_with(IndexSet::new);
555
556        if seen.contains(&build_unit) {
557            trace!("rejecting build unit `{:?}` already seen", build_unit);
558            continue;
559        }
560
561        let file_map = build_unit_map
562            .entry(build_unit.clone())
563            .or_insert(IndexMap::new());
564
565        let filter = if env::var("__CARGO_FIX_YOLO").is_ok() {
566            rustfix::Filter::Everything
567        } else {
568            rustfix::Filter::MachineApplicableOnly
569        };
570
571        let Some(suggestion) = collect_suggestions(&diagnostic, &only, filter) else {
572            trace!("rejecting as not a MachineApplicable diagnosis: {diagnostic:?}");
573            if let Some(rendered) = diagnostic.rendered {
574                errors.insert(rendered);
575            }
576            continue;
577        };
578
579        let mut file_names = suggestion
580            .solutions
581            .iter()
582            .flat_map(|s| s.replacements.iter())
583            .map(|r| &r.snippet.file_name);
584
585        let Some(file_name) = file_names.next() else {
586            trace!("rejecting as it has no solutions {:?}", suggestion);
587            if let Some(rendered) = diagnostic.rendered {
588                errors.insert(rendered);
589            }
590            continue;
591        };
592
593        if !file_names.all(|f| f == file_name) {
594            trace!("rejecting as it changes multiple files: {:?}", suggestion);
595            if let Some(rendered) = diagnostic.rendered {
596                errors.insert(rendered);
597            }
598            continue;
599        }
600
601        let file_path = Path::new(&file_name);
602        // Do not write into registry cache. See rust-lang/cargo#9857.
603        if let Ok(home) = env::var("CARGO_HOME") {
604            if file_path.starts_with(home) {
605                if let Some(rendered) = diagnostic.rendered {
606                    errors.insert(rendered);
607                }
608                continue;
609            }
610        }
611
612        if file_path.is_absolute() {
613            if let Some(sysroot) = get_sysroot() {
614                if file_path.starts_with(sysroot) {
615                    if let Some(rendered) = diagnostic.rendered {
616                        errors.insert(rendered);
617                    }
618                    continue;
619                }
620            }
621        }
622
623        file_map
624            .entry(file_name.to_owned())
625            .or_insert_with(IndexSet::new)
626            .insert((suggestion, diagnostic.rendered));
627    }
628
629    (errors, build_unit_map)
630}
631
632#[tracing::instrument(skip_all)]
633fn fix_errors(
634    files: &mut IndexMap<String, File>,
635    file_map: IndexMap<String, IndexSet<(Suggestion, Option<String>)>>,
636    errors: &mut IndexSet<String>,
637) -> CargoResult<bool> {
638    let mut made_changes = false;
639    for (file, suggestions) in file_map {
640        let source = match paths::read(file.as_ref()) {
641            Ok(s) => s,
642            Err(e) => {
643                warn!("failed to read `{}`: {}", file, e);
644                errors.extend(suggestions.iter().filter_map(|(_, e)| e.clone()));
645                continue;
646            }
647        };
648
649        let mut fixed = CodeFix::new(&source);
650        let mut num_fixes = 0;
651
652        for (suggestion, rendered) in suggestions.iter().rev() {
653            match fixed.apply(suggestion) {
654                Ok(()) => num_fixes += 1,
655                Err(rustfix::Error::AlreadyReplaced {
656                    is_identical: true, ..
657                }) => {}
658                Err(e) => {
659                    if let Some(rendered) = rendered {
660                        errors.insert(rendered.to_owned());
661                    }
662                    warn!("{e:?}");
663                }
664            }
665        }
666        if fixed.modified() {
667            let new_source = fixed.finish()?;
668            let file_state = files.entry(file.clone()).or_insert(File {
669                fixes: 0,
670                original_source: source,
671            });
672            paths::write(&file, new_source)?;
673            made_changes = true;
674            file_state.fixes += num_fixes;
675        }
676    }
677
678    Ok(made_changes)
679}