-
-
Notifications
You must be signed in to change notification settings - Fork 70
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
feat: Per rule autofix configuration #125
base: main
Are you sure you want to change the base?
Changes from all commits
41598f3
7497a52
ef18a67
e4440f9
07a9016
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
- Repo: <https://github.com/eslint/eslint> | ||
- Start Date: 2024-10-22 | ||
- RFC PR: | ||
- Authors: [Samuel Therrien](https://github.com/Samuel-Therrien-Beslogic) (aka [@Avasam](https://github.com/Avasam)) | ||
|
||
# Per-rule autofix configuration | ||
|
||
## Summary | ||
|
||
<!-- One-paragraph explanation of the feature. --> | ||
This feature aims to make it possible to control autofixes through shareable configuration on a per-rule basis. | ||
|
||
## Motivation | ||
|
||
<!-- Why are we doing this? What use cases does it support? What is the expected | ||
outcome? --> | ||
Some rules provide autofixing, which is great, but can sometimes be broken or otherwise simply unwanted for various reasons. | ||
Unsafe autofixes should be suggestions, and broken fixes should be reported, *but* ESLint is a large ecosystem where some very useful plugins are not always actively maintained. Even then, wanting to disable an autofix for project-specific or personal reasons could still happen. | ||
|
||
## Detailed Design | ||
|
||
<!-- | ||
This is the bulk of the RFC. | ||
|
||
Explain the design with enough detail that someone familiar with ESLint | ||
can implement it by reading this document. Please get into specifics | ||
of your approach, corner cases, and examples of how the change will be | ||
used. Be sure to define any new terms in this section. | ||
--> | ||
|
||
Similar to how Ruff (<https://docs.astral.sh/ruff/settings/#lint_unfixable>) does it, a top-level key to specify which rules to not autofix would be in my opinion the least disruptive and forward/backwards compatible. It should be overridable in per-file configurations, and picked up when extending a configuration. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An alternative could be to expand rule config values to allow objects in which this would be a property. For example: export default [
{
rules: {
"@eslint-community/eslint-comments/no-unused-disable": {
severity: "error",
options: [/* ... */],
disableAutofixes: true
}
}
}
]; The advantages of this approach are that all the configurations for the rule can be in one place, and it would be easier to add more "meta" options if needed in the future. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like it! 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We considered this a while back, even without trying to turn off autofixing. I'm not a fan of forcing rules to have to write out "severity" and "options", which is why we stuck with just an array. This would also complicate rule configuration merging. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given a large number of config in the community are already in array format, I don't think we can drop it; instead, we can support both: "@eslint-community/eslint-comments/no-unused-disable": ["error", {...}]
// is as the same as:
"@eslint-community/eslint-comments/no-unused-disable": {
severity: "error",
options: [/* ... */],
disableAutofixes: false, // the default
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think anyone was suggesting dropping the current format. I just think an object format adds additional complexity when merging rule configurations that isn't necessary. Plus, to modify a rule to disable autofix, you'd first need to convert the array into an object vs. adding a new key elsewhere. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Another advantage is that autofixes could be turned off from the CLI (with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changing the rule config format was a considered option (although not too much thoughts was put into what exactly that change would entail). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @fasttime this is additional syntax, though. Config comments and the command line don't already support this object notation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Well, true. I should have written "with the same new syntax". My point is that rules can be configured in multiple ways, but the suggested approach only allows disabling autofixes inside a config. I think while we are here, we should at least ask the question if it's sensible to disable autofixes from the CLI and in config comments. If the answer is no, we can go with the suggested approach and be assured that we won't have to rethink the design later. |
||
|
||
Concretely, it could look like this: | ||
|
||
```js | ||
export default [ | ||
{ | ||
disableAutofixes: { | ||
// We don't want this to autofix, as a rule suddenly not failing should require human attention | ||
"@eslint-community/eslint-comments/no-unused-disable": true, | ||
}, | ||
rules: { | ||
'@eslint-community/eslint-comments/no-unused-disable': 'error', | ||
} | ||
}, | ||
{ | ||
files: ["*.spec.js"], | ||
disableAutofixes: { | ||
// Let's pretend we want this to be autofixed in tests, for the sake of the RFC | ||
"@eslint-community/eslint-comments/no-unused-disable": false, | ||
}, | ||
}, | ||
] | ||
``` | ||
|
||
The fix should still exist as a suggestion. Only autofixing (when running `eslint --fix` or editor action on save) should be disabled. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm assuming you mean in all situations when autofixes are applied? That would include using the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not familiar with ESLint's API or implementation, how would you rephrase this? I wrote this purely from an ESLint user's PoV because, well, that's what I am ^^ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically, https://eslint.org/docs/latest/integrate/nodejs-api#-lintmessage-type There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm surprised that anything with an an autofix available isn't also automatically available as an editor suggestion. Do plugins have to provide both separately? If not, what is the "desc" equivalent to here? Or maybe ESLint just autofills If it's the case that they're provided separately, only the entry from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is a good point :-) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Fix this autofix/prefer-template problem" is a text VS Code generates by itself when there's For So, IDEs use both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it makes sense to provide both sometimes - an autofix can be partial, and suggestions can go farther. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mdjermanovic Is there no way to provide a non-automatic fix (ie: a suggestion) using a default message ? Oh yeah, look at that, I can see the implication of your explanation where the editor hint for the fix is completely lost. Using the That's a behaviour I'd like to improve as part of this RFC over how the If there's no way to provide a "use default message", can we just manually pass {
suggestion: {
fix: lintMessage.fix,
// Something like this, with translation support?
desc: `Fix this ${ruleName} problem`,
// or
desc: `Apply the disabled autofix for ${ruleName}`,
},
fix: undefined,
} or maybe allowing an empty Thoughts on how to handle this? I'm pretty convinced on "promoting" the fix to suggestion, but the description if still an open question. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe just
I think there's no need for the rule name in
Suggestions should generally provide a descriptive message, so I think it's better to not implement a fallback. |
||
|
||
This means removing the `LintMessage.fix` object into the `LintMessage.suggestions` array ([`LintMessage` API](https://eslint.org/docs/latest/integrate/nodejs-api#-lintmessage-type)) | ||
|
||
```js | ||
const newSuggestion = { | ||
desc: 'Apply the disabled autofix', | ||
fix: lintMessage.fix, | ||
} | ||
if (!lintMessage.suggestions) { | ||
lintMessage.suggestions = [newSuggestion] | ||
} else { | ||
lintMessage.suggestions.push(newSuggestion) | ||
} | ||
lintMessage.fix = undefined | ||
``` | ||
|
||
The chosen key name `disableAutofixes` aims to remove the concern about "turning on" an autofix that doesn't exist. Disabling autofixes for a rule that doesn't have any or doesn't exist should be a no-op. Just like turning `off` a rule that doesn't exist. The reasoning being that this allows much more flexible shareable configurations. | ||
|
||
## Documentation | ||
|
||
<!-- | ||
How will this RFC be documented? Does it need a formal announcement | ||
on the ESLint blog to explain the motivation? | ||
--> | ||
I think that "Configuring autofixes" or "Disabling autofixes" could be documented as a subsection of [Configuring Rules](https://eslint.org/docs/latest/use/configure/rules). Or as a section on the same level (between "Configuring Rules" and "Configuring Plugins") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a new top-level property like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think a new section inside the "Configure Rules" page is fine. |
||
|
||
As a new top-level property added to configuration objects, `disableAutofixes` should be documented in [Configuration Files > Configuration Objects](https://eslint.org/docs/latest/use/configure/configuration-files#configuration-objects) section. Additionally we may want to add a note to [Custom Rules](https://eslint.org/docs/latest/extend/custom-rulesd) to mention that some autofixes will be converted automatically into suggestions when the new feature is used. | ||
|
||
## Drawbacks | ||
|
||
<!-- | ||
Why should we *not* do this? Consider why adding this into ESLint | ||
might not benefit the project or the community. Attempt to think | ||
about any opposing viewpoints that reviewers might bring up. | ||
|
||
Any change has potential downsides, including increased maintenance | ||
burden, incompatibility with other tools, breaking existing user | ||
experience, etc. Try to identify as many potential problems with | ||
implementing this RFC as possible. | ||
--> | ||
A potential drawback I could see is that the configuration for autofixing a rule is not directly related with the rule itself. As a counter, I'd say this is already the case for plenty of rule-related settings, environment and parser configurations, etc. It's also less of a drawback than [Alternatives - Configure in the rule itself](#configure-in-the-rule-itself). | ||
|
||
## Backwards Compatibility Analysis | ||
|
||
<!-- | ||
How does this change affect existing ESLint users? Will any behavior | ||
change for them? If so, how are you going to minimize the disruption | ||
to existing users? | ||
--> | ||
Given that this proposal adds a new optional configuration section, this feature should be fully backwards compatible. Users that don't want to use this feature should stay completely unaffected. (see [Alternatives - Configure in the rule itself](#configure-in-the-rule-itself)) | ||
|
||
## Alternatives | ||
|
||
<!-- | ||
What other designs did you consider? Why did you decide against those? | ||
|
||
This section should also include prior art, such as whether similar | ||
projects have already implemented a similar feature. | ||
--> | ||
|
||
### Configure in the rule itself | ||
|
||
Another approach I can think of is to encode that in the rule config itself. Something like `"my-plugin/my-rule": "[{severity: "error", autofix: False}, {...otherConfigs}]"` but it's harder to commit to such a change, and means that any config extension needs to reconfigure the rule correctly just to disable autofixing (which is already an issue when someone wants to set a pre-configured rule as warning for example) | ||
|
||
### Use of a 3rd-party plugin | ||
|
||
<https://www.npmjs.com/package/eslint-plugin-no-autofix> is a tool that exists to currently work around this limitation of ESLint, but it is not perfect. | ||
|
||
1. It is an extra third-party dependency, with its own potential maintenance issues (having to keep up with ESLint, separate dependencies that can fall out of date, obsolete, unsecure, etc.) | ||
2. It may not work in all environments. For example, pre-commit.ci: <https://github.com/aladdin-add/eslint-plugin/issues/98> | ||
3. It may not work correctly with all third-party rules: <https://github.com/eslint-community/eslint-plugin-eslint-comments/issues/234> | ||
|
||
## Open Questions | ||
|
||
<!-- | ||
This section is optional, but is suggested for a first draft. | ||
|
||
What parts of this proposal are you unclear about? What do you | ||
need to know before you can finalize this RFC? | ||
|
||
List the questions that you'd like reviewers to focus on. When | ||
you've received the answers and updated the design to reflect them, | ||
you can remove this section. | ||
--> | ||
- Where exactly should the documentation go ? | ||
- Where this needs to be implemented in code. Those familiar with ESLint's codebase are welcome to provide this information | ||
|
||
## Help Needed | ||
|
||
<!-- | ||
This section is optional. | ||
|
||
Are you able to implement this RFC on your own? If not, what kind | ||
of help would you need from the team? | ||
--> | ||
My knowledge of ESLint's internals isn't that great. Whilst I think it's above the average user due to reading and configuring a lot, I haven't yet even learned how to write a plugin, and haven't migrated any project to ESLint 9 yet. | ||
My free time both at work and personal, is currently also very limited (see how long it too me to just get to writing this RFC). | ||
So I unfortunately don't think I can implement this feature myself, due to both a lack of time, personal motivation (I won't be able to use it for a while, but will push us towards ESLint 9 once implemented), and experience. | ||
|
||
## Frequently Asked Questions | ||
|
||
<!-- | ||
This section is optional but suggested. | ||
|
||
Try to anticipate points of clarification that might be needed by | ||
the people reviewing this RFC. Include those questions and answers | ||
in this section. | ||
--> | ||
|
||
Q: Could `disableAutofixes` be an array of autofixes to disable? | ||
A: `disableAutofixes` is a record to allow re-enabling autofixes in downstream configurations and on a per-file basis. We could allow a shorthand to `disableAutofixes` to accept an array of rules to disable the autofix for, but that would result in additional complexity on the implementation side with marginal benefits to the user. | ||
|
||
## Related Discussions | ||
|
||
<!-- | ||
This section is optional but suggested. | ||
|
||
If there is an issue, pull request, or other URL that provides useful | ||
context for this proposal, please include those links here. | ||
--> | ||
- <https://github.com/eslint/eslint/issues/18696> | ||
- <https://github.com/aladdin-add/eslint-plugin/issues/98> | ||
- <https://github.com/eslint-community/eslint-plugin-eslint-comments/issues/234> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're missing details on how this will be implemented. Please take a look at the code and see how you expect this feature to be implemented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I really need to learn about how ESLint is implemented I can, but idk when I'll get to it. I'd be more than happy if an existing contributor is willing to fill in this section for me as I am not personally concerned with implementation details.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the code a bit, I would say this is probably a good place to start exploring: https://github.com/eslint/eslint/blob/main/lib/eslint/eslint.js#L386-L394
It's what's being used to determine if a rule should have a fix applied to it, based on a few different conditions. Based on the response from that, a "fixer" is passed back to the linter to use as part of its fix attempt loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Samuel-Therrien-Beslogic exploring the implementation is part of the RFC process. We can't really evaluate any proposal without it.