Skip to content

Commit 0b80061

Browse files
committed
Auto merge of #5842 - alexcrichton:fix-lots, r=ehuss
fix: Iteratively apply suggestions from the compiler This commit updates the `cargo fix` implementation to iteratively apply fixes from the compiler instead of only once. Currently the compiler can sometimes emit overlapping suggestions, such as in the case of transitioning ::foo::<::Bar>(); to ... crate::foo::<crate::Bar>(); and `rustfix` rightfully can't handle overlapping suggestions as there's no clear way of how to disambiguate the fixes. To fix this problem Cargo will now run `rustc` and `rustfix` multiple times, attempting to reach a steady state where no fixes failed to apply. Naturally this is a pretty tricky thing to do and we want to be sure that Cargo doesn't loop forever, for example. A number of safeguards are in place to prevent Cargo from going off into the weeds when fixing files, notably avoiding to reattempt fixes if no successful fixes ended up being applied. Closes #5813 Closes rust-lang/rust#52754
2 parents 765a2a0 + 876a503 commit 0b80061

File tree

4 files changed

+173
-45
lines changed

4 files changed

+173
-45
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ libc = "0.2"
4040
log = "0.4"
4141
libgit2-sys = "0.7.5"
4242
num_cpus = "1.0"
43-
rustfix = "0.4"
43+
rustfix = "0.4.2"
4444
same-file = "1"
4545
semver = { version = "0.9.0", features = ["serde"] }
4646
serde = "1.0"

src/cargo/ops/fix.rs

Lines changed: 132 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::str;
99
use failure::{Error, ResultExt};
1010
use git2;
1111
use rustfix::diagnostics::Diagnostic;
12-
use rustfix;
12+
use rustfix::{self, CodeFix};
1313
use serde_json;
1414

1515
use core::Workspace;
@@ -144,15 +144,18 @@ pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
144144
// If we didn't actually make any changes then we can immediately exec the
145145
// new rustc, and otherwise we capture the output to hide it in the scenario
146146
// that we have to back it all out.
147-
if !fixes.original_files.is_empty() {
147+
if !fixes.files.is_empty() {
148148
let mut cmd = Command::new(&rustc);
149149
args.apply(&mut cmd);
150150
cmd.arg("--error-format=json");
151151
let output = cmd.output().context("failed to spawn rustc")?;
152152

153153
if output.status.success() {
154-
for message in fixes.messages.drain(..) {
155-
message.post()?;
154+
for (path, file) in fixes.files.iter() {
155+
Message::Fixing {
156+
file: path.clone(),
157+
fixes: file.fixes_applied,
158+
}.post()?;
156159
}
157160
}
158161

@@ -167,9 +170,9 @@ pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
167170
// user's code with our changes. Back out everything and fall through
168171
// below to recompile again.
169172
if !output.status.success() {
170-
for (k, v) in fixes.original_files {
171-
fs::write(&k, v)
172-
.with_context(|_| format!("failed to write file `{}`", k))?;
173+
for (path, file) in fixes.files.iter() {
174+
fs::write(path, &file.original_code)
175+
.with_context(|_| format!("failed to write file `{}`", path))?;
173176
}
174177
log_failed_fix(&output.stderr)?;
175178
}
@@ -182,8 +185,13 @@ pub fn fix_maybe_exec_rustc() -> CargoResult<bool> {
182185

183186
#[derive(Default)]
184187
struct FixedCrate {
185-
messages: Vec<Message>,
186-
original_files: HashMap<String, String>,
188+
files: HashMap<String, FixedFile>,
189+
}
190+
191+
struct FixedFile {
192+
errors_applying_fixes: Vec<String>,
193+
fixes_applied: u32,
194+
original_code: String,
187195
}
188196

189197
fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &Path, args: &FixArgs)
@@ -192,11 +200,6 @@ fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &Path, args: &FixArgs)
192200
args.verify_not_preparing_for_enabled_edition()?;
193201
args.warn_if_preparing_probably_inert()?;
194202

195-
// If not empty, filter by these lints
196-
//
197-
// TODO: Implement a way to specify this
198-
let only = HashSet::new();
199-
200203
// First up we want to make sure that each crate is only checked by one
201204
// process at a time. If two invocations concurrently check a crate then
202205
// it's likely to corrupt it.
@@ -205,7 +208,96 @@ fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &Path, args: &FixArgs)
205208
// argument that looks like a Rust file.
206209
let _lock = LockServerClient::lock(&lock_addr.parse()?, filename)?;
207210

208-
let mut cmd = Command::new(&rustc);
211+
// Next up this is a bit suspicious, but we *iteratively* execute rustc and
212+
// collect suggestions to feed to rustfix. Once we hit our limit of times to
213+
// execute rustc or we appear to be reaching a fixed point we stop running
214+
// rustc.
215+
//
216+
// This is currently done to handle code like:
217+
//
218+
// ::foo::<::Bar>();
219+
//
220+
// where there are two fixes to happen here: `crate::foo::<crate::Bar>()`.
221+
// The spans for these two suggestions are overlapping and its difficult in
222+
// the compiler to *not* have overlapping spans here. As a result, a naive
223+
// implementation would feed the two compiler suggestions for the above fix
224+
// into `rustfix`, but one would be rejected because it overlaps with the
225+
// other.
226+
//
227+
// In this case though, both suggestions are valid and can be automatically
228+
// applied! To handle this case we execute rustc multiple times, collecting
229+
// fixes each time we do so. Along the way we discard any suggestions that
230+
// failed to apply, assuming that they can be fixed the next time we run
231+
// rustc.
232+
//
233+
// Naturally we want a few protections in place here though to avoid looping
234+
// forever or otherwise losing data. To that end we have a few termination
235+
// conditions:
236+
//
237+
// * Do this whole process a fixed number of times. In theory we probably
238+
// need an infinite number of times to apply fixes, but we're not gonna
239+
// sit around waiting for that.
240+
// * If it looks like a fix genuinely can't be applied we need to bail out.
241+
// Detect this when a fix fails to get applied *and* no suggestions
242+
// successfully applied to the same file. In that case looks like we
243+
// definitely can't make progress, so bail out.
244+
let mut fixes = FixedCrate::default();
245+
let mut last_fix_counts = HashMap::new();
246+
let iterations = env::var("CARGO_FIX_MAX_RETRIES")
247+
.ok()
248+
.and_then(|n| n.parse().ok())
249+
.unwrap_or(4);
250+
for _ in 0..iterations {
251+
last_fix_counts.clear();
252+
for (path, file) in fixes.files.iter_mut() {
253+
last_fix_counts.insert(path.clone(), file.fixes_applied);
254+
file.errors_applying_fixes.clear(); // we'll generate new errors below
255+
}
256+
rustfix_and_fix(&mut fixes, rustc, filename, args)?;
257+
let mut progress_yet_to_be_made = false;
258+
for (path, file) in fixes.files.iter_mut() {
259+
if file.errors_applying_fixes.len() == 0 {
260+
continue
261+
}
262+
// If anything was successfully fixed *and* there's at least one
263+
// error, then assume the error was spurious and we'll try again on
264+
// the next iteration.
265+
if file.fixes_applied != *last_fix_counts.get(path).unwrap_or(&0) {
266+
progress_yet_to_be_made = true;
267+
}
268+
}
269+
if !progress_yet_to_be_made {
270+
break
271+
}
272+
}
273+
274+
// Any errors still remaining at this point need to be reported as probably
275+
// bugs in Cargo and/or rustfix.
276+
for (path, file) in fixes.files.iter_mut() {
277+
for error in file.errors_applying_fixes.drain(..) {
278+
Message::ReplaceFailed {
279+
file: path.clone(),
280+
message: error,
281+
}.post()?;
282+
}
283+
}
284+
285+
Ok(fixes)
286+
}
287+
288+
/// Execute `rustc` to apply one round of suggestions to the crate in question.
289+
///
290+
/// This will fill in the `fixes` map with original code, suggestions applied,
291+
/// and any errors encountered while fixing files.
292+
fn rustfix_and_fix(fixes: &mut FixedCrate, rustc: &Path, filename: &Path, args: &FixArgs)
293+
-> Result<(), Error>
294+
{
295+
// If not empty, filter by these lints
296+
//
297+
// TODO: Implement a way to specify this
298+
let only = HashSet::new();
299+
300+
let mut cmd = Command::new(rustc);
209301
cmd.arg("--error-format=json");
210302
args.apply(&mut cmd);
211303
let output = cmd.output()
@@ -280,8 +372,6 @@ fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &Path, args: &FixArgs)
280372
filename.display(),
281373
);
282374

283-
let mut original_files = HashMap::with_capacity(file_map.len());
284-
let mut messages = Vec::new();
285375
for (file, suggestions) in file_map {
286376
// Attempt to read the source code for this file. If this fails then
287377
// that'd be pretty surprising, so log a message and otherwise keep
@@ -296,29 +386,35 @@ fn rustfix_crate(lock_addr: &str, rustc: &Path, filename: &Path, args: &FixArgs)
296386
let num_suggestions = suggestions.len();
297387
debug!("applying {} fixes to {}", num_suggestions, file);
298388

299-
messages.push(Message::fixing(&file, num_suggestions));
300-
301-
match rustfix::apply_suggestions(&code, &suggestions) {
302-
Err(e) => {
303-
Message::ReplaceFailed {
304-
file,
305-
message: e.to_string(),
306-
}.post()?;
307-
// TODO: Add flag to decide if we want to continue or bail out
308-
continue;
309-
}
310-
Ok(new_code) => {
311-
fs::write(&file, new_code)
312-
.with_context(|_| format!("failed to write file `{}`", file))?;
313-
original_files.insert(file, code);
389+
// If this file doesn't already exist then we just read the original
390+
// code, so save it. If the file already exists then the original code
391+
// doesn't need to be updated as we've just read an interim state with
392+
// some fixes but perhaps not all.
393+
let fixed_file = fixes.files.entry(file.clone())
394+
.or_insert_with(|| {
395+
FixedFile {
396+
errors_applying_fixes: Vec::new(),
397+
fixes_applied: 0,
398+
original_code: code.clone(),
399+
}
400+
});
401+
let mut fixed = CodeFix::new(&code);
402+
403+
// As mentioned above in `rustfix_crate`, we don't immediately warn
404+
// about suggestions that fail to apply here, and instead we save them
405+
// off for later processing.
406+
for suggestion in suggestions.iter().rev() {
407+
match fixed.apply(suggestion) {
408+
Ok(()) => fixed_file.fixes_applied += 1,
409+
Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
314410
}
315411
}
412+
let new_code = fixed.finish()?;
413+
fs::write(&file, new_code)
414+
.with_context(|_| format!("failed to write file `{}`", file))?;
316415
}
317416

318-
Ok(FixedCrate {
319-
messages,
320-
original_files,
321-
})
417+
Ok(())
322418
}
323419

324420
fn exit_with(status: ExitStatus) -> ! {

src/cargo/util/diagnostic_server.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const PLEASE_REPORT_THIS_BUG: &str =
3030
pub enum Message {
3131
Fixing {
3232
file: String,
33-
fixes: usize,
33+
fixes: u32,
3434
},
3535
FixFailed {
3636
files: Vec<String>,
@@ -51,13 +51,6 @@ pub enum Message {
5151
}
5252

5353
impl Message {
54-
pub fn fixing(file: &str, num: usize) -> Message {
55-
Message::Fixing {
56-
file: file.into(),
57-
fixes: num,
58-
}
59-
}
60-
6154
pub fn post(&self) -> Result<(), Error> {
6255
let addr = env::var(DIAGNOSICS_SERVER_VAR)
6356
.context("diagnostics collector misconfigured")?;

tests/testsuite/fix.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,3 +927,42 @@ issues in preparation for the 2018 edition
927927
.with_status(0),
928928
);
929929
}
930+
931+
#[test]
932+
fn fix_overlapping() {
933+
if !is_nightly() {
934+
return
935+
}
936+
let p = project()
937+
.file(
938+
"src/lib.rs",
939+
r#"
940+
#![feature(rust_2018_preview)]
941+
942+
pub fn foo<T>() {}
943+
pub struct A;
944+
945+
pub mod bar {
946+
pub fn baz() {
947+
::foo::<::A>();
948+
}
949+
}
950+
"#
951+
)
952+
.build();
953+
954+
let stderr = "\
955+
[CHECKING] foo [..]
956+
[FIXING] src[/]lib.rs (2 fixes)
957+
[FINISHED] dev [..]
958+
";
959+
960+
assert_that(
961+
p.cargo("fix --allow-no-vcs --prepare-for 2018 --lib"),
962+
execs().with_status(0).with_stderr(stderr),
963+
);
964+
965+
let contents = p.read_file("src/lib.rs");
966+
println!("{}", contents);
967+
assert!(contents.contains("crate::foo::<crate::A>()"));
968+
}

0 commit comments

Comments
 (0)