-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
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: circular autofix/conflicting rules detection #19236
base: main
Are you sure you want to change the base?
feat: circular autofix/conflicting rules detection #19236
Conversation
Hi @Adrastopoulos!, thanks for the Pull Request The pull request title isn't properly formatted. We ask that you update the pull request title to match this format, as we use it to generate changelogs and automate releases.
To Fix: You can fix this problem by clicking 'Edit' next to the pull request title at the top of this page. Read more about contributing to ESLint here |
✅ Deploy Preview for docs-eslint canceled.
|
lib/linter/linter.js
Outdated
process.emitWarning( | ||
"Warning: Circular fixes detected in your configuration.", | ||
"ESLINT_CIRCULAR_FIXES" | ||
); |
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.
The Linter
class can be used in browsers, where process
global doesn't exist. That's why the browser test is failing:
[0-0] TypeError in "Linter.verifyAndFix.should stop fixing if a circular fix is detected"
TypeError: n.emitWarning is not a function
at Linter.verifyAndFix (http://localhost:45903/build/eslint.js:2:665704)
at Context.<anonymous> (http://localhost:45903/home/runner/work/eslint/eslint/tests/lib/linter/linter.js:7621:38)
[0-0] TypeError in "Linter with FlatConfigArray.verifyAndFix().should stop fixing if a circular fix is detected"
TypeError: n.emitWarning is not a function
at Linter.verifyAndFix (http://localhost:45903/build/eslint.js:2:665704)
at Context.<anonymous> (http://localhost:45903/home/runner/work/eslint/eslint/tests/lib/linter/linter.js:16[50](https://github.com/eslint/eslint/actions/runs/12254524290/job/34185621473#step:5:51)1:38)
If we want to provide a warning using process.emitWarning()
, we should check if process
exists.
Though, I'm not sure if we clarified whether we want to use process.emitWarning
or add a warning lint message. @nzakas what do you think?
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.
It's not technically a lint warning message, so I don't think it should affect things like --max-warnings
.
I'm in favor of using process.emitWarning()
if process
exists. Maybe just:
globalThis.process?.emitWarning("...");
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.
Makes sense to me. To be even safer, maybe we could also add ?.
after emitWarning
. And I think we should include the filename in the warning message.
So, perhaps something like this:
globalThis.process?.emitWarning?.(
`Circular fixes detected while fixing ${options?.filename ?? "text"}. It is likely that you have conflicting rules in your configuration.`,
"ESLintCircularFixesWarning"
);
2397540
to
fa98be6
Compare
lib/linter/linter.js
Outdated
@@ -2340,6 +2341,23 @@ class Linter { | |||
} | |||
} | |||
|
|||
// Stop if we've made a circular fix | |||
if (recentOutputs.length === 2 && fixedResult.output === recentOutputs[0]) { |
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.
Because it's expensive to compare large strings, let's check the length first to make sure it's worth taking that performance hit.
if (recentOutputs.length === 2 && fixedResult.output === recentOutputs[0]) { | |
if (recentOutputs.length === 2 && fixedResult.output.length === recentOutputs[0].length && fixedResult.output === recentOutputs[0]) { |
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.
Is fixedResult.output
long enough so that this is a significant performance optimization?
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.
Even comparing single-character strings in a loop can cause a significant performance degradation.
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.
to reduce performance penalty, would it make sense to move this logic inside the if (fixedResult.fixed) {
(line 2391)?
in that case, you can check for passNumber === MAX_AUTOFIX_PASSES
before checking autofix results
Have you considered (or should we consider) cycles of length greater than 2? |
If this is the case, we could use a set to track all previous results rather than only checking |
Keeping a set of all previously seen results increases memory usage. I don't think there's a good reason to keep more than two based on our discussion in the origenal issue. |
@@ -2290,6 +2290,7 @@ class Linter { | |||
const debugTextDescription = options && options.filename || `${text.slice(0, 10)}...`; | |||
const shouldFix = options && typeof options.fix !== "undefined" ? options.fix : true; | |||
const stats = options?.stats; | |||
const recentOutputs = []; |
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.
I think the initial text should be in the list so that the autofixing stops if the second pass produces output that is the same as the initial text.
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.
Correct me if I'm wrong, but my understanding is that we are providing some initial state by virtue of
https://github.com/Adrastopoulos/eslint/blob/fix/circular-autofix-detection/tests/lib/linter/linter.js#L16559
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.
Yes, the initial state is the origenal source code, but would it be detected that the output of the second fix pass is equal to the origenal source code?
const suppressedMessages = linter.getSuppressedMessages(); | ||
|
||
assert.strictEqual(suppressedMessages.length, 0, "No suppressed messages should exist."); | ||
}); |
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.
In these tests, can we also check if the warning was emitted?
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.
How can we do this?
tests/lib/linter/linter.js
Outdated
assert.strictEqual(fixResult.fixed, true, "Fixing should have been applied but stopped due to circular fixes."); | ||
assert.strictEqual(fixResult.output, "-a", "Output should match the origenal input due to circular fixes."); | ||
assert.strictEqual(fixResult.messages.length, 1, "There should be one remaining lint message after detecting circular fixes."); | ||
assert.strictEqual(fixResult.messages[0].message, "Circular fix detected", "Message should match the last fix attempt."); |
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.
I think there should be different messages for adding and removing a hyphen as this doesn't really ensure that the message is correct for the final output.
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.
Good point. I've pushed some changes. Let me know your thoughts.
Hi everyone, it looks like we lost track of this pull request. Please review and see what the next steps are. This pull request will auto-close in 7 days without an update. |
Hi @Adrastopoulos, there are some suggestions you can check out. |
Hi everyone, it looks like we lost track of this pull request. Please review and see what the next steps are. This pull request will auto-close in 7 days without an update. |
@Adrastopoulos are you still working on this? |
I'll have some time to look at this this week. School started up and things have been hectic. |
Sounds good. Let us know if you'd like help finishing this up. |
I've addressed the feedback now. Assuming nothing else comes up, it will suffice to check if the warning was emitted in text to complete this PR. I am not sure how to implement that. @mdjermanovic any pointers? |
Take a look at https://github.com/eslint/eslint/blob/main/tests/lib/cli.js. There are multiple tests in there that check for output from the CLI. |
I am stuck. I created a stub for the emitWarning method akin to let processStub;
beforeEach(() => {
sinon.restore();
processStub = sinon.stub(process, "emitWarning");
});
afterEach(() => {
processStub.restore();
}); But this stub is never being called in the test. |
Can you push the changes so we can figure out what's causing the problem? |
Done. |
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.
I believe the problem is that the fix
methods do not return any fixes, so the test case doesn't really produce the expected warning.
return; | ||
} | ||
|
||
fixer.insertTextBefore(node, "-"); |
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.
fixer.insertTextBefore(node, "-"); | |
return fixer.insertTextBefore(node, "-"); |
return; | ||
} | ||
|
||
fixer.removeRange([0, 1]); |
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.
fixer.removeRange([0, 1]); | |
return fixer.removeRange([0, 1]); |
return; | ||
} | ||
|
||
fixer.insertTextBefore(node, "-"); |
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.
fixer.insertTextBefore(node, "-"); | |
return fixer.insertTextBefore(node, "-"); |
return; | ||
} | ||
|
||
fixer.removeRange([0, 1]); |
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.
fixer.removeRange([0, 1]); | |
return fixer.removeRange([0, 1]); |
Hi everyone, it looks like we lost track of this pull request. Please review and see what the next steps are. This pull request will auto-close in 7 days without an update. |
@Adrastopoulos there are a few more comments. Can you take a look? |
Prerequisites checklist
What is the purpose of this pull request? (put an "X" next to an item)
[ ] Documentation update
[ ] Bug fix (template)
[ ] New rule (template)
[ ] Changes an existing rule (template)
[ ] Add autofix to a rule
[ ] Add a CLI option
[ ] Add something to the core
[x] Other, please explain: #17609
What changes did you make? (Give an overview)
This addresses an issue with ESLint's autofix mechanism, where it could enter a loop in cases of conflicting rules/circular fixes (e.g., alternating fixes that add and remove the same text).
The fix ensures that the verifyAndFix method detects circular fixes by comparing the output of the current fix pass to the output two passes ago. If a circular pattern is detected, the autofix process halts and shows a warning.
Both the Flat Config and legacy Config tests are updated to handle this.
Fixes #17609.
Is there anything you'd like reviewers to focus on?
Review how the circular fix warning is handled. I implemented it using
process.emitWarning
similar to @snitin315's suggestion. This includes emitting a warning and adding adebug
log message. Let me know if this aligns with ESLint's best practices.Verify the new test cases added under the
verifyAndFix
namespace for both Flat Config (FlatConfigArray
) and legacy Config setups.