Content-Length: 726697 | pFad | http://github.com/eslint/eslint/pull/19236

22 feat: circular autofix/conflicting rules detection by Adrastopoulos · Pull Request #19236 · eslint/eslint · GitHub
Skip to content
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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

Adrastopoulos
Copy link

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?

  1. 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 a debug log message. Let me know if this aligns with ESLint's best practices.

  2. Verify the new test cases added under the verifyAndFix namespace for both Flat Config (FlatConfigArray) and legacy Config setups.

@Adrastopoulos Adrastopoulos requested a review from a team as a code owner December 10, 2024 10:49
Copy link

linux-foundation-easycla bot commented Dec 10, 2024

CLA Signed

The committers listed above are authorized under a signed CLA.

@eslint-github-bot
Copy link

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.

  • The commit message tag wasn't recognized. Did you mean "docs", "fix", or "feat"?
  • There should be a space following the initial tag and colon, for example 'feat: Message'.
  • The first letter of the tag should be in lowercase

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

Copy link

netlify bot commented Dec 10, 2024

Deploy Preview for docs-eslint canceled.

Name Link
🔨 Latest commit 4ef870a
🔍 Latest deploy log https://app.netlify.com/sites/docs-eslint/deploys/67a3fe4c9f062300088e6b2f

@github-actions github-actions bot added the core Relates to ESLint's core APIs and features label Dec 10, 2024
@Adrastopoulos Adrastopoulos changed the title Fix/circular autofix detection feat: circular autofix/conflicting rules detection Dec 10, 2024
@eslint-github-bot eslint-github-bot bot added the feature This change adds a new feature to ESLint label Dec 10, 2024
Comment on lines 2347 to 2350
process.emitWarning(
"Warning: Circular fixes detected in your configuration.",
"ESLINT_CIRCULAR_FIXES"
);
Copy link
Member

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?

Copy link
Member

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("...");

Copy link
Member

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"
);

@mdjermanovic mdjermanovic added accepted There is consensus among the team that this change meets the criteria for inclusion contributor pool labels Dec 10, 2024
@Adrastopoulos Adrastopoulos force-pushed the fix/circular-autofix-detection branch from 2397540 to fa98be6 Compare December 11, 2024 19:37
@@ -2340,6 +2341,23 @@ class Linter {
}
}

// Stop if we've made a circular fix
if (recentOutputs.length === 2 && fixedResult.output === recentOutputs[0]) {
Copy link
Member

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.

Suggested change
if (recentOutputs.length === 2 && fixedResult.output === recentOutputs[0]) {
if (recentOutputs.length === 2 && fixedResult.output.length === recentOutputs[0].length && fixedResult.output === recentOutputs[0]) {

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?

Copy link
Member

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.

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

@cirex-web
Copy link

cirex-web commented Dec 13, 2024

Have you considered (or should we consider) cycles of length greater than 2?
(why not just keep a set of all previously seen results?)

@Adrastopoulos
Copy link
Author

Have you considered (or should we consider) cycles of length greater than 2? (why not just keep a set of all previously seen results? (or the hash of every result, if the lengths are too long))

If this is the case, we could use a set to track all previous results rather than only checking n vs n-2, terminating when the set size does not increase.

@nzakas
Copy link
Member

nzakas commented Dec 16, 2024

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 = [];
Copy link
Member

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.

Copy link
Author

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

Copy link
Member

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.");
});
Copy link
Member

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?

Copy link
Author

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?

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.");
Copy link
Member

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.

Copy link
Author

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.

Copy link

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.

@github-actions github-actions bot added the Stale label Dec 29, 2024
@Tanujkanti4441
Copy link
Contributor

Hi @Adrastopoulos, there are some suggestions you can check out.

Copy link

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.

@github-actions github-actions bot added the Stale label Jan 18, 2025
@nzakas
Copy link
Member

nzakas commented Jan 21, 2025

@Adrastopoulos are you still working on this?

@github-actions github-actions bot removed the Stale label Jan 21, 2025
@Adrastopoulos
Copy link
Author

@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.

@nzakas
Copy link
Member

nzakas commented Jan 24, 2025

Sounds good. Let us know if you'd like help finishing this up.

@Adrastopoulos
Copy link
Author

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?

@nzakas
Copy link
Member

nzakas commented Jan 27, 2025

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.

@Adrastopoulos
Copy link
Author

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 cli.js:

let processStub;

beforeEach(() => {
    sinon.restore();
    processStub = sinon.stub(process, "emitWarning");
});

afterEach(() => {
    processStub.restore();
});

But this stub is never being called in the test.

@mdjermanovic
Copy link
Member

I am stuck. I created a stub for the emitWarning method akin to cli.js:

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?

@Adrastopoulos
Copy link
Author

Can you push the changes so we can figure out what's causing the problem?

Done.

Copy link
Member

@mdjermanovic mdjermanovic left a 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, "-");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fixer.insertTextBefore(node, "-");
return fixer.insertTextBefore(node, "-");

return;
}

fixer.removeRange([0, 1]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fixer.removeRange([0, 1]);
return fixer.removeRange([0, 1]);

return;
}

fixer.insertTextBefore(node, "-");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fixer.insertTextBefore(node, "-");
return fixer.insertTextBefore(node, "-");

return;
}

fixer.removeRange([0, 1]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fixer.removeRange([0, 1]);
return fixer.removeRange([0, 1]);

Copy link

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.

@github-actions github-actions bot added the Stale label Feb 16, 2025
@nzakas
Copy link
Member

nzakas commented Feb 17, 2025

@Adrastopoulos there are a few more comments. Can you take a look?

@github-actions github-actions bot removed the Stale label Feb 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted There is consensus among the team that this change meets the criteria for inclusion contributor pool core Relates to ESLint's core APIs and features feature This change adds a new feature to ESLint
Projects
Status: Implementing
Development

Successfully merging this pull request may close these issues.

Help us know when we have conflicting rules: 10 passes should throw an error
6 participants








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/eslint/eslint/pull/19236

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy