-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[rustfix] Two suggestions applying to the same span cause syntax errors #14699
Comments
The issue in the Lines 983 to 989 in cf53cc5
cargo/crates/rustfix/src/lib.rs Lines 236 to 252 in cf53cc5
When a In this particular case, the It seems to me that most reasonable fixes are:
The advantage of the former is that it would prevent similar mistakes in the future, but it would require all callers pay the cost associated with whatever solution provides the atomic application -- it is reasonable that a caller might only wish to discard attempts that result in errors. Making the change in Footnotes
|
Thanks @saites for this. I planned to draft out a similar reply and your writing is much better than what I had in my mind! Perhaps the easiest approach to make it "just work" is this patch diff --git a/src/cargo/ops/fix.rs b/src/cargo/ops/fix.rs
index ba3c9f2666c..571f7603369 100644
--- a/src/cargo/ops/fix.rs
+++ b/src/cargo/ops/fix.rs
@@ -980,12 +980,14 @@ fn rustfix_and_fix(
{
continue;
}
- match fixed.apply(suggestion) {
- Ok(()) => fixed_file.fixes_applied += 1,
- // As mentioned above in `rustfix_crate`, we don't immediately
- // warn about suggestions that fail to apply here, and instead
- // we save them off for later processing.
- Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
+ for solution in &suggestion.solutions {
+ match fixed.apply_solution(solution) {
+ Ok(()) => fixed_file.fixes_applied += 1,
+ // As mentioned above in `rustfix_crate`, we don't immediately
+ // warn about suggestions that fail to apply here, and instead
+ // we save them off for later processing.
+ Err(e) => fixed_file.errors_applying_fixes.push(e.to_string()),
+ }
}
}
if fixed.modified() { Though this needs more designs in We could also extend the |
No problem -- glad to help.
I think that'll have the same issue, as in this case, the I implemented a very lazy fix that successfully addresses the issue by adjusting the code in /// Applies a suggestion to the code.
pub fn apply(&mut self, suggestion: &Suggestion) -> Result<(), Error> {
+ let mut prev = self.clone();
for solution in &suggestion.solutions {
- self.apply_solution(solution)?;
+ prev.apply_solution(solution)?;
}
+ self.data = prev.data;
+ self.modified = prev.modified;
Ok(())
} This can be made more efficient by first checking if the But I think a better and more efficient approach is to introduce transactional semantics directly on pub fn apply_solution(&mut self, solution: &Solution) -> Result<(), Error> {
for r in &solution.replacements {
self.data
- .replace_range(r.snippet.range.clone(), r.replacement.as_bytes())?;
+ .replace_range(r.snippet.range.clone(), r.replacement.as_bytes())
+ .inspect_err(|_| self.data.restore())?;
- self.modified = true;
}
+ self.data.commit();
+ self.modified = true;
Ok(())
} As for implementation, one approach could be to just extend the existing enum State {
/// The initial state. No change applied.
Initial,
+ /// Intends to be replaced.
+ UncommittedReplace(Rc<[u8]>),
+ /// Intends to be inserted.
+ UncommittedInsert(Rc<[u8]>),
/// Has been replaced.
Replaced(Rc<[u8]>),
/// Has been inserted.
Inserted(Rc<[u8]>),
}
impl State {
fn is_inserted(&self) -> bool {
- matches!(*self, State::Inserted(..))
+ matches!(*self, State::Inserted(..) | State::UncommittedInsert(..))
}
} I think it's most sensible for pub fn to_vec(&self) -> Vec<u8> {
if self.original.is_empty() {
return Vec::new();
}
self.parts.iter().fold(Vec::new(), |mut acc, d| {
match d.data {
State::Initial => acc.extend_from_slice(&self.original[d.start..d.end]),
+ State::UncommittedReplace(ref d) | State::UncommittedInsert(ref d) |
State::Replaced(ref d) | State::Inserted(ref d) => acc.extend_from_slice(d),
};
acc
})
} As for // New part
new_parts.push(Span {
start: range.start,
end: range.end,
data: if insert_only {
- State::Inserted(data.into())
+ State::UncommittedInsert(data.into())
} else {
- State::Replaced(data.into())
+ State::UncommittedReplace(data.into())
},
}); If you are OK with a behavior change to let part_to_split = &self.parts[index_of_part_to_split];
- // If this replacement matches exactly the part that we would
- // otherwise split then we ignore this for now. This means that you
- // can replace the exact same range with the exact same content
- // multiple times and we'll process and allow it.
- //
- // This is currently done to alleviate issues like
- // rust-lang/rust#51211 although this clause likely wants to be
- // removed if that's fixed deeper in the compiler.
- if part_to_split.start == range.start && part_to_split.end == range.end {
- if let State::Replaced(ref replacement) = part_to_split.data {
- if &**replacement == data {
- return Ok(());
- }
- }
- }
-
if part_to_split.data != State::Initial {
return Err(Error::AlreadyReplaced);
} The /// Commit pending changes.
pub fn commit(&mut self) {
use State::*;
for part in &mut self.parts {
part.data = match part.data {
UncommittedInsert(ref data) => Inserted(data.clone()),
UncommittedReplace(ref data) => Replaced(data.clone()),
Initial | Inserted(..) | Replaced(..) => continue,
}
}
} The pub fn restore(&mut self) {
use State::*;
let mut restored = Vec::with_capacity(self.parts.len());
for part in self.parts.drain(..) {
match part.data {
Replaced(..) | Inserted(..) => {
// Keep committed changes.
restored.push(part);
},
UncommittedInsert(..) => {
// Drop uncommitted inserts.
},
UncommittedReplace(..) | Initial => {
// Convert uncommitted replacements back to Initial parts.
// Dropping uncommitted replacements can leave adjacent Initial parts,
// so if the previously retained part was also Initial, merge this one with it.
if let Some(last @ Span{ data: Initial, .. }) = restored.last_mut() {
last.end = part.end;
} else {
restored.push(Span { start: part.start, end: part.end, data: Initial });
}
},
}
}
self.parts = restored;
} To me, this set of changes seems like the best trade-off among fixing the issue, reducing changes, and maintaining performance. That said, if you're up for something more radical, I also think it'd be reasonable to change the implementation for In any case, I've gone on long enough 😅. I'd be happy to submit a PR for (any or all of) the changes I discussed in this comment, if these ideas seem reasonable. |
@saites Thanks for the inspiring solution! I think I quite like the idea of transaction.
I just realized that I was targeting at the wrong level of the code. The current Cargo never generates broken code because it'll revert when compile error happened, unless While your patch is at a lower Really looking forward to your pull request :) |
Problem
rust-lang/rust-clippy#13549
Two suggestions all making changes to the same span have the potential to cause a syntax error.
Steps
cargo new --lib test-crate
src/lib.rs
:cargo clippy --fix
Possible Solution(s)
Making sure that a spans don't overlap each other when having made a suggestion. See more context in the linked issue
Notes
No response
Version
cargo version --verbose
The text was updated successfully, but these errors were encountered: