Proposal: Allow a fixer to apply multiple fixes simultaneously
See original GitHub issueProblem
As of ESLint 3.7, the autofixer for a rule can only perform one “fix” at a time. In this context, a “fix” is an atomic change (removal/insertion/replacement) to a single contiguous range of code.
From what I understand, this restriction was introduced when the autofixers for core rules almost exclusively fixed whitespace, and didn’t touch any tokens. The intention was to ensure that each fix was as small as possible, to increase the likelihood of any given fix getting applied.
There are cases where a rule wants to apply multiple fixes at separate locations. Nowadays, many autofixers modify tokens and the AST, not just whitespace. As a result, these multi-fix operations cannot be split into smaller atomic operations, because the syntax or runtime behavior of the code will be incorrect if only one fix gets applied without the other.
The autofixer for no-extra-parens
is a good example of this:
/* eslint no-extra-parens: error */
var foo = (function () {
// function body
});
The autofixer for no-extra-parens
would like to remove the parentheses around the function, to obtain the following code:
var foo = function () {
// function body
};
Clearly, the left and right parentheses have to be removed in the same pass of the code; if only one paren is removed without the other, the resulting syntax will be invalid. Reporting two separate errors as a workaround does not solve this issue; since the fix from any given reported error is not guaranteed to be applied, reporting an for each paren can still result in only one fix being applied under certain circumstances.
Due to these restrictions, no-extra-parens
(and many other core rules, such as prefer-arrow-callback
, quote-props
, wrap-regex
, etc.) uses a a different workaround: it replaces the entire text range between the two parens. Applied to this example, it replaces the entire range containing (function () { ... })
with function () { ... }
.
This workaround works because it only applies to a single text range, so it is a single contiguous fix. However, it is not ideal. Since the resulting fix is so large, it can prevent other fixes from being applied during the same pass; this results in a larger number of passes, which hurts the performance of autofixing. The fix is also overcomplicated intuitively; for a large function, it could easily end up replacing tens of thousands of characters, even though it is only trying to remove two characters.
Proposal: Allow multiple fixes
This problem could be solved if a rule could apply multiple fixes per reported problem. For example, the no-extra-parens
autofixer would apply two fixes; a single-character fix to remove the left paren, and a single-character fix to remove the right paren.
The fixes for a given reported problem would be applied atomically. In other words, there would still be no guarantee that any problem’s fixes would get applied in general, but the core engine would guarantee that all the fixes for a problem would be applied simultaneously, so the code would never end up in a state where only some of a problem’s fixes were applied.
API Changes
Currently, fixes are applied when a fixer function returns an object with range
and text
keys. (Usually, these fix objects are generated by helper functions such as RuleFixer#replaceText
.)
To apply multiple fixes, a fixer function could instead return an array of these fix objects. Returning a single fix object would still be supported for backwards-compatibility.
For example, the fixer function for no-extra-parens
would look something like this:
context.report({
node,
message,
fix(fixer) {
return [
fixer.remove(leftParenToken),
fixer.remove(rightParenToken)
];
}
});
Issue Analytics
- State:
- Created 7 years ago
- Reactions:3
- Comments:18 (16 by maintainers)
Top GitHub Comments
I’m okay with the API. I just want to make sure we agree on what the requirements of a new implementation should be (which may include things like linear time performance, atomicity of sets of fixes, etc.)
Interesting example. I’ll think about whether it’s possible to maintain linear time and atomicity, but I’m also wondering: How bad for overall performance would a quadratic-time implementation actually be?
For example, if a file has 100 fixes (probably much larger than the average), this requires updating ranges
O(10000)
times. I’m inclined to think that updating a range is computationally inexpensive, as it just requires a couple arithmetic operations, so even 10000 range updates would not cause a performance difference noticeable to the user. In comparison, doing another autofixing pass is very expensive, as it requires reparsing the file and running every rule again on the new AST.In other words, even if this change would require the autofixing implementation to be O(n2) in the number of fixes, it might still improve performance overall by reducing the number of passes required (since fewer fixes would conflict with each other)