Skip to content

Commit 765a6f1

Browse files
committed
Auto merge of #14782 - saites:rustfix-duplicate-errors, r=weihanglo
rustfix: replace special-case duplicate handling with error ### What does this PR try to resolve? This PR changes how conflicts are handled in `rustfix`. By design, `cargo fix` repeatedly compiles the code, collects suggested changes, and applies changes that don't conflict with one another. Often, conflicts arise because the same suggestion is reported multiple times e.g., due to a macro expansion. There have been previous changes to address that specific case, but following [the PR to add transactional semantics](#14747), it makes sense to change how this is handled. Specifically, this PR adds details to `Error::AlreadyReplaced` to indicate whether the specific replacement is identical to the one it conflicts with, allowing callers to handle this case, rather than special-casing it within `replace.rs` itself. To that point, this PR changes `fix.rs` and `lib.rs` to handle identical replacements by skipping the conflicting suggestion. This is not exactly identical to their previous behavior: before, it skipped a suggestion if any _solution_ had been applied, whereas this skips it if any _replacement within a solution_ has been applied. While more expansive, this is very much the spirit of the goal described above. ### How should we test and review this PR? The existing tests have been updated to account for this change. ### Additional information See: #13027
2 parents 91809c4 + 7d6889b commit 765a6f1

File tree

4 files changed

+84
-52
lines changed

4 files changed

+84
-52
lines changed

crates/rustfix/src/error.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@ pub enum Error {
1111
#[error("invalid range {0:?}, original data is only {1} byte long")]
1212
DataLengthExceeded(Range<usize>, usize),
1313

14-
#[non_exhaustive] // There are plans to add fields to this variant at a later time.
14+
#[non_exhaustive]
1515
#[error("cannot replace slice of data that was already replaced")]
16-
AlreadyReplaced,
16+
AlreadyReplaced {
17+
/// The location of the intended replacement.
18+
range: Range<usize>,
19+
/// Whether the modification exactly matches (both range and data) the one it conflicts with.
20+
/// Some clients may wish to simply ignore this condition.
21+
is_identical: bool,
22+
},
1723

1824
#[error(transparent)]
1925
Utf8(#[from] std::string::FromUtf8Error),

crates/rustfix/src/lib.rs

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,14 @@ impl CodeFix {
234234
/// Applies a suggestion to the code.
235235
pub fn apply(&mut self, suggestion: &Suggestion) -> Result<(), Error> {
236236
for solution in &suggestion.solutions {
237-
self.apply_solution(solution)?;
237+
for r in &solution.replacements {
238+
self.data
239+
.replace_range(r.snippet.range.clone(), r.replacement.as_bytes())
240+
.inspect_err(|_| self.data.restore())?;
241+
}
238242
}
243+
self.data.commit();
244+
self.modified = true;
239245
Ok(())
240246
}
241247

@@ -262,22 +268,25 @@ impl CodeFix {
262268
}
263269
}
264270

265-
/// Applies multiple `suggestions` to the given `code`.
271+
/// Applies multiple `suggestions` to the given `code`, handling certain conflicts automatically.
272+
///
273+
/// If a replacement in a suggestion exactly matches a replacement of a previously applied solution,
274+
/// that entire suggestion will be skipped without generating an error.
275+
/// This is currently done to alleviate issues like rust-lang/rust#51211,
276+
/// although it may be removed if that's fixed deeper in the compiler.
277+
///
278+
/// The intent of this design is that the overall application process
279+
/// should repeatedly apply non-conflicting suggestions then rëevaluate the result,
280+
/// looping until either there are no more suggestions to apply or some budget is exhausted.
266281
pub fn apply_suggestions(code: &str, suggestions: &[Suggestion]) -> Result<String, Error> {
267-
let mut already_applied = HashSet::new();
268282
let mut fix = CodeFix::new(code);
269283
for suggestion in suggestions.iter().rev() {
270-
// This assumes that if any of the machine applicable fixes in
271-
// a diagnostic suggestion is a duplicate, we should see the
272-
// entire suggestion as a duplicate.
273-
if suggestion
274-
.solutions
275-
.iter()
276-
.any(|sol| !already_applied.insert(sol))
277-
{
278-
continue;
279-
}
280-
fix.apply(suggestion)?;
284+
fix.apply(suggestion).or_else(|err| match err {
285+
Error::AlreadyReplaced {
286+
is_identical: true, ..
287+
} => Ok(()),
288+
_ => Err(err),
289+
})?;
281290
}
282291
fix.finish()
283292
}

crates/rustfix/src/replace.rs

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -169,26 +169,21 @@ impl Data {
169169
// Reject if the change starts before the previous one ends.
170170
if let Some(before) = ins_point.checked_sub(1).and_then(|i| self.parts.get(i)) {
171171
if incoming.range.start < before.range.end {
172-
return Err(Error::AlreadyReplaced);
172+
return Err(Error::AlreadyReplaced {
173+
is_identical: incoming == *before,
174+
range: incoming.range,
175+
});
173176
}
174177
}
175178

176-
// Reject if the change ends after the next one starts.
179+
// Reject if the change ends after the next one starts,
180+
// or if this is an insert and there's already an insert there.
177181
if let Some(after) = self.parts.get(ins_point) {
178-
// If this replacement matches exactly the part that we would
179-
// otherwise split then we ignore this for now. This means that you
180-
// can replace the exact same range with the exact same content
181-
// multiple times and we'll process and allow it.
182-
//
183-
// This is currently done to alleviate issues like
184-
// rust-lang/rust#51211 although this clause likely wants to be
185-
// removed if that's fixed deeper in the compiler.
186-
if incoming == *after {
187-
return Ok(());
188-
}
189-
190-
if incoming.range.end > after.range.start {
191-
return Err(Error::AlreadyReplaced);
182+
if incoming.range.end > after.range.start || incoming.range == after.range {
183+
return Err(Error::AlreadyReplaced {
184+
is_identical: incoming == *after,
185+
range: incoming.range,
186+
});
192187
}
193188
}
194189

@@ -274,15 +269,34 @@ mod tests {
274269
}
275270

276271
#[test]
277-
fn replace_overlapping_stuff_errs() {
272+
fn replace_same_range_diff_data() {
278273
let mut d = Data::new(b"foo bar baz");
279274

280275
d.replace_range(4..7, b"lol").unwrap();
281276
assert_eq!("foo lol baz", str(&d.to_vec()));
282277

283278
assert!(matches!(
284279
d.replace_range(4..7, b"lol2").unwrap_err(),
285-
Error::AlreadyReplaced,
280+
Error::AlreadyReplaced {
281+
is_identical: false,
282+
..
283+
},
284+
));
285+
}
286+
287+
#[test]
288+
fn replace_same_range_same_data() {
289+
let mut d = Data::new(b"foo bar baz");
290+
291+
d.replace_range(4..7, b"lol").unwrap();
292+
assert_eq!("foo lol baz", str(&d.to_vec()));
293+
294+
assert!(matches!(
295+
d.replace_range(4..7, b"lol").unwrap_err(),
296+
Error::AlreadyReplaced {
297+
is_identical: true,
298+
..
299+
},
286300
));
287301
}
288302

@@ -296,11 +310,18 @@ mod tests {
296310
}
297311

298312
#[test]
299-
fn replace_same_twice() {
313+
fn insert_same_twice() {
300314
let mut d = Data::new(b"foo");
301-
d.replace_range(0..1, b"b").unwrap();
302-
d.replace_range(0..1, b"b").unwrap();
303-
assert_eq!("boo", str(&d.to_vec()));
315+
d.replace_range(1..1, b"b").unwrap();
316+
assert_eq!("fboo", str(&d.to_vec()));
317+
assert!(matches!(
318+
d.replace_range(1..1, b"b").unwrap_err(),
319+
Error::AlreadyReplaced {
320+
is_identical: true,
321+
..
322+
},
323+
));
324+
assert_eq!("fboo", str(&d.to_vec()));
304325
}
305326

306327
#[test]

src/cargo/ops/fix.rs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -968,23 +968,19 @@ fn rustfix_and_fix(
968968
});
969969
let mut fixed = CodeFix::new(&code);
970970

971-
let mut already_applied = HashSet::new();
972971
for suggestion in suggestions.iter().rev() {
973-
// This assumes that if any of the machine applicable fixes in
974-
// a diagnostic suggestion is a duplicate, we should see the
975-
// entire suggestion as a duplicate.
976-
if suggestion
977-
.solutions
978-
.iter()
979-
.any(|sol| !already_applied.insert(sol))
980-
{
981-
continue;
982-
}
972+
// As mentioned above in `rustfix_crate`,
973+
// we don't immediately warn about suggestions that fail to apply here,
974+
// and instead we save them off for later processing.
975+
//
976+
// However, we don't bother reporting conflicts that exactly match prior replacements.
977+
// This is currently done to reduce noise for things like rust-lang/rust#51211,
978+
// although it may be removed if that's fixed deeper in the compiler.
983979
match fixed.apply(suggestion) {
984980
Ok(()) => fixed_file.fixes_applied += 1,
985-
// As mentioned above in `rustfix_crate`, we don't immediately
986-
// warn about suggestions that fail to apply here, and instead
987-
// we save them off for later processing.
981+
Err(rustfix::Error::AlreadyReplaced {
982+
is_identical: true, ..
983+
}) => continue,
988984
Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
989985
}
990986
}

0 commit comments

Comments
 (0)