Skip to content

Commit dfdd5ff

Browse files
committed
Auto merge of #13243 - ehuss:fix-fewer-rustc, r=epage
`cargo fix`: Call rustc fewer times. This is an improvement of `cargo fix` so that it calls `rustc` one fewer times per target. The original code an extra step of calling `rustc` to display final diagnostics to the user. Part of the reason is that in the past, cargo did not always use JSON, and so `cargo fix` was forced to call `rustc` with and without JSON. Now that cargo uses JSON all the time, that is not necessary. This avoids the final call to `rustc` by remembering the original output from `rustc`. This needs to keep track of both the first and last output from `rustc`. This is to handle the situation where `cargo fix` fails to apply some suggestion (either because the verification fails, or `rustfix` fails). The `cargo fix` output includes both the error, and the original diagnostics before the error. The first commit is a little test framework to exercise the various edge cases around how fix works. The comments should explain how it works, but it essentially has a `rustc` replacement that emits various different diagnostics and counts how often it is called. The subsequent commit includes the change to keep track of the output from `rustc` and to avoid the final call. Fixes #13215
2 parents b41f084 + e7eaa51 commit dfdd5ff

File tree

6 files changed

+658
-264
lines changed

6 files changed

+658
-264
lines changed

crates/rustfix/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,16 @@ pub fn collect_suggestions<S: ::std::hash::BuildHasher>(
215215
/// 3. Calls [`CodeFix::finish`] to get the "fixed" code.
216216
pub struct CodeFix {
217217
data: replace::Data,
218+
/// Whether or not the data has been modified.
219+
modified: bool,
218220
}
219221

220222
impl CodeFix {
221223
/// Creates a `CodeFix` with the source of a file to modify.
222224
pub fn new(s: &str) -> CodeFix {
223225
CodeFix {
224226
data: replace::Data::new(s.as_bytes()),
227+
modified: false,
225228
}
226229
}
227230

@@ -231,6 +234,7 @@ impl CodeFix {
231234
for r in &sol.replacements {
232235
self.data
233236
.replace_range(r.snippet.range.clone(), r.replacement.as_bytes())?;
237+
self.modified = true;
234238
}
235239
}
236240
Ok(())
@@ -240,6 +244,11 @@ impl CodeFix {
240244
pub fn finish(&self) -> Result<String, Error> {
241245
Ok(String::from_utf8(self.data.to_vec())?)
242246
}
247+
248+
/// Returns whether or not the data has been modified.
249+
pub fn modified(&self) -> bool {
250+
self.modified
251+
}
243252
}
244253

245254
/// Applies multiple `suggestions` to the given `code`.

src/cargo/ops/fix.rs

Lines changed: 135 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,12 @@
3535
//! applied cleanly, rustc is run again to verify the suggestions didn't
3636
//! break anything. The change will be backed out if it fails (unless
3737
//! `--broken-code` is used).
38-
//! - If there are any warnings or errors, rustc will be run one last time to
39-
//! show them to the user.
4038
4139
use std::collections::{BTreeSet, HashMap, HashSet};
4240
use std::ffi::OsString;
41+
use std::io::Write;
4342
use std::path::{Path, PathBuf};
44-
use std::process::{self, ExitStatus};
43+
use std::process::{self, ExitStatus, Output};
4544
use std::{env, fs, str};
4645

4746
use anyhow::{bail, Context as _};
@@ -388,73 +387,94 @@ pub fn fix_exec_rustc(config: &Config, lock_addr: &str) -> CargoResult<()> {
388387
trace!("start rustfixing {:?}", args.file);
389388
let fixes = rustfix_crate(&lock_addr, &rustc, &args.file, &args, config)?;
390389

391-
// Ok now we have our final goal of testing out the changes that we applied.
392-
// If these changes went awry and actually started to cause the crate to
393-
// *stop* compiling then we want to back them out and continue to print
394-
// warnings to the user.
395-
//
396-
// If we didn't actually make any changes then we can immediately execute the
397-
// new rustc, and otherwise we capture the output to hide it in the scenario
398-
// that we have to back it all out.
399-
if !fixes.files.is_empty() {
400-
debug!("calling rustc for final verification: {rustc}");
401-
let output = rustc.output()?;
402-
403-
if output.status.success() {
404-
for (path, file) in fixes.files.iter() {
405-
Message::Fixed {
406-
file: path.clone(),
407-
fixes: file.fixes_applied,
408-
}
409-
.post(config)?;
390+
if fixes.last_output.status.success() {
391+
for (path, file) in fixes.files.iter() {
392+
Message::Fixed {
393+
file: path.clone(),
394+
fixes: file.fixes_applied,
410395
}
396+
.post(config)?;
411397
}
398+
// Display any remaining diagnostics.
399+
emit_output(&fixes.last_output)?;
400+
return Ok(());
401+
}
412402

413-
// If we succeeded then we'll want to commit to the changes we made, if
414-
// any. If stderr is empty then there's no need for the final exec at
415-
// the end, we just bail out here.
416-
if output.status.success() && output.stderr.is_empty() {
417-
return Ok(());
403+
let allow_broken_code = config.get_env_os(BROKEN_CODE_ENV_INTERNAL).is_some();
404+
405+
// There was an error running rustc during the last run.
406+
//
407+
// Back out all of the changes unless --broken-code was used.
408+
if !allow_broken_code {
409+
for (path, file) in fixes.files.iter() {
410+
debug!("reverting {:?} due to errors", path);
411+
paths::write(path, &file.original_code)?;
418412
}
413+
}
419414

420-
// Otherwise, if our rustc just failed, then that means that we broke the
421-
// user's code with our changes. Back out everything and fall through
422-
// below to recompile again.
423-
if !output.status.success() {
424-
if config.get_env_os(BROKEN_CODE_ENV_INTERNAL).is_none() {
425-
for (path, file) in fixes.files.iter() {
426-
debug!("reverting {:?} due to errors", path);
427-
paths::write(path, &file.original_code)?;
415+
// If there were any fixes, let the user know that there was a failure
416+
// attempting to apply them, and to ask for a bug report.
417+
//
418+
// FIXME: The error message here is not correct with --broken-code.
419+
// https://github.com/rust-lang/cargo/issues/10955
420+
if fixes.files.is_empty() {
421+
// No fixes were available. Display whatever errors happened.
422+
emit_output(&fixes.last_output)?;
423+
exit_with(fixes.last_output.status);
424+
} else {
425+
let krate = {
426+
let mut iter = rustc.get_args();
427+
let mut krate = None;
428+
while let Some(arg) = iter.next() {
429+
if arg == "--crate-name" {
430+
krate = iter.next().and_then(|s| s.to_owned().into_string().ok());
428431
}
429432
}
430-
431-
let krate = {
432-
let mut iter = rustc.get_args();
433-
let mut krate = None;
434-
while let Some(arg) = iter.next() {
435-
if arg == "--crate-name" {
436-
krate = iter.next().and_then(|s| s.to_owned().into_string().ok());
437-
}
438-
}
439-
krate
440-
};
441-
log_failed_fix(config, krate, &output.stderr, output.status)?;
442-
}
433+
krate
434+
};
435+
log_failed_fix(
436+
config,
437+
krate,
438+
&fixes.last_output.stderr,
439+
fixes.last_output.status,
440+
)?;
441+
// Display the diagnostics that appeared at the start, before the
442+
// fixes failed. This can help with diagnosing which suggestions
443+
// caused the failure.
444+
emit_output(&fixes.first_output)?;
445+
// Exit with whatever exit code we initially started with. `cargo fix`
446+
// treats this as a warning, and shouldn't return a failure code
447+
// unless the code didn't compile in the first place.
448+
exit_with(fixes.first_output.status);
443449
}
450+
}
444451

445-
// This final fall-through handles multiple cases;
446-
// - If the fix failed, show the original warnings and suggestions.
447-
// - If `--broken-code`, show the error messages.
448-
// - If the fix succeeded, show any remaining warnings.
449-
debug!("calling rustc to display remaining diagnostics: {rustc}");
450-
exit_with(rustc.status()?);
452+
fn emit_output(output: &Output) -> CargoResult<()> {
453+
// Unfortunately if there is output on stdout, this does not preserve the
454+
// order of output relative to stderr. In practice, rustc should never
455+
// print to stdout unless some proc-macro does it.
456+
std::io::stderr().write_all(&output.stderr)?;
457+
std::io::stdout().write_all(&output.stdout)?;
458+
Ok(())
451459
}
452460

453-
#[derive(Default)]
454461
struct FixedCrate {
462+
/// Map of file path to some information about modifications made to that file.
455463
files: HashMap<String, FixedFile>,
464+
/// The output from rustc from the first time it was called.
465+
///
466+
/// This is needed when fixes fail to apply, so that it can display the
467+
/// original diagnostics to the user which can help with diagnosing which
468+
/// suggestions caused the failure.
469+
first_output: Output,
470+
/// The output from rustc from the last time it was called.
471+
///
472+
/// This will be displayed to the user to show any remaining diagnostics
473+
/// or errors.
474+
last_output: Output,
456475
}
457476

477+
#[derive(Debug)]
458478
struct FixedFile {
459479
errors_applying_fixes: Vec<String>,
460480
fixes_applied: u32,
@@ -472,11 +492,6 @@ fn rustfix_crate(
472492
args: &FixArgs,
473493
config: &Config,
474494
) -> CargoResult<FixedCrate> {
475-
if !args.can_run_rustfix(config)? {
476-
// This fix should not be run. Skipping...
477-
return Ok(FixedCrate::default());
478-
}
479-
480495
// First up, we want to make sure that each crate is only checked by one
481496
// process at a time. If two invocations concurrently check a crate then
482497
// it's likely to corrupt it.
@@ -488,6 +503,23 @@ fn rustfix_crate(
488503
// modification.
489504
let _lock = LockServerClient::lock(&lock_addr.parse()?, "global")?;
490505

506+
// Map of files that have been modified.
507+
let mut files = HashMap::new();
508+
509+
if !args.can_run_rustfix(config)? {
510+
// This fix should not be run. Skipping...
511+
// We still need to run rustc at least once to make sure any potential
512+
// rmeta gets generated, and diagnostics get displayed.
513+
debug!("can't fix {filename:?}, running rustc: {rustc}");
514+
let last_output = rustc.output()?;
515+
let fixes = FixedCrate {
516+
files,
517+
first_output: last_output.clone(),
518+
last_output,
519+
};
520+
return Ok(fixes);
521+
}
522+
491523
// Next up, this is a bit suspicious, but we *iteratively* execute rustc and
492524
// collect suggestions to feed to rustfix. Once we hit our limit of times to
493525
// execute rustc or we appear to be reaching a fixed point we stop running
@@ -521,41 +553,53 @@ fn rustfix_crate(
521553
// Detect this when a fix fails to get applied *and* no suggestions
522554
// successfully applied to the same file. In that case looks like we
523555
// definitely can't make progress, so bail out.
524-
let mut fixes = FixedCrate::default();
525-
let mut last_fix_counts = HashMap::new();
526-
let iterations = config
556+
let max_iterations = config
527557
.get_env("CARGO_FIX_MAX_RETRIES")
528558
.ok()
529559
.and_then(|n| n.parse().ok())
530560
.unwrap_or(4);
531-
for _ in 0..iterations {
532-
last_fix_counts.clear();
533-
for (path, file) in fixes.files.iter_mut() {
534-
last_fix_counts.insert(path.clone(), file.fixes_applied);
561+
let mut last_output;
562+
let mut last_made_changes;
563+
let mut first_output = None;
564+
let mut current_iteration = 0;
565+
loop {
566+
for file in files.values_mut() {
535567
// We'll generate new errors below.
536568
file.errors_applying_fixes.clear();
537569
}
538-
rustfix_and_fix(&mut fixes, rustc, filename, config)?;
570+
(last_output, last_made_changes) = rustfix_and_fix(&mut files, rustc, filename, config)?;
571+
if current_iteration == 0 {
572+
first_output = Some(last_output.clone());
573+
}
539574
let mut progress_yet_to_be_made = false;
540-
for (path, file) in fixes.files.iter_mut() {
575+
for (path, file) in files.iter_mut() {
541576
if file.errors_applying_fixes.is_empty() {
542577
continue;
543578
}
579+
debug!("had rustfix apply errors in {path:?} {file:?}");
544580
// If anything was successfully fixed *and* there's at least one
545581
// error, then assume the error was spurious and we'll try again on
546582
// the next iteration.
547-
if file.fixes_applied != *last_fix_counts.get(path).unwrap_or(&0) {
583+
if last_made_changes {
548584
progress_yet_to_be_made = true;
549585
}
550586
}
551587
if !progress_yet_to_be_made {
552588
break;
553589
}
590+
current_iteration += 1;
591+
if current_iteration >= max_iterations {
592+
break;
593+
}
594+
}
595+
if last_made_changes {
596+
debug!("calling rustc one last time for final results: {rustc}");
597+
last_output = rustc.output()?;
554598
}
555599

556600
// Any errors still remaining at this point need to be reported as probably
557601
// bugs in Cargo and/or rustfix.
558-
for (path, file) in fixes.files.iter_mut() {
602+
for (path, file) in files.iter_mut() {
559603
for error in file.errors_applying_fixes.drain(..) {
560604
Message::ReplaceFailed {
561605
file: path.clone(),
@@ -565,19 +609,23 @@ fn rustfix_crate(
565609
}
566610
}
567611

568-
Ok(fixes)
612+
Ok(FixedCrate {
613+
files,
614+
first_output: first_output.expect("at least one iteration"),
615+
last_output,
616+
})
569617
}
570618

571619
/// Executes `rustc` to apply one round of suggestions to the crate in question.
572620
///
573621
/// This will fill in the `fixes` map with original code, suggestions applied,
574622
/// and any errors encountered while fixing files.
575623
fn rustfix_and_fix(
576-
fixes: &mut FixedCrate,
624+
files: &mut HashMap<String, FixedFile>,
577625
rustc: &ProcessBuilder,
578626
filename: &Path,
579627
config: &Config,
580-
) -> CargoResult<()> {
628+
) -> CargoResult<(Output, bool)> {
581629
// If not empty, filter by these lints.
582630
// TODO: implement a way to specify this.
583631
let only = HashSet::new();
@@ -596,7 +644,7 @@ fn rustfix_and_fix(
596644
filename,
597645
output.status.code()
598646
);
599-
return Ok(());
647+
return Ok((output, false));
600648
}
601649

602650
let fix_mode = config
@@ -664,6 +712,7 @@ fn rustfix_and_fix(
664712
filename.display(),
665713
);
666714

715+
let mut made_changes = false;
667716
for (file, suggestions) in file_map {
668717
// Attempt to read the source code for this file. If this fails then
669718
// that'd be pretty surprising, so log a message and otherwise keep
@@ -682,14 +731,11 @@ fn rustfix_and_fix(
682731
// code, so save it. If the file already exists then the original code
683732
// doesn't need to be updated as we've just read an interim state with
684733
// some fixes but perhaps not all.
685-
let fixed_file = fixes
686-
.files
687-
.entry(file.clone())
688-
.or_insert_with(|| FixedFile {
689-
errors_applying_fixes: Vec::new(),
690-
fixes_applied: 0,
691-
original_code: code.clone(),
692-
});
734+
let fixed_file = files.entry(file.clone()).or_insert_with(|| FixedFile {
735+
errors_applying_fixes: Vec::new(),
736+
fixes_applied: 0,
737+
original_code: code.clone(),
738+
});
693739
let mut fixed = CodeFix::new(&code);
694740

695741
// As mentioned above in `rustfix_crate`, we don't immediately warn
@@ -701,17 +747,19 @@ fn rustfix_and_fix(
701747
Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
702748
}
703749
}
704-
let new_code = fixed.finish()?;
705-
paths::write(&file, new_code)?;
750+
if fixed.modified() {
751+
made_changes = true;
752+
let new_code = fixed.finish()?;
753+
paths::write(&file, new_code)?;
754+
}
706755
}
707756

708-
Ok(())
757+
Ok((output, made_changes))
709758
}
710759

711760
fn exit_with(status: ExitStatus) -> ! {
712761
#[cfg(unix)]
713762
{
714-
use std::io::Write;
715763
use std::os::unix::prelude::*;
716764
if let Some(signal) = status.signal() {
717765
drop(writeln!(

src/cargo/util/diagnostic_server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-proje
234234
fn gen_please_report_this_bug_text(url: &str) -> String {
235235
format!(
236236
"This likely indicates a bug in either rustc or cargo itself,\n\
237-
and we would appreciate a bug report! You're likely to see \n\
237+
and we would appreciate a bug report! You're likely to see\n\
238238
a number of compiler warnings after this message which cargo\n\
239239
attempted to fix but failed. If you could open an issue at\n\
240240
{}\n\

0 commit comments

Comments
 (0)