From b844324e4e8f511c9985a96c7aca063269df9570 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Sat, 3 Feb 2024 00:24:27 -0700 Subject: [PATCH 01/50] docs: Update team responsibilities (#18048) * docs: Update team responsibilities * Update docs/src/contribute/governance.md Co-authored-by: Francesco Trotta * Add #team Discord channel mention * Clarifications --------- Co-authored-by: Francesco Trotta --- docs/src/contribute/governance.md | 38 +++++++++++-------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/docs/src/contribute/governance.md b/docs/src/contribute/governance.md index 3f761fb2a51..7feb222bf71 100644 --- a/docs/src/contribute/governance.md +++ b/docs/src/contribute/governance.md @@ -32,12 +32,17 @@ Website Team Members are community members who have shown that they are committe Website Team Members: +* Are expected to work at least one hour per week triaging issues and reviewing pull requests. +* Are expected to work at least two hours total per week on ESLint. +* May invoice for the hours they spend working on ESLint at a rate of $50 USD per hour. +* Are expected to check in on the `#team` Discord channel once per week day (excluding holidays and other time off) for team updates. * Are expected to work on public branches of the source repository and submit pull requests from that branch to the master branch. * Are expected to delete their public branches when they are no longer necessary. * Must submit pull requests for all changes. * Have their work reviewed by Reviewers and TSC members before acceptance into the repository. * May label and close website-related issues (see [Manage Issues](../maintain/manage-issues)) * May merge some pull requests (see [Review Pull Requests](../maintain/review-pull-requests)) +* May take time off whenever they want, and are expected to post in the `#team` Discord channel when they will be away for more than a couple of days. To become a Website Team Member: @@ -55,12 +60,19 @@ Committers are community members who have shown that they are committed to the c Committers: +* Are expected to work at least one hour per week triaging issues and reviewing pull requests. +* Are expected to work at least two hours total per week on ESLint. +* May invoice for the hours they spend working on ESLint at a rate of $50 USD per hour. +* Are expected to check in on the `#team` Discord channel once per week day (excluding holidays and other time off) for team updates. * Are expected to work on public branches of the source repository and submit pull requests from that branch to the master branch. * Are expected to delete their public branches when they are no longer necessary. +* Are expected to provide feedback on issues in the "Feedback Needed" column of the [Triage Board](https://github.com/orgs/eslint/projects/3/views/1). +* Are expected to work on at least one issue in the "Ready to Implement" column of the [Triage Board](https://github.com/orgs/eslint/projects/3/views/1) that they didn't create each month. * Must submit pull requests for all changes. * Have their work reviewed by TSC members before acceptance into the repository. * May label and close issues (see [Manage Issues](../maintain/manage-issues)) * May merge some pull requests (see [Review Pull Requests](../maintain/review-pull-requests)) +* May take time off whenever they want, and are expected to post in the `#team` Discord channel when they will be away for more than a couple of days. To become a Committer: @@ -74,14 +86,6 @@ It is important to recognize that committership is a privilege, not a right. Tha A Committer who shows an above-average level of contribution to the project, particularly with respect to its strategic direction and long-term health, may be nominated to become a reviewer, described below. -#### Process for Adding Committers - -1. Send email congratulating the new committer and confirming that they would like to accept. This should also outline the responsibilities of a committer with a link to the maintainer guide. -1. Add the GitHub user to the "ESLint Team" team -1. Add committer email to the ESLint team mailing list -1. Invite to Discord team channel -1. Tweet congratulations to the new committer from the ESLint Twitter account - ### Reviewers Reviewers are community members who have contributed a significant amount of time to the project through triaging of issues, fixing bugs, implementing enhancements/features, and are trusted community leaders. @@ -90,6 +94,7 @@ Reviewers may perform all of the duties of Committers, and also: * May merge external pull requests for accepted issues upon reviewing and approving the changes. * May merge their own pull requests once they have collected the feedback they deem necessary. (No pull request should be merged without at least one Committer/Reviewer/TSC member comment stating they've looked at the code.) +* May invoice for the hours they spend working on ESLint at a rate of $80 USD per hour. To become a Reviewer: @@ -100,11 +105,6 @@ To become a Reviewer: A Committer is invited to become a Reviewer by existing Reviewers and TSC members. A nomination will result in discussion and then a decision by the TSC. -#### Process for Adding Reviewers - -1. Add the GitHub user to the "ESLint Reviewers" GitHub team -1. Tweet congratulations to the new Reviewer from the ESLint Twitter account - ### Technical Steering Committee (TSC) The ESLint project is jointly governed by a Technical Steering Committee (TSC) which is responsible for high-level guidance of the project. @@ -139,18 +139,6 @@ There is no specific set of requirements or qualifications for TSC members beyon A Reviewer is invited to become a TSC member by existing TSC members. A nomination will result in discussion and then a decision by the TSC. -#### Process for Adding TSC Members - -1. Add the GitHub user to the "ESLint TSC" GitHub team -1. Set the GitHub user to be have the "Owner" role for the ESLint organization -1. Send a welcome email with a link to the [Maintain ESLint documentation](../maintain/) and instructions for npm 2FA. -1. Invite to the Discord TSC channel -1. Make the TSC member an admin on the ESLint team mailing list -1. Add the TSC member to the recurring TSC meeting event on Google Calendar -1. Add the TSC member as an admin to ESLint Twitter Account on Tweetdeck -1. Add the TSC member to the ESLint TSC mailing list as an "Owner" -1. Tweet congratulations to the new TSC member from the ESLint Twitter account - #### TSC Meetings The TSC meets every other week in the TSC Meeting [Discord](https://eslint.org/chat) channel. The meeting is run by a designated moderator approved by the TSC. From 8f06a606845f40aaf0fea1fd83d5930747c5acec Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Sat, 3 Feb 2024 13:51:18 +0100 Subject: [PATCH 02/50] chore: update dependency shelljs to ^0.8.5 (#18079) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index da3b573d5df..10c8693e248 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,7 @@ "regenerator-runtime": "^0.14.0", "rollup-plugin-node-polyfills": "^0.2.1", "semver": "^7.5.3", - "shelljs": "^0.8.2", + "shelljs": "^0.8.5", "sinon": "^11.0.0", "vite-plugin-commonjs": "^0.10.0", "webdriverio": "^8.14.6", From 07a1ada7166b76c7af6186f4c5e5de8b8532edba Mon Sep 17 00:00:00 2001 From: Bryan Mishkin <698306+bmish@users.noreply.github.com> Date: Sun, 4 Feb 2024 00:56:32 -0500 Subject: [PATCH 03/50] docs: link from `--fix` CLI doc to the relevant core concept (#18080) docs: link from fix CLI doc to core concept --- docs/src/use/command-line-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/use/command-line-interface.md b/docs/src/use/command-line-interface.md index e6123391aff..daa2225d790 100644 --- a/docs/src/use/command-line-interface.md +++ b/docs/src/use/command-line-interface.md @@ -327,7 +327,7 @@ npx eslint --rulesdir my-rules/ --rulesdir my-other-rules/ file.js #### `--fix` -This option instructs ESLint to try to fix as many issues as possible. The fixes are made to the actual files themselves and only the remaining unfixed issues are output. +This option instructs ESLint to try to [fix](https://eslint.org/docs/latest/use/core-concepts#rule-fixes) as many issues as possible. The fixes are made to the actual files themselves and only the remaining unfixed issues are output. * **Argument Type**: No argument. From 9458735381269d12b24f76e1b2b6fda1bc5a509b Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Sun, 4 Feb 2024 18:04:40 +0100 Subject: [PATCH 04/50] docs: fix malformed `eslint` config comments in rule examples (#18078) * fix: fix malformed `eslint` config comments in rule examples * extend rule example validation * fix test for runtime-specific error message --- docs/src/rules/lines-around-comment.md | 8 +++---- tests/fixtures/bad-examples.md | 12 ++++++++++- tests/tools/check-rule-examples.js | 17 +++++++++++---- tools/check-rule-examples.js | 30 +++++++++++++++----------- 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/docs/src/rules/lines-around-comment.md b/docs/src/rules/lines-around-comment.md index 146224fa9af..23582fefd60 100644 --- a/docs/src/rules/lines-around-comment.md +++ b/docs/src/rules/lines-around-comment.md @@ -233,7 +233,7 @@ class C { switch (foo) { /* what a great and wonderful day */ - case 1: + case 1: bar(); break; } @@ -317,7 +317,7 @@ class C { } switch (foo) { - case 1: + case 1: bar(); break; @@ -663,7 +663,7 @@ Examples of **correct** code for the `ignorePattern` option: /*eslint lines-around-comment: ["error"]*/ foo(); -/* eslint mentioned in this comment */ +/* jshint mentioned in this comment */ bar(); /*eslint lines-around-comment: ["error", { "ignorePattern": "pragma" }] */ @@ -712,7 +712,7 @@ Examples of **incorrect** code for the `{ "applyDefaultIgnorePatterns": false }` /*eslint lines-around-comment: ["error", { "applyDefaultIgnorePatterns": false }] */ foo(); -/* eslint mentioned in comment */ +/* jshint mentioned in comment */ ``` diff --git a/tests/fixtures/bad-examples.md b/tests/fixtures/bad-examples.md index 2cc35a19d88..872a1f79bf8 100644 --- a/tests/fixtures/bad-examples.md +++ b/tests/fixtures/bad-examples.md @@ -2,7 +2,7 @@ title: no-restricted-syntax --- -This file contains rule example code with syntax errors. +This file contains rule example code with syntax errors and other problems. @@ -32,3 +32,13 @@ const foo = "baz"; ``` ::: + +:::correct + +```js +/* eslint no-restricted-syntax: "error" */ + +/* eslint doesn't allow this comment */ +``` + +::: diff --git a/tests/tools/check-rule-examples.js b/tests/tools/check-rule-examples.js index 1ee68168f9e..d93bcbe1931 100644 --- a/tests/tools/check-rule-examples.js +++ b/tests/tools/check-rule-examples.js @@ -55,10 +55,18 @@ describe("check-rule-examples", () => { assert.strictEqual(code, 1); assert.strictEqual(stdout, ""); - // Remove OS-dependent path except base name. + /* eslint-disable no-control-regex -- escaping control characters */ + const normalizedStderr = - // eslint-disable-next-line no-control-regex -- escaping control character - stderr.replace(/(?<=\x1B\[4m).*(?=bad-examples\.md)/u, ""); + stderr + + // Remove OS-dependent path except base name. + .replace(/(?<=\x1B\[4m).*(?=bad-examples\.md)/u, "") + + // Remove runtime-specific error message part (different in Node.js 18, 20 and 21). + .replace(/(?<=' doesn't allow this comment'):.*(?=\x1B\[0m)/u, ""); + + /* eslint-enable no-control-regex -- re-enable rule */ const expectedStderr = "\x1B[0m\x1B[0m\n" + @@ -68,8 +76,9 @@ describe("check-rule-examples", () => { "\x1B[0m \x1B[2m20:5\x1B[22m \x1B[31merror\x1B[39m Nonstandard language tag 'ts': use one of 'javascript', 'js' or 'jsx'\x1B[0m\n" + "\x1B[0m \x1B[2m23:7\x1B[22m \x1B[31merror\x1B[39m Syntax error: Identifier 'foo' has already been declared\x1B[0m\n" + "\x1B[0m \x1B[2m31:1\x1B[22m \x1B[31merror\x1B[39m Example code should contain a configuration comment like /* eslint no-restricted-syntax: \"error\" */\x1B[0m\n" + + "\x1B[0m \x1B[2m41:1\x1B[22m \x1B[31merror\x1B[39m Failed to parse JSON from ' doesn't allow this comment'\x1B[0m\n" + "\x1B[0m\x1B[0m\n" + - "\x1B[0m\x1B[31m\x1B[1mβœ– 5 problems (5 errors, 0 warnings)\x1B[22m\x1B[39m\x1B[0m\n" + + "\x1B[0m\x1B[31m\x1B[1mβœ– 6 problems (6 errors, 0 warnings)\x1B[22m\x1B[39m\x1B[0m\n" + "\x1B[0m\x1B[31m\x1B[1m\x1B[22m\x1B[39m\x1B[0m\n"; assert.strictEqual(normalizedStderr, expectedStderr); diff --git a/tools/check-rule-examples.js b/tools/check-rule-examples.js index 7373737cc02..b475d46b872 100644 --- a/tools/check-rule-examples.js +++ b/tools/check-rule-examples.js @@ -38,7 +38,7 @@ const commentParser = new ConfigCommentParser(); */ function tryParseForPlayground(code, parserOptions) { try { - const ast = parse(code, { ecmaVersion: "latest", ...parserOptions, comment: true }); + const ast = parse(code, { ecmaVersion: "latest", ...parserOptions, comment: true, loc: true }); return { ast }; } catch (error) { @@ -81,20 +81,26 @@ async function findProblems(filename) { const { ast, error } = tryParseForPlayground(code, parserOptions); - if (ast && !isRuleRemoved) { - const hasRuleConfigComment = ast.comments.some( - comment => { - if (comment.type !== "Block" || !/^\s*eslint(?!\S)/u.test(comment.value)) { - return false; - } - const { directiveValue } = commentParser.parseDirective(comment); - const parseResult = commentParser.parseJsonConfig(directiveValue, comment.loc); + if (ast) { + let hasRuleConfigComment = false; - return parseResult.success && Object.hasOwn(parseResult.config, title); + for (const comment of ast.comments) { + if (comment.type !== "Block" || !/^\s*eslint(?!\S)/u.test(comment.value)) { + continue; } - ); + const { directiveValue } = commentParser.parseDirective(comment); + const parseResult = commentParser.parseJsonConfig(directiveValue, comment.loc); + const parseError = parseResult.error; + + if (parseError) { + parseError.line += codeBlockToken.map[0] + 1; + problems.push(parseError); + } else if (Object.hasOwn(parseResult.config, title)) { + hasRuleConfigComment = true; + } + } - if (!hasRuleConfigComment) { + if (!isRuleRemoved && !hasRuleConfigComment) { const message = `Example code should contain a configuration comment like /* eslint ${title}: "error" */`; problems.push({ From 54df731174d2528170560d1f765e1336eca0a8bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 10:10:28 +0800 Subject: [PATCH 05/50] chore: update dependency markdownlint-cli to ^0.39.0 (#18084) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 10c8693e248..d167be6245c 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "markdown-it": "^12.2.0", "markdown-it-container": "^3.0.0", "markdownlint": "^0.33.0", - "markdownlint-cli": "^0.38.0", + "markdownlint-cli": "^0.39.0", "marked": "^4.0.8", "memfs": "^3.0.1", "metascraper": "^5.25.7", From 3c816f193eecace5efc6166efa2852a829175ef8 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Mon, 5 Feb 2024 03:12:12 +0100 Subject: [PATCH 06/50] docs: use relative link from CLI to core concepts (#18083) --- docs/src/use/command-line-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/use/command-line-interface.md b/docs/src/use/command-line-interface.md index daa2225d790..f1ceddfd8f3 100644 --- a/docs/src/use/command-line-interface.md +++ b/docs/src/use/command-line-interface.md @@ -327,7 +327,7 @@ npx eslint --rulesdir my-rules/ --rulesdir my-other-rules/ file.js #### `--fix` -This option instructs ESLint to try to [fix](https://eslint.org/docs/latest/use/core-concepts#rule-fixes) as many issues as possible. The fixes are made to the actual files themselves and only the remaining unfixed issues are output. +This option instructs ESLint to try to [fix](core-concepts#rule-fixes) as many issues as possible. The fixes are made to the actual files themselves and only the remaining unfixed issues are output. * **Argument Type**: No argument. From ce838adc3b673e52a151f36da0eedf5876977514 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 10:14:40 +0800 Subject: [PATCH 07/50] chore: replace dependency npm-run-all with npm-run-all2 ^5.0.0 (#18045) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docs/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/package.json b/docs/package.json index 9b254092490..24d84f499f8 100644 --- a/docs/package.json +++ b/docs/package.json @@ -48,7 +48,7 @@ "markdown-it-anchor": "^8.1.2", "markdown-it-container": "^3.0.0", "netlify-cli": "^10.3.1", - "npm-run-all": "^4.1.5", + "npm-run-all2": "^5.0.0", "postcss-cli": "^10.0.0", "postcss-html": "^1.5.0", "prismjs": "^1.29.0", From 6ea339e658d29791528ab26aabd86f1683cab6c3 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Mon, 5 Feb 2024 19:44:03 +0100 Subject: [PATCH 08/50] docs: add stricter rule test validations to v9 migration guide (#18085) --- docs/src/use/migrate-to-9.0.0.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/src/use/migrate-to-9.0.0.md b/docs/src/use/migrate-to-9.0.0.md index f2eaee34ffa..2a192ee46f6 100644 --- a/docs/src/use/migrate-to-9.0.0.md +++ b/docs/src/use/migrate-to-9.0.0.md @@ -543,9 +543,16 @@ As announced in our [blog post](/blog/2023/10/flat-config-rollout-plans/), the t In order to aid in the development of high-quality custom rules that are free from common bugs, ESLint v9.0.0 implements several changes to `RuleTester`: +1. **Test case `output` must be different from `code`.** In ESLint v8.x, if `output` is the same as `code`, it asserts that there was no autofix. When looking at a test case, it's not always immediately clear whether `output` differs from `code`, especially if the strings are longer or multiline, making it difficult for developers to determine whether or not the test case expects an autofix. In ESLint v9.0.0, to avoid this ambiguity, `RuleTester` now throws an error if the test `output` has the same value as the test `code`. Therefore, specifying `output` now necessarily means that the test case expects an autofix and asserts its result. If the test case doesn't expect an autofix, omit the `output` property or set it to `null`. This asserts that there was no autofix. +1. **Test error objects must specify `message` or `messageId`.** To improve the quality of test coverage, `RuleTester` now throws an error if neither `message` nor `messageId` is specified on test error objects. +1. **Test error object must specify `suggestions` if the actual error provides suggestions.** In ESLint v8.x, if the `suggestions` property was omitted from test error objects, `RuleTester` wasn't performing any checks related to suggestions, so it was easy to forget to assert if a test case produces suggestions. In ESLint v9.0.0, omitting the `suggestions` property asserts that the actual error does not provide suggestions, while you need to specify the `suggestions` property if the actual error does provide suggestions. We highly recommend that you test suggestions in detail by specifying an array of test suggestion objects, but you can also specify `suggestions: ` to assert just the number of suggestions. +1. **Test suggestion objects must specify `output`.** To improve the quality of test coverage, `RuleTester` now throws an error if `output` property is not specified on test suggestion objects. +1. **Test suggestion objects must specify `desc` or `messageId`.** To improve the quality of test coverage, `RuleTester` now throws an error if neither `desc` nor `messageId` property is specified on test suggestion objects. It's also not allowed to specify both. If you want to assert the suggestion description text in addition to the `messageId`, then also add the `data` property. 1. **Suggestion messages must be unique.** Because suggestions are typically displayed in an editor as a dropdown list, it's important that no two suggestions for the same lint problem have the same message. Otherwise, it's impossible to know what any given suggestion will do. This additional check runs automatically. +1. **Suggestions must change the code.** Suggestions are expected to fix the reported problem by changing the code. `RuleTester` now throws an error if the suggestion test `output` is the same as the test `code`. 1. **Suggestions must generate valid syntax.** In order for rule suggestions to be helpful, they need to be valid syntax. `RuleTester` now parses the output of suggestions using the same language options as the `code` value and throws an error if parsing fails. 1. **Test cases must be unique.** Identical test cases can cause confusion and be hard to detect manually in a long test file. Duplicates are now automatically detected and can be safely removed. +1. **`filename` and `only` must be of the expected type.** `RuleTester` now checks the type of `filename` and `only` properties of test objects. If specified, `filename` must be a string value. If specified, `only` must be a boolean value. **To address:** Run your rule tests using `RuleTester` and fix any errors that occur. The changes you'll need to make to satisfy `RuleTester` are compatible with ESLint v8.x. From c4d26fd3d1f59c1c0f2266664887ad18692039f3 Mon Sep 17 00:00:00 2001 From: StyleShit <32631382+StyleShit@users.noreply.github.com> Date: Tue, 6 Feb 2024 19:45:05 +0200 Subject: [PATCH 09/50] fix: `use-isnan` doesn't report on `SequenceExpression`s (#18059) * fix: `use-isnan` doesn't report on `SequenceExpression`s Closes #18058 * kinda docs? * fix suggestions * more cases * fix tests --------- Co-authored-by: Amaresh S M --- docs/src/rules/use-isnan.md | 2 + lib/rules/use-isnan.js | 31 ++++++++---- tests/lib/rules/use-isnan.js | 93 ++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 10 deletions(-) diff --git a/docs/src/rules/use-isnan.md b/docs/src/rules/use-isnan.md index d6eda9d5d91..f3de5919d18 100644 --- a/docs/src/rules/use-isnan.md +++ b/docs/src/rules/use-isnan.md @@ -224,6 +224,8 @@ var hasNaN = myArray.indexOf(NaN) >= 0; var firstIndex = myArray.indexOf(NaN); var lastIndex = myArray.lastIndexOf(NaN); + +var indexWithSequenceExpression = myArray.indexOf((doStuff(), NaN)); ``` ::: diff --git a/lib/rules/use-isnan.js b/lib/rules/use-isnan.js index b00a701c6bd..357c51d13d6 100644 --- a/lib/rules/use-isnan.js +++ b/lib/rules/use-isnan.js @@ -21,9 +21,17 @@ const astUtils = require("./utils/ast-utils"); * @returns {boolean} `true` if the node is 'NaN' identifier. */ function isNaNIdentifier(node) { - return Boolean(node) && ( - astUtils.isSpecificId(node, "NaN") || - astUtils.isSpecificMemberAccess(node, "Number", "NaN") + if (!node) { + return false; + } + + const nodeToCheck = node.type === "SequenceExpression" + ? node.expressions.at(-1) + : node; + + return ( + astUtils.isSpecificId(nodeToCheck, "NaN") || + astUtils.isSpecificMemberAccess(nodeToCheck, "Number", "NaN") ); } @@ -115,7 +123,10 @@ module.exports = { (isNaNIdentifier(node.left) || isNaNIdentifier(node.right)) ) { const suggestedFixes = []; - const isFixable = fixableOperators.has(node.operator); + const NaNNode = isNaNIdentifier(node.left) ? node.left : node.right; + + const isSequenceExpression = NaNNode.type === "SequenceExpression"; + const isFixable = fixableOperators.has(node.operator) && !isSequenceExpression; const isCastable = castableOperators.has(node.operator); if (isFixable) { @@ -123,13 +134,13 @@ module.exports = { messageId: "replaceWithIsNaN", fix: getBinaryExpressionFixer(node, value => `Number.isNaN(${value})`) }); - } - if (isCastable) { - suggestedFixes.push({ - messageId: "replaceWithCastingAndIsNaN", - fix: getBinaryExpressionFixer(node, value => `Number.isNaN(Number(${value}))`) - }); + if (isCastable) { + suggestedFixes.push({ + messageId: "replaceWithCastingAndIsNaN", + fix: getBinaryExpressionFixer(node, value => `Number.isNaN(Number(${value}))`) + }); + } } context.report({ diff --git a/tests/lib/rules/use-isnan.js b/tests/lib/rules/use-isnan.js index 26c887a1975..337f1f4d146 100644 --- a/tests/lib/rules/use-isnan.js +++ b/tests/lib/rules/use-isnan.js @@ -49,6 +49,9 @@ ruleTester.run("use-isnan", rule, { "foo(2 / Number.NaN)", "var x; if (x = Number.NaN) { }", "x === Number[NaN];", + "x === (NaN, 1)", + "x === (doStuff(), NaN, 1)", + "x === (doStuff(), Number.NaN, 1)", //------------------------------------------------------------------------------ // enforceForSwitchCase @@ -174,6 +177,14 @@ ruleTester.run("use-isnan", rule, { code: "switch(foo) { case foo.Number.NaN: break }", options: [{ enforceForSwitchCase: true }] }, + { + code: "switch((NaN, doStuff(), 1)) {}", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch((Number.NaN, doStuff(), 1)) {}", + options: [{ enforceForSwitchCase: true }] + }, //------------------------------------------------------------------------------ // enforceForIndexOf @@ -344,6 +355,22 @@ ruleTester.run("use-isnan", rule, { { code: "foo.lastIndexOf(Number.NaN())", options: [{ enforceForIndexOf: true }] + }, + { + code: "foo.indexOf((NaN, 1))", + options: [{ enforceForIndexOf: true }] + }, + { + code: "foo.lastIndexOf((NaN, 1))", + options: [{ enforceForIndexOf: true }] + }, + { + code: "foo.indexOf((Number.NaN, 1))", + options: [{ enforceForIndexOf: true }] + }, + { + code: "foo.lastIndexOf((Number.NaN, 1))", + options: [{ enforceForIndexOf: true }] } ], invalid: [ @@ -747,6 +774,34 @@ ruleTester.run("use-isnan", rule, { ] }] }, + { + code: "x === (doStuff(), NaN);", + errors: [{ + ...comparisonError, + suggestions: [] + }] + }, + { + code: "x === (doStuff(), Number.NaN);", + errors: [{ + ...comparisonError, + suggestions: [] + }] + }, + { + code: "x == (doStuff(), NaN);", + errors: [{ + ...comparisonError, + suggestions: [] + }] + }, + { + code: "x == (doStuff(), Number.NaN);", + errors: [{ + ...comparisonError, + suggestions: [] + }] + }, //------------------------------------------------------------------------------ // enforceForSwitchCase @@ -910,6 +965,20 @@ ruleTester.run("use-isnan", rule, { { messageId: "caseNaN", type: "SwitchCase", column: 22 } ] }, + { + code: "switch((doStuff(), NaN)) {}", + options: [{ enforceForSwitchCase: true }], + errors: [ + { messageId: "switchNaN", type: "SwitchStatement", column: 1 } + ] + }, + { + code: "switch((doStuff(), Number.NaN)) {}", + options: [{ enforceForSwitchCase: true }], + errors: [ + { messageId: "switchNaN", type: "SwitchStatement", column: 1 } + ] + }, //------------------------------------------------------------------------------ // enforceForIndexOf @@ -1010,6 +1079,30 @@ ruleTester.run("use-isnan", rule, { options: [{ enforceForIndexOf: true }], languageOptions: { ecmaVersion: 2020 }, errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + }, + { + code: "foo.indexOf((1, NaN))", + options: [{ enforceForIndexOf: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + }, + { + code: "foo.indexOf((1, Number.NaN))", + options: [{ enforceForIndexOf: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + }, + { + code: "foo.lastIndexOf((1, NaN))", + options: [{ enforceForIndexOf: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "lastIndexOf" } }] + }, + { + code: "foo.lastIndexOf((1, Number.NaN))", + options: [{ enforceForIndexOf: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "lastIndexOf" } }] } ] }); From 15c143f96ef164943fd3d39b5ad79d9a4a40de8f Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 7 Feb 2024 13:36:56 -0700 Subject: [PATCH 10/50] docs: JS Foundation -> OpenJS Foundation in PR template (#18092) --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 35d24100191..5fd747b2906 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,7 @@ #### Prerequisites checklist From f1c7e6fc8ea77fcdae4ad1f8fe1cd104a281d2e9 Mon Sep 17 00:00:00 2001 From: Strek Date: Fri, 9 Feb 2024 09:39:04 -0800 Subject: [PATCH 11/50] docs: Switch to Ethical Ads (#18090) * feat: Switch to Ethical Ads * chore: run stylelint * chore: review comments * chore: remove comments --- docs/src/_data/sites/en.yml | 5 ++- docs/src/_includes/components/hero.macro.html | 2 +- docs/src/_includes/layouts/doc.html | 2 +- .../partials/{carbon-ad.html => ad.html} | 12 ++++++ .../assets/scss/{carbon-ads.scss => ads.scss} | 39 +++++++++++++++++++ docs/src/assets/scss/styles.scss | 2 +- 6 files changed, 57 insertions(+), 5 deletions(-) rename docs/src/_includes/partials/{carbon-ad.html => ad.html} (51%) rename docs/src/assets/scss/{carbon-ads.scss => ads.scss} (71%) diff --git a/docs/src/_data/sites/en.yml b/docs/src/_data/sites/en.yml index 532630be810..dc90e3dd233 100644 --- a/docs/src/_data/sites/en.yml +++ b/docs/src/_data/sites/en.yml @@ -27,8 +27,9 @@ google_analytics: #------------------------------------------------------------------------------ carbon_ads: - serve: CESDV2QM - placement: eslintorg + serve: "" + placement: "" +ethical_ads: true #------------------------------------------------------------------------------ # Shared diff --git a/docs/src/_includes/components/hero.macro.html b/docs/src/_includes/components/hero.macro.html index 3ff0c9c6f80..5b6ccb38bf4 100644 --- a/docs/src/_includes/components/hero.macro.html +++ b/docs/src/_includes/components/hero.macro.html @@ -22,7 +22,7 @@

{{ params.title }}

{% endif %}
- {% include "partials/carbon-ad.html" %} + {% include "partials/ad.html" %}
diff --git a/docs/src/_includes/layouts/doc.html b/docs/src/_includes/layouts/doc.html index 58d8986a5dc..92db212806e 100644 --- a/docs/src/_includes/layouts/doc.html +++ b/docs/src/_includes/layouts/doc.html @@ -111,7 +111,7 @@

{{ title }}

{% include 'components/docs-toc.html' %}
diff --git a/docs/src/_includes/partials/carbon-ad.html b/docs/src/_includes/partials/ad.html similarity index 51% rename from docs/src/_includes/partials/carbon-ad.html rename to docs/src/_includes/partials/ad.html index c79eba5a679..e71b3585544 100644 --- a/docs/src/_includes/partials/carbon-ad.html +++ b/docs/src/_includes/partials/ad.html @@ -11,3 +11,15 @@ } {% endif %} +{% if site.ethical_ads %} + +
+{% endif %} \ No newline at end of file diff --git a/docs/src/assets/scss/carbon-ads.scss b/docs/src/assets/scss/ads.scss similarity index 71% rename from docs/src/assets/scss/carbon-ads.scss rename to docs/src/assets/scss/ads.scss index bd7ea8e660c..4b1b4e84e1b 100644 --- a/docs/src/assets/scss/carbon-ads.scss +++ b/docs/src/assets/scss/ads.scss @@ -4,6 +4,15 @@ } } +.docs-ad { + height: 290px; +} + +/* + * Carbon Ads + * https://www.carbonads.net/ + */ + #carbonads * { margin: initial; padding: initial; @@ -113,3 +122,33 @@ font-size: 8px; } } + +/* + * Ethical Ads + */ + +[data-ea-publisher].loaded .ea-content, +[data-ea-type].loaded .ea-content { + background-color: var(--body-background-color) !important; + border: 1px solid var(--border-color) !important; +} + +[data-ea-publisher].loaded .ea-content a:link, +[data-ea-type].loaded .ea-content a:link { + color: var(--body-text-color) !important; +} + +[data-ea-publisher].loaded .ea-callout a:link, +[data-ea-type].loaded .ea-callout a:link { + color: var(--body-text-color) !important; +} + +.jumbotron [data-ea-publisher].loaded .ea-content a, +.jumbotron [data-ea-type].loaded .ea-content a { + color: #eee; +} + +.jumbotron [data-ea-publisher].loaded .ea-content a:hover, +.jumbotron [data-ea-type].loaded .ea-content a:hover { + color: #ccc; +} diff --git a/docs/src/assets/scss/styles.scss b/docs/src/assets/scss/styles.scss index 8907a6c4bf9..09915d60b4e 100644 --- a/docs/src/assets/scss/styles.scss +++ b/docs/src/assets/scss/styles.scss @@ -30,6 +30,6 @@ @import "components/tabs"; @import "components/resources"; -@import "carbon-ads"; +@import "ads"; @import "utilities"; From 53f0f47badffa1b04ec2836f2ae599f4fc464da2 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 9 Feb 2024 12:10:36 -0700 Subject: [PATCH 12/50] feat: Add loadESLint() API method for v9 (#18097) * feat: Add loadESLint() API method for v9 refs #18075 * Fix docs * Add more tests using environment variables --- docs/src/integrate/nodejs-api.md | 43 +++++++++++++++++++++++++++++++ lib/api.js | 26 ++++++++++++++++++- lib/eslint/eslint.js | 6 +++++ lib/eslint/legacy-eslint.js | 6 +++++ tests/lib/api.js | 41 ++++++++++++++++++++++++++++- tests/lib/eslint/eslint.js | 5 ++++ tests/lib/eslint/legacy-eslint.js | 5 ++++ 7 files changed, 130 insertions(+), 2 deletions(-) diff --git a/docs/src/integrate/nodejs-api.md b/docs/src/integrate/nodejs-api.md index ce0890c79b6..5d081df3d3b 100644 --- a/docs/src/integrate/nodejs-api.md +++ b/docs/src/integrate/nodejs-api.md @@ -442,6 +442,49 @@ The `LoadedFormatter` value is the object to convert the [LintResult] objects to --- +## loadESLint() + +The `loadESLint()` function is used for integrations that wish to support both the current configuration system (flat config) and the old configuration system (eslintrc). This function returns the correct `ESLint` class implementation based on the arguments provided: + +```js +const { loadESLint } = require("eslint"); + +// loads the default ESLint that the CLI would use based on process.cwd() +const DefaultESLint = await loadESLint(); + +// loads the default ESLint that the CLI would use based on the provided cwd +const CwdDefaultESLint = await loadESLint({ cwd: "/foo/bar" }); + +// loads the flat config version specifically +const FlatESLint = await loadESLint({ useFlatConfig: true }); + +// loads the legacy version specifically +const LegacyESLint = await loadESLint({ useFlatConfig: false }); +``` + +You can then use the returned constructor to instantiate a new `ESLint` instance, like this: + +```js +// loads the default ESLint that the CLI would use based on process.cwd() +const DefaultESLint = await loadESLint(); +const eslint = new DefaultESLint(); +``` + +If you're ever unsure which config system the returned constructor uses, check the `configType` property, which is either `"flat"` or `"eslintrc"`: + +```js +// loads the default ESLint that the CLI would use based on process.cwd() +const DefaultESLint = await loadESLint(); + +if (DefaultESLint.configType === "flat") { + // do something specific to flat config +} +``` + +If you don't need to support both the old and new configuration systems, then it's recommended to just use the `ESLint` constructor directly. + +--- + ## SourceCode The `SourceCode` type represents the parsed source code that ESLint executes on. It's used internally in ESLint and is also available so that already-parsed code can be used. You can create a new instance of `SourceCode` by passing in the text string representing the code and an abstract syntax tree (AST) in [ESTree](https://github.com/estree/estree) format (including location information, range information, comments, and tokens): diff --git a/lib/api.js b/lib/api.js index 4a689250af7..ab0ec2fcd31 100644 --- a/lib/api.js +++ b/lib/api.js @@ -9,17 +9,41 @@ // Requirements //----------------------------------------------------------------------------- -const { ESLint } = require("./eslint/eslint"); +const { ESLint, shouldUseFlatConfig } = require("./eslint/eslint"); +const { LegacyESLint } = require("./eslint/legacy-eslint"); const { Linter } = require("./linter"); const { RuleTester } = require("./rule-tester"); const { SourceCode } = require("./source-code"); +//----------------------------------------------------------------------------- +// Functions +//----------------------------------------------------------------------------- + +/** + * Loads the correct ESLint constructor given the options. + * @param {Object} [options] The options object + * @param {boolean} [options.useFlatConfig] Whether or not to use a flat config + * @returns {Promise} The ESLint constructor + */ +async function loadESLint({ useFlatConfig } = {}) { + + /* + * Note: The v8.x version of this function also accepted a `cwd` option, but + * it is not used in this implementation so we silently ignore it. + */ + + const shouldESLintUseFlatConfig = useFlatConfig ?? (await shouldUseFlatConfig()); + + return shouldESLintUseFlatConfig ? ESLint : LegacyESLint; +} + //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- module.exports = { Linter, + loadESLint, ESLint, RuleTester, SourceCode diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 49bc0e7579a..97102d3fe0e 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -565,6 +565,12 @@ function createExtraneousResultsError() { */ class ESLint { + /** + * The type of configuration used by this class. + * @type {string} + */ + static configType = "flat"; + /** * Creates a new instance of the main ESLint API. * @param {ESLintOptions} options The options for this instance. diff --git a/lib/eslint/legacy-eslint.js b/lib/eslint/legacy-eslint.js index 251a3890db8..9c86163ef63 100644 --- a/lib/eslint/legacy-eslint.js +++ b/lib/eslint/legacy-eslint.js @@ -438,6 +438,12 @@ function compareResultsByFilePath(a, b) { */ class LegacyESLint { + /** + * The type of configuration used by this class. + * @type {string} + */ + static configType = "eslintrc"; + /** * Creates a new instance of the main ESLint API. * @param {LegacyESLintOptions} options The options for this instance. diff --git a/tests/lib/api.js b/tests/lib/api.js index abcbea5aef1..71a5f42930a 100644 --- a/tests/lib/api.js +++ b/tests/lib/api.js @@ -10,7 +10,8 @@ //----------------------------------------------------------------------------- const assert = require("chai").assert, - api = require("../../lib/api"); + api = require("../../lib/api"), + { LegacyESLint } = require("../../lib/eslint/legacy-eslint"); //----------------------------------------------------------------------------- // Tests @@ -41,4 +42,42 @@ describe("api", () => { it("should have SourceCode exposed", () => { assert.isFunction(api.SourceCode); }); + + describe("loadESLint", () => { + + afterEach(() => { + delete process.env.ESLINT_USE_FLAT_CONFIG; + }); + + it("should be a function", () => { + assert.isFunction(api.loadESLint); + }); + + it("should return a Promise", () => { + assert.instanceOf(api.loadESLint(), Promise); + }); + + it("should return ESLint when useFlatConfig is true", async () => { + assert.strictEqual(await api.loadESLint({ useFlatConfig: true }), api.ESLint); + }); + + it("should return LegacyESLint when useFlatConfig is false", async () => { + assert.strictEqual(await api.loadESLint({ useFlatConfig: false }), LegacyESLint); + }); + + it("should return ESLint when useFlatConfig is not provided", async () => { + assert.strictEqual(await api.loadESLint(), api.ESLint); + }); + + it("should return LegacyESLint when useFlatConfig is not provided and ESLINT_USE_FLAT_CONFIG is false", async () => { + process.env.ESLINT_USE_FLAT_CONFIG = "false"; + assert.strictEqual(await api.loadESLint(), LegacyESLint); + }); + + it("should return ESLint when useFlatConfig is not provided and ESLINT_USE_FLAT_CONFIG is true", async () => { + process.env.ESLINT_USE_FLAT_CONFIG = "true"; + assert.strictEqual(await api.loadESLint(), api.ESLint); + }); + }); + }); diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 3bec83d441e..9360d39449d 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -134,6 +134,11 @@ describe("ESLint", () => { }); describe("ESLint constructor function", () => { + + it("should have a static property indicating the configType being used", () => { + assert.strictEqual(ESLint.configType, "flat"); + }); + it("the default value of 'options.cwd' should be the current working directory.", async () => { process.chdir(__dirname); try { diff --git a/tests/lib/eslint/legacy-eslint.js b/tests/lib/eslint/legacy-eslint.js index 7bc70d4be93..60b40cb5cd6 100644 --- a/tests/lib/eslint/legacy-eslint.js +++ b/tests/lib/eslint/legacy-eslint.js @@ -114,6 +114,11 @@ describe("LegacyESLint", () => { }); describe("ESLint constructor function", () => { + + it("should have a static property indicating the configType being used", () => { + assert.strictEqual(LegacyESLint.configType, "eslintrc"); + }); + it("the default value of 'options.cwd' should be the current working directory.", async () => { process.chdir(__dirname); try { From 916364692bae6a93c10b5d48fc1e9de1677d0d09 Mon Sep 17 00:00:00 2001 From: fnx <966276+DMartens@users.noreply.github.com> Date: Fri, 9 Feb 2024 21:43:33 +0100 Subject: [PATCH 13/50] feat!: Rule Tester checks for missing placeholder data in the message (#18073) * feat!: rule tester checks for missing placeholder data for the reported message * chore: incorporate bmishs suggestions * chore: differentiate message between a single and multiple missing data properties * fix: check for missing placeholders with data specified * feat: share regular expression for message placeholders * feat: ignore introduced message placeholders * refactor: simplified logic for getting unsubstituted message placeholders * docs: also use term unsubstituted for migration guide Co-authored-by: Milos Djermanovic * docs: clarify placeholder versus data Co-authored-by: Milos Djermanovic * chore: message grammar fixes Co-authored-by: Milos Djermanovic * chore: update error messages for the grammar fixes * fix: remove unnecessary check for added placeholders * chore: split up object to avoid referencing the placeholder matcher via module.exports * chore: add mention of the issue for the migration guide * chore: stylize rule tester in migration guide * chore: clarify message for unsubstituted placeholders and introduce fixture for non-string data values --------- Co-authored-by: Milos Djermanovic --- docs/src/use/migrate-to-9.0.0.md | 3 +- lib/linter/index.js | 2 +- lib/linter/interpolate.js | 26 +++- lib/linter/report-translator.js | 2 +- lib/rule-tester/rule-tester.js | 60 +++++++++- .../fixtures/testers/rule-tester/messageId.js | 107 +++++++++++++++++ .../testers/rule-tester/suggestions.js | 58 +++++++++ tests/lib/linter/interpolate.js | 30 ++++- tests/lib/rule-tester/rule-tester.js | 112 ++++++++++++++++++ 9 files changed, 393 insertions(+), 7 deletions(-) diff --git a/docs/src/use/migrate-to-9.0.0.md b/docs/src/use/migrate-to-9.0.0.md index 2a192ee46f6..9db6f4724ec 100644 --- a/docs/src/use/migrate-to-9.0.0.md +++ b/docs/src/use/migrate-to-9.0.0.md @@ -553,10 +553,11 @@ In order to aid in the development of high-quality custom rules that are free fr 1. **Suggestions must generate valid syntax.** In order for rule suggestions to be helpful, they need to be valid syntax. `RuleTester` now parses the output of suggestions using the same language options as the `code` value and throws an error if parsing fails. 1. **Test cases must be unique.** Identical test cases can cause confusion and be hard to detect manually in a long test file. Duplicates are now automatically detected and can be safely removed. 1. **`filename` and `only` must be of the expected type.** `RuleTester` now checks the type of `filename` and `only` properties of test objects. If specified, `filename` must be a string value. If specified, `only` must be a boolean value. +1. **Messages cannot have unsubstituted placeholders.** The `RuleTester` now also checks if there are {% raw %}`{{ placeholder }}` {% endraw %} still in the message as their values were not passed via `data` in the respective `context.report()` call. **To address:** Run your rule tests using `RuleTester` and fix any errors that occur. The changes you'll need to make to satisfy `RuleTester` are compatible with ESLint v8.x. -**Related Issues(s):** [#15104](https://github.com/eslint/eslint/issues/15104), [#15735](https://github.com/eslint/eslint/issues/15735), [#16908](https://github.com/eslint/eslint/issues/16908) +**Related Issues(s):** [#15104](https://github.com/eslint/eslint/issues/15104), [#15735](https://github.com/eslint/eslint/issues/15735), [#16908](https://github.com/eslint/eslint/issues/16908), [#18016](https://github.com/eslint/eslint/issues/18016) ## `FlatESLint` is now `ESLint` diff --git a/lib/linter/index.js b/lib/linter/index.js index 25fd769bde9..795a414abf4 100644 --- a/lib/linter/index.js +++ b/lib/linter/index.js @@ -1,7 +1,7 @@ "use strict"; const { Linter } = require("./linter"); -const interpolate = require("./interpolate"); +const { interpolate } = require("./interpolate"); const SourceCodeFixer = require("./source-code-fixer"); module.exports = { diff --git a/lib/linter/interpolate.js b/lib/linter/interpolate.js index 87e06a02369..5f4ff922736 100644 --- a/lib/linter/interpolate.js +++ b/lib/linter/interpolate.js @@ -9,13 +9,30 @@ // Public Interface //------------------------------------------------------------------------------ -module.exports = (text, data) => { +/** + * Returns a global expression matching placeholders in messages. + * @returns {RegExp} Global regular expression matching placeholders + */ +function getPlaceholderMatcher() { + return /\{\{([^{}]+?)\}\}/gu; +} + +/** + * Replaces {{ placeholders }} in the message with the provided data. + * Does not replace placeholders not available in the data. + * @param {string} text Original message with potential placeholders + * @param {Record} data Map of placeholder name to its value + * @returns {string} Message with replaced placeholders + */ +function interpolate(text, data) { if (!data) { return text; } + const matcher = getPlaceholderMatcher(); + // Substitution content for any {{ }} markers. - return text.replace(/\{\{([^{}]+?)\}\}/gu, (fullMatch, termWithWhitespace) => { + return text.replace(matcher, (fullMatch, termWithWhitespace) => { const term = termWithWhitespace.trim(); if (term in data) { @@ -25,4 +42,9 @@ module.exports = (text, data) => { // Preserve old behavior: If parameter name not provided, don't replace it. return fullMatch; }); +} + +module.exports = { + getPlaceholderMatcher, + interpolate }; diff --git a/lib/linter/report-translator.js b/lib/linter/report-translator.js index 91fef4d95ec..c4a159a993e 100644 --- a/lib/linter/report-translator.js +++ b/lib/linter/report-translator.js @@ -11,7 +11,7 @@ const assert = require("assert"); const ruleFixer = require("./rule-fixer"); -const interpolate = require("./interpolate"); +const { interpolate } = require("./interpolate"); //------------------------------------------------------------------------------ // Typedefs diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index bc728159f03..261a1bb73bf 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -17,7 +17,8 @@ const equal = require("fast-deep-equal"), Traverser = require("../shared/traverser"), { getRuleOptionsSchema } = require("../config/flat-config-helpers"), - { Linter, SourceCodeFixer, interpolate } = require("../linter"), + { Linter, SourceCodeFixer } = require("../linter"), + { interpolate, getPlaceholderMatcher } = require("../linter/interpolate"), stringify = require("json-stable-stringify-without-jsonify"); const { FlatConfigArray } = require("../config/flat-config-array"); @@ -304,6 +305,39 @@ function throwForbiddenMethodError(methodName, prototype) { }; } +/** + * Extracts names of {{ placeholders }} from the reported message. + * @param {string} message Reported message + * @returns {string[]} Array of placeholder names + */ +function getMessagePlaceholders(message) { + const matcher = getPlaceholderMatcher(); + + return Array.from(message.matchAll(matcher), ([, name]) => name.trim()); +} + +/** + * Returns the placeholders in the reported messages but + * only includes the placeholders available in the raw message and not in the provided data. + * @param {string} message The reported message + * @param {string} raw The raw message specified in the rule meta.messages + * @param {undefined|Record} data The passed + * @returns {string[]} Missing placeholder names + */ +function getUnsubstitutedMessagePlaceholders(message, raw, data = {}) { + const unsubstituted = getMessagePlaceholders(message); + + if (unsubstituted.length === 0) { + return []; + } + + // Remove false positives by only counting placeholders in the raw message, which were not provided in the data matcher or added with a data property + const known = getMessagePlaceholders(raw); + const provided = Object.keys(data); + + return unsubstituted.filter(name => known.includes(name) && !provided.includes(name)); +} + const metaSchemaDescription = ` \t- If the rule has options, set \`meta.schema\` to an array or non-empty object to enable options validation. \t- If the rule doesn't have options, omit \`meta.schema\` to enforce that no options can be passed to the rule. @@ -997,6 +1031,18 @@ class RuleTester { error.messageId, `messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.` ); + + const unsubstitutedPlaceholders = getUnsubstitutedMessagePlaceholders( + message.message, + rule.meta.messages[message.messageId], + error.data + ); + + assert.ok( + unsubstitutedPlaceholders.length === 0, + `The reported message has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property in the context.report() call.` + ); + if (hasOwnProperty(error, "data")) { /* @@ -1096,6 +1142,18 @@ class RuleTester { expectedSuggestion.messageId, `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.` ); + + const unsubstitutedPlaceholders = getUnsubstitutedMessagePlaceholders( + actualSuggestion.desc, + rule.meta.messages[expectedSuggestion.messageId], + expectedSuggestion.data + ); + + assert.ok( + unsubstitutedPlaceholders.length === 0, + `The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property for the suggestion in the context.report() call.` + ); + if (hasOwnProperty(expectedSuggestion, "data")) { const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId]; const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data); diff --git a/tests/fixtures/testers/rule-tester/messageId.js b/tests/fixtures/testers/rule-tester/messageId.js index d7386e395a0..8e60749af6c 100644 --- a/tests/fixtures/testers/rule-tester/messageId.js +++ b/tests/fixtures/testers/rule-tester/messageId.js @@ -34,3 +34,110 @@ module.exports.withMessageOnly = { }; } }; + +module.exports.withMissingData = { + meta: { + messages: { + avoidFoo: "Avoid using variables named '{{ name }}'.", + unused: "An unused key" + } + }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + messageId: "avoidFoo", + }); + } + } + }; + } +}; + +module.exports.withMultipleMissingDataProperties = { + meta: { + messages: { + avoidFoo: "Avoid using {{ type }} named '{{ name }}'.", + unused: "An unused key" + } + }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + messageId: "avoidFoo", + }); + } + } + }; + } +}; + +module.exports.withPlaceholdersInData = { + meta: { + messages: { + avoidFoo: "Avoid using variables named '{{ name }}'.", + unused: "An unused key" + } + }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + messageId: "avoidFoo", + data: { name: '{{ placeholder }}' }, + }); + } + } + }; + } +}; + +module.exports.withSamePlaceholdersInData = { + meta: { + messages: { + avoidFoo: "Avoid using variables named '{{ name }}'.", + unused: "An unused key" + } + }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + messageId: "avoidFoo", + data: { name: '{{ name }}' }, + }); + } + } + }; + } +}; + +module.exports.withNonStringData = { + meta: { + messages: { + avoid: "Avoid using the value '{{ value }}'.", + } + }, + create(context) { + return { + Literal(node) { + if (node.value === 0) { + context.report({ + node, + messageId: "avoid", + data: { value: 0 }, + }); + } + } + }; + } +}; diff --git a/tests/fixtures/testers/rule-tester/suggestions.js b/tests/fixtures/testers/rule-tester/suggestions.js index 34f404d26d8..ccbcff217d4 100644 --- a/tests/fixtures/testers/rule-tester/suggestions.js +++ b/tests/fixtures/testers/rule-tester/suggestions.js @@ -198,3 +198,61 @@ module.exports.withFailingFixer = { }; } }; + +module.exports.withMissingPlaceholderData = { + meta: { + messages: { + avoidFoo: "Avoid using identifiers named '{{ name }}'.", + renameFoo: "Rename identifier 'foo' to '{{ newName }}'" + }, + hasSuggestions: true + }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + messageId: "avoidFoo", + data: { + name: "foo" + }, + suggest: [{ + messageId: "renameFoo", + fix: fixer => fixer.replaceText(node, "bar") + }] + }); + } + } + }; + } +}; + +module.exports.withMultipleMissingPlaceholderDataProperties = { + meta: { + messages: { + avoidFoo: "Avoid using identifiers named '{{ name }}'.", + rename: "Rename identifier '{{ currentName }}' to '{{ newName }}'" + }, + hasSuggestions: true + }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + messageId: "avoidFoo", + data: { + name: "foo" + }, + suggest: [{ + messageId: "rename", + fix: fixer => fixer.replaceText(node, "bar") + }] + }); + } + } + }; + } +}; diff --git a/tests/lib/linter/interpolate.js b/tests/lib/linter/interpolate.js index 04e7140956b..9c96d09117b 100644 --- a/tests/lib/linter/interpolate.js +++ b/tests/lib/linter/interpolate.js @@ -5,12 +5,40 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert; -const interpolate = require("../../../lib/linter/interpolate"); +const { getPlaceholderMatcher, interpolate } = require("../../../lib/linter/interpolate"); //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ +describe("getPlaceholderMatcher", () => { + it("returns a global regular expression", () => { + const matcher = getPlaceholderMatcher(); + + assert.strictEqual(matcher.global, true); + }); + + it("matches text with placeholders", () => { + const matcher = getPlaceholderMatcher(); + + assert.match("{{ placeholder }}", matcher); + }); + + it("does not match text without placeholders", () => { + const matcher = getPlaceholderMatcher(); + + assert.notMatch("no placeholders in sight", matcher); + }); + + it("captures the text inside the placeholder", () => { + const matcher = getPlaceholderMatcher(); + const text = "{{ placeholder }}"; + const matches = Array.from(text.matchAll(matcher)); + + assert.deepStrictEqual(matches, [[text, " placeholder "]]); + }); +}); + describe("interpolate()", () => { it("passes through text without {{ }}", () => { const message = "This is a very important message!"; diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 15820284252..28860af6f7f 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -1897,6 +1897,7 @@ describe("RuleTester", () => { invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] }); }); + it("should assert match between resulting message output if messageId and data provided in both test and result", () => { assert.throws(() => { ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withMetaWithData, { @@ -1906,6 +1907,63 @@ describe("RuleTester", () => { }, "Hydrated message \"Avoid using variables named 'notFoo'.\" does not match \"Avoid using variables named 'foo'.\""); }); + it("should throw if the message has a single unsubstituted placeholder when data is not specified", () => { + assert.throws(() => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withMissingData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] + }); + }, "The reported message has an unsubstituted placeholder 'name'. Please provide the missing value via the 'data' property in the context.report() call."); + }); + + it("should throw if the message has a single unsubstituted placeholders when data is specified", () => { + assert.throws(() => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withMissingData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "name" } }] }] + }); + }, "Hydrated message \"Avoid using variables named 'name'.\" does not match \"Avoid using variables named '{{ name }}'."); + }); + + it("should throw if the message has multiple unsubstituted placeholders when data is not specified", () => { + assert.throws(() => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withMultipleMissingDataProperties, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] + }); + }, "The reported message has unsubstituted placeholders: 'type', 'name'. Please provide the missing values via the 'data' property in the context.report() call."); + }); + + it("should not throw if the data in the message contains placeholders not present in the raw message", () => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withPlaceholdersInData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] + }); + }); + + it("should throw if the data in the message contains the same placeholder and data is not specified", () => { + assert.throws(() => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withSamePlaceholdersInData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] + }); + }, "The reported message has an unsubstituted placeholder 'name'. Please provide the missing value via the 'data' property in the context.report() call."); + }); + + it("should not throw if the data in the message contains the same placeholder and data is specified", () => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withSamePlaceholdersInData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "{{ name }}" } }] }] + }); + }); + + it("should not throw an error for specifying non-string data values", () => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/messageId").withNonStringData, { + valid: [], + invalid: [{ code: "0", errors: [{ messageId: "avoid", data: { value: 0 } }] }] + }); + }); + // messageId/message misconfiguration cases it("should throw if user tests for both message and messageId", () => { assert.throws(() => { @@ -2157,6 +2215,60 @@ describe("RuleTester", () => { }); }); + it("should fail with a single missing data placeholder when data is not specified", () => { + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMissingPlaceholderData, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + messageId: "renameFoo", + output: "var bar;" + }] + }] + }] + }); + }, "The message of the suggestion has an unsubstituted placeholder 'newName'. Please provide the missing value via the 'data' property for the suggestion in the context.report() call."); + }); + + it("should fail with a single missing data placeholder when data is specified", () => { + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMissingPlaceholderData, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + messageId: "renameFoo", + data: { other: "name" }, + output: "var bar;" + }] + }] + }] + }); + }, "The message of the suggestion has an unsubstituted placeholder 'newName'. Please provide the missing value via the 'data' property for the suggestion in the context.report() call."); + }); + + it("should fail with multiple missing data placeholders when data is not specified", () => { + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMultipleMissingPlaceholderDataProperties, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + messageId: "rename", + output: "var bar;" + }] + }] + }] + }); + }, "The message of the suggestion has unsubstituted placeholders: 'currentName', 'newName'. Please provide the missing values via the 'data' property for the suggestion in the context.report() call."); + }); it("should fail when tested using empty suggestion test objects even if the array length is correct", () => { assert.throw(() => { From 5e2b2922aa65bda54b0966d1bf71acda82b3047c Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 9 Feb 2024 23:42:20 +0100 Subject: [PATCH 14/50] chore: upgrade eslint-visitor-keys@4.0.0 (#18105) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d167be6245c..6efb5a31c72 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.0.0", - "eslint-visitor-keys": "^3.4.3", + "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "esquery": "^1.4.2", "esutils": "^2.0.2", From 81f0294e651928b49eb49495b90b54376073a790 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 10 Feb 2024 00:02:46 +0100 Subject: [PATCH 15/50] chore: upgrade espree@10.0.1 (#18106) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6efb5a31c72..f5e0faf0a07 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.0.0", "eslint-visitor-keys": "^4.0.0", - "espree": "^10.0.0", + "espree": "^10.0.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", From 2c62e797a433e5fc298b976872a89c594f88bb19 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 10 Feb 2024 00:23:28 +0100 Subject: [PATCH 16/50] chore: upgrade @eslint/eslintrc@3.0.1 (#18107) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f5e0faf0a07..12a2e273d63 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^3.0.0", + "@eslint/eslintrc": "^3.0.1", "@eslint/js": "9.0.0-alpha.2", "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", From 9870f93e714edefb410fccae1e9924a3c1972a2e Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 9 Feb 2024 23:31:13 +0000 Subject: [PATCH 17/50] chore: package.json update for @eslint/js release --- packages/js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/package.json b/packages/js/package.json index 138c7dd07a5..068a029a055 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/js", - "version": "9.0.0-alpha.2", + "version": "9.0.0-beta.0", "description": "ESLint JavaScript language implementation", "main": "./src/index.js", "scripts": {}, From e40d1d74a5b9788cbec195f4e602b50249f26659 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 10 Feb 2024 00:42:03 +0100 Subject: [PATCH 18/50] chore: upgrade @eslint/js@9.0.0-beta.0 (#18108) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 12a2e273d63..1280e74a92b 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^3.0.1", - "@eslint/js": "9.0.0-alpha.2", + "@eslint/js": "9.0.0-beta.0", "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", From 1bbc495aecbd3e4a4aaf54d7c489191809c1b65b Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 9 Feb 2024 23:53:42 +0000 Subject: [PATCH 19/50] Build: changelog update for 9.0.0-beta.0 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c0fcdad68..bc36ad3bf74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +v9.0.0-beta.0 - February 9, 2024 + +* [`e40d1d7`](https://github.com/eslint/eslint/commit/e40d1d74a5b9788cbec195f4e602b50249f26659) chore: upgrade @eslint/js@9.0.0-beta.0 (#18108) (Milos Djermanovic) +* [`9870f93`](https://github.com/eslint/eslint/commit/9870f93e714edefb410fccae1e9924a3c1972a2e) chore: package.json update for @eslint/js release (Jenkins) +* [`2c62e79`](https://github.com/eslint/eslint/commit/2c62e797a433e5fc298b976872a89c594f88bb19) chore: upgrade @eslint/eslintrc@3.0.1 (#18107) (Milos Djermanovic) +* [`81f0294`](https://github.com/eslint/eslint/commit/81f0294e651928b49eb49495b90b54376073a790) chore: upgrade espree@10.0.1 (#18106) (Milos Djermanovic) +* [`5e2b292`](https://github.com/eslint/eslint/commit/5e2b2922aa65bda54b0966d1bf71acda82b3047c) chore: upgrade eslint-visitor-keys@4.0.0 (#18105) (Milos Djermanovic) +* [`9163646`](https://github.com/eslint/eslint/commit/916364692bae6a93c10b5d48fc1e9de1677d0d09) feat!: Rule Tester checks for missing placeholder data in the message (#18073) (fnx) +* [`53f0f47`](https://github.com/eslint/eslint/commit/53f0f47badffa1b04ec2836f2ae599f4fc464da2) feat: Add loadESLint() API method for v9 (#18097) (Nicholas C. Zakas) +* [`f1c7e6f`](https://github.com/eslint/eslint/commit/f1c7e6fc8ea77fcdae4ad1f8fe1cd104a281d2e9) docs: Switch to Ethical Ads (#18090) (Strek) +* [`15c143f`](https://github.com/eslint/eslint/commit/15c143f96ef164943fd3d39b5ad79d9a4a40de8f) docs: JS Foundation -> OpenJS Foundation in PR template (#18092) (Nicholas C. Zakas) +* [`c4d26fd`](https://github.com/eslint/eslint/commit/c4d26fd3d1f59c1c0f2266664887ad18692039f3) fix: `use-isnan` doesn't report on `SequenceExpression`s (#18059) (StyleShit) +* [`6ea339e`](https://github.com/eslint/eslint/commit/6ea339e658d29791528ab26aabd86f1683cab6c3) docs: add stricter rule test validations to v9 migration guide (#18085) (Milos Djermanovic) +* [`ce838ad`](https://github.com/eslint/eslint/commit/ce838adc3b673e52a151f36da0eedf5876977514) chore: replace dependency npm-run-all with npm-run-all2 ^5.0.0 (#18045) (renovate[bot]) +* [`3c816f1`](https://github.com/eslint/eslint/commit/3c816f193eecace5efc6166efa2852a829175ef8) docs: use relative link from CLI to core concepts (#18083) (Milos Djermanovic) +* [`54df731`](https://github.com/eslint/eslint/commit/54df731174d2528170560d1f765e1336eca0a8bd) chore: update dependency markdownlint-cli to ^0.39.0 (#18084) (renovate[bot]) +* [`9458735`](https://github.com/eslint/eslint/commit/9458735381269d12b24f76e1b2b6fda1bc5a509b) docs: fix malformed `eslint` config comments in rule examples (#18078) (Francesco Trotta) +* [`07a1ada`](https://github.com/eslint/eslint/commit/07a1ada7166b76c7af6186f4c5e5de8b8532edba) docs: link from `--fix` CLI doc to the relevant core concept (#18080) (Bryan Mishkin) +* [`8f06a60`](https://github.com/eslint/eslint/commit/8f06a606845f40aaf0fea1fd83d5930747c5acec) chore: update dependency shelljs to ^0.8.5 (#18079) (Francesco Trotta) +* [`b844324`](https://github.com/eslint/eslint/commit/b844324e4e8f511c9985a96c7aca063269df9570) docs: Update team responsibilities (#18048) (Nicholas C. Zakas) +* [`aadfb60`](https://github.com/eslint/eslint/commit/aadfb609f1b847e492fc3b28ced62f830fe7f294) docs: document languageOptions and other v9 changes for context (#18074) (fnx) +* [`3c4d51d`](https://github.com/eslint/eslint/commit/3c4d51d55fa5435ab18b6bf46f6b97df0f480ae7) feat!: default for `enforceForClassMembers` in `no-useless-computed-key` (#18054) (Francesco Trotta) +* [`47e60f8`](https://github.com/eslint/eslint/commit/47e60f85e0c3f275207bb4be9b5947166a190477) feat!: Stricter rule test validations (#17654) (fnx) +* [`1a94589`](https://github.com/eslint/eslint/commit/1a945890105d307541dcbff15f6438c19b476ade) feat!: `no-unused-vars` default caughtErrors to 'all' (#18043) (Josh Goldberg ✨) +* [`857e242`](https://github.com/eslint/eslint/commit/857e242584227181ecb8af79fc6bc236b9975228) docs: tweak explanation for meta.docs rule properties (#18057) (Bryan Mishkin) +* [`10485e8`](https://github.com/eslint/eslint/commit/10485e8b961d045514bc1e34227cf09867a6c4b7) docs: recommend messageId over message for reporting rule violations (#18050) (Bryan Mishkin) +* [`98b5ab4`](https://github.com/eslint/eslint/commit/98b5ab406bac6279eadd84e8a5fd5a01fc586ff1) docs: Update README (GitHub Actions Bot) +* [`93ffe30`](https://github.com/eslint/eslint/commit/93ffe30da5e2127e336c1c22e69e09ec0558a8e6) chore: update dependency file-entry-cache to v8 (#17903) (renovate[bot]) +* [`505fbf4`](https://github.com/eslint/eslint/commit/505fbf4b35c14332bffb0c838cce4843a00fad68) docs: update `no-restricted-imports` rule (#18015) (Tanuj Kanti) +* [`2d11d46`](https://github.com/eslint/eslint/commit/2d11d46e890a9f1b5f639b8ee034ffa9bd453e42) feat: add suggestions to `use-isnan` in binary expressions (#17996) (StyleShit) +* [`c25b4af`](https://github.com/eslint/eslint/commit/c25b4aff1fe35e5bd9d4fcdbb45b739b6d253828) docs: Update README (GitHub Actions Bot) + v9.0.0-alpha.2 - January 26, 2024 * [`6ffdcbb`](https://github.com/eslint/eslint/commit/6ffdcbb8c51956054d3f81c5ce446c15dcd51a6f) chore: upgrade @eslint/js@9.0.0-alpha.2 (#18038) (Milos Djermanovic) From 428dbdbef367e17edef7ba648fba0d37c860be9c Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 9 Feb 2024 23:53:43 +0000 Subject: [PATCH 20/50] 9.0.0-beta.0 --- docs/package.json | 2 +- docs/src/_data/rules.json | 2 +- docs/src/_data/rules_meta.json | 1 + .../formatters/html-formatter-example.html | 2 +- docs/src/use/formatters/index.md | 52 ++++++++++++++++++- package.json | 2 +- 6 files changed, 55 insertions(+), 6 deletions(-) diff --git a/docs/package.json b/docs/package.json index 24d84f499f8..cb11570a689 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,7 +1,7 @@ { "name": "docs-eslint", "private": true, - "version": "9.0.0-alpha.2", + "version": "9.0.0-beta.0", "description": "", "main": "index.js", "keywords": [], diff --git a/docs/src/_data/rules.json b/docs/src/_data/rules.json index 90d297ff540..79374b96435 100644 --- a/docs/src/_data/rules.json +++ b/docs/src/_data/rules.json @@ -398,7 +398,7 @@ "description": "Require calls to `isNaN()` when checking for `NaN`", "recommended": true, "fixable": false, - "hasSuggestions": false + "hasSuggestions": true }, { "name": "valid-typeof", diff --git a/docs/src/_data/rules_meta.json b/docs/src/_data/rules_meta.json index 3b8101f1203..647481b3fe5 100644 --- a/docs/src/_data/rules_meta.json +++ b/docs/src/_data/rules_meta.json @@ -2594,6 +2594,7 @@ "fixable": "whitespace" }, "use-isnan": { + "hasSuggestions": true, "type": "problem", "docs": { "description": "Require calls to `isNaN()` when checking for `NaN`", diff --git a/docs/src/use/formatters/html-formatter-example.html b/docs/src/use/formatters/html-formatter-example.html index d26023346cf..9949da4e90f 100644 --- a/docs/src/use/formatters/html-formatter-example.html +++ b/docs/src/use/formatters/html-formatter-example.html @@ -118,7 +118,7 @@

ESLint Report

- 8 problems (4 errors, 4 warnings) - Generated on Fri Jan 26 2024 20:36:42 GMT+0000 (Coordinated Universal Time) + 8 problems (4 errors, 4 warnings) - Generated on Fri Feb 09 2024 23:53:44 GMT+0000 (Coordinated Universal Time)
diff --git a/docs/src/use/formatters/index.md b/docs/src/use/formatters/index.md index c7d3cb37a46..1c842b9d6c1 100644 --- a/docs/src/use/formatters/index.md +++ b/docs/src/use/formatters/index.md @@ -100,7 +100,31 @@ Example output (formatted for easier reading): "nodeType": "BinaryExpression", "messageId": "comparisonWithNaN", "endLine": 2, - "endColumn": 17 + "endColumn": 17, + "suggestions": [ + { + "messageId": "replaceWithIsNaN", + "fix": { + "range": [ + 29, + 37 + ], + "text": "!Number.isNaN(i)" + }, + "desc": "Replace with Number.isNaN." + }, + { + "messageId": "replaceWithCastingAndIsNaN", + "fix": { + "range": [ + 29, + 37 + ], + "text": "!Number.isNaN(Number(i))" + }, + "desc": "Replace with Number.isNaN cast to a Number." + } + ] }, { "ruleId": "space-unary-ops", @@ -684,7 +708,31 @@ Example output (formatted for easier reading): "nodeType": "BinaryExpression", "messageId": "comparisonWithNaN", "endLine": 2, - "endColumn": 17 + "endColumn": 17, + "suggestions": [ + { + "messageId": "replaceWithIsNaN", + "fix": { + "range": [ + 29, + 37 + ], + "text": "!Number.isNaN(i)" + }, + "desc": "Replace with Number.isNaN." + }, + { + "messageId": "replaceWithCastingAndIsNaN", + "fix": { + "range": [ + 29, + 37 + ], + "text": "!Number.isNaN(Number(i))" + }, + "desc": "Replace with Number.isNaN cast to a Number." + } + ] }, { "ruleId": "space-unary-ops", diff --git a/package.json b/package.json index 1280e74a92b..24a14e710cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "9.0.0-alpha.2", + "version": "9.0.0-beta.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From d8068ec70fac050e900dc400510a4ad673e17633 Mon Sep 17 00:00:00 2001 From: Svetlana Date: Mon, 12 Feb 2024 13:44:14 +0400 Subject: [PATCH 21/50] docs: Update link for schema examples (#18112) --- docs/src/extend/custom-rules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/extend/custom-rules.md b/docs/src/extend/custom-rules.md index fa66bcb13c9..9b99121d22e 100644 --- a/docs/src/extend/custom-rules.md +++ b/docs/src/extend/custom-rules.md @@ -794,7 +794,7 @@ module.exports = { **Note:** If your rule schema uses JSON schema [`$ref`](https://json-schema.org/understanding-json-schema/structuring.html#ref) properties, you must use the full JSON Schema object rather than the array of positional property schemas. This is because ESLint transforms the array shorthand into a single schema without updating references that makes them incorrect (they are ignored). -To learn more about JSON Schema, we recommend looking at some examples on the [JSON Schema website](https://json-schema.org/learn/), or reading the free [Understanding JSON Schema](https://json-schema.org/understanding-json-schema/) ebook. +To learn more about JSON Schema, we recommend looking at some examples on the [JSON Schema website](https://json-schema.org/learn/miscellaneous-examples), or reading the free [Understanding JSON Schema](https://json-schema.org/understanding-json-schema/) ebook. ### Accessing Shebangs From 9aa4df3f4d85960eee72923f3b9bfc88e62f04fb Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Tue, 13 Feb 2024 11:19:35 +0100 Subject: [PATCH 22/50] refactor: remove `globals` dependency (#18115) --- lib/rules/no-constant-binary-expression.js | 5 ++--- lib/rules/no-extend-native.js | 3 +-- lib/rules/utils/ast-utils.js | 9 ++++++++ package.json | 2 +- tests/lib/rules/utils/ast-utils.js | 26 ++++++++++++++++++++++ 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/lib/rules/no-constant-binary-expression.js b/lib/rules/no-constant-binary-expression.js index 775265f190d..2f3013b74f6 100644 --- a/lib/rules/no-constant-binary-expression.js +++ b/lib/rules/no-constant-binary-expression.js @@ -5,8 +5,7 @@ "use strict"; -const globals = require("globals"); -const { isNullLiteral, isConstant, isReferenceToGlobalVariable, isLogicalAssignmentOperator } = require("./utils/ast-utils"); +const { isNullLiteral, isConstant, isReferenceToGlobalVariable, isLogicalAssignmentOperator, ECMASCRIPT_GLOBALS } = require("./utils/ast-utils"); const NUMERIC_OR_STRING_BINARY_OPERATORS = new Set(["+", "-", "*", "/", "%", "|", "^", "&", "**", "<<", ">>", ">>>"]); @@ -376,7 +375,7 @@ function isAlwaysNew(scope, node) { * Catching these is especially useful for primitive constructors * which return boxed values, a surprising gotcha' in JavaScript. */ - return Object.hasOwn(globals.builtin, node.callee.name) && + return Object.hasOwn(ECMASCRIPT_GLOBALS, node.callee.name) && isReferenceToGlobalVariable(scope, node.callee); } case "Literal": diff --git a/lib/rules/no-extend-native.js b/lib/rules/no-extend-native.js index fcbb3855725..a69dd690039 100644 --- a/lib/rules/no-extend-native.js +++ b/lib/rules/no-extend-native.js @@ -10,7 +10,6 @@ //------------------------------------------------------------------------------ const astUtils = require("./utils/ast-utils"); -const globals = require("globals"); //------------------------------------------------------------------------------ // Rule Definition @@ -54,7 +53,7 @@ module.exports = { const sourceCode = context.sourceCode; const exceptions = new Set(config.exceptions || []); const modifiedBuiltins = new Set( - Object.keys(globals.builtin) + Object.keys(astUtils.ECMASCRIPT_GLOBALS) .filter(builtin => builtin[0].toUpperCase() === builtin[0]) .filter(builtin => !exceptions.has(builtin)) ); diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js index 4b074e0198f..ed9a31af34c 100644 --- a/lib/rules/utils/ast-utils.js +++ b/lib/rules/utils/ast-utils.js @@ -19,6 +19,8 @@ const { lineBreakPattern, shebangPattern } = require("../../shared/ast-utils"); +const globals = require("../../../conf/globals"); +const { LATEST_ECMA_VERSION } = require("../../../conf/ecma-version"); //------------------------------------------------------------------------------ // Helpers @@ -46,6 +48,12 @@ const OCTAL_OR_NON_OCTAL_DECIMAL_ESCAPE_PATTERN = /^(?:[^\\]|\\.)*\\(?:[1-9]|0[0 const LOGICAL_ASSIGNMENT_OPERATORS = new Set(["&&=", "||=", "??="]); +/** + * All builtin global variables defined in the latest ECMAScript specification. + * @type {Record} Key is the name of the variable. Value is `true` if the variable is considered writable, `false` otherwise. + */ +const ECMASCRIPT_GLOBALS = globals[`es${LATEST_ECMA_VERSION}`]; + /** * Checks reference if is non initializer and writable. * @param {Reference} reference A reference to check. @@ -1133,6 +1141,7 @@ module.exports = { LINEBREAK_MATCHER: lineBreakPattern, SHEBANG_MATCHER: shebangPattern, STATEMENT_LIST_PARENTS, + ECMASCRIPT_GLOBALS, /** * Determines whether two adjacent tokens are on the same line. diff --git a/package.json b/package.json index 24a14e710cf..9cf566264fb 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,6 @@ "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", @@ -128,6 +127,7 @@ "fast-glob": "^3.2.11", "fs-teardown": "^0.1.3", "glob": "^10.0.0", + "globals": "^14.0.0", "got": "^11.8.3", "gray-matter": "^4.0.3", "js-yaml": "^4.1.0", diff --git a/tests/lib/rules/utils/ast-utils.js b/tests/lib/rules/utils/ast-utils.js index 6684df35f00..7700f2ce7e9 100644 --- a/tests/lib/rules/utils/ast-utils.js +++ b/tests/lib/rules/utils/ast-utils.js @@ -59,6 +59,32 @@ describe("ast-utils", () => { }); }); + describe("ECMASCRIPT_GLOBALS", () => { + it("should contain es3 globals", () => { + assert.ownInclude(astUtils.ECMASCRIPT_GLOBALS, { Object: false }); + }); + + it("should contain es5 globals", () => { + assert.ownInclude(astUtils.ECMASCRIPT_GLOBALS, { JSON: false }); + }); + + it("should contain es2015 globals", () => { + assert.ownInclude(astUtils.ECMASCRIPT_GLOBALS, { Promise: false }); + }); + + it("should contain es2017 globals", () => { + assert.ownInclude(astUtils.ECMASCRIPT_GLOBALS, { SharedArrayBuffer: false }); + }); + + it("should contain es2020 globals", () => { + assert.ownInclude(astUtils.ECMASCRIPT_GLOBALS, { BigInt: false }); + }); + + it("should contain es2021 globals", () => { + assert.ownInclude(astUtils.ECMASCRIPT_GLOBALS, { WeakRef: false }); + }); + }); + describe("isTokenOnSameLine", () => { it("should return false if the tokens are not on the same line", () => { linter.defineRule("checker", { From f95cd27679eef228173e27e170429c9710c939b3 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 14 Feb 2024 11:47:14 +0100 Subject: [PATCH 23/50] docs: Disallow multiple rule configuration comments in the same example (#18116) --- docs/src/rules/func-name-matching.md | 9 ++++----- docs/src/rules/lines-around-comment.md | 18 ++++++++++++++++-- tests/fixtures/bad-examples.md | 10 ++++++++++ tests/tools/check-rule-examples.js | 3 ++- tools/check-rule-examples.js | 9 +++++++++ 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/docs/src/rules/func-name-matching.md b/docs/src/rules/func-name-matching.md index 8ef1a1c8540..82748623f76 100644 --- a/docs/src/rules/func-name-matching.md +++ b/docs/src/rules/func-name-matching.md @@ -54,8 +54,7 @@ Examples of **correct** code for this rule: ```js /*eslint func-name-matching: "error"*/ -/*eslint func-name-matching: ["error", "always"]*/ // these are equivalent -/*eslint-env es6*/ +// equivalent to /*eslint func-name-matching: ["error", "always"]*/ var foo = function foo() {}; var foo = function() {}; @@ -157,7 +156,7 @@ Examples of **correct** code for the `{ considerPropertyDescriptor: true }` opti ```js /*eslint func-name-matching: ["error", { "considerPropertyDescriptor": true }]*/ -/*eslint func-name-matching: ["error", "always", { "considerPropertyDescriptor": true }]*/ // these are equivalent +// equivalent to /*eslint func-name-matching: ["error", "always", { "considerPropertyDescriptor": true }]*/ var obj = {}; Object.create(obj, {foo:{value: function foo() {}}}); Object.defineProperty(obj, 'bar', {value: function bar() {}}); @@ -173,7 +172,7 @@ Examples of **incorrect** code for the `{ considerPropertyDescriptor: true }` op ```js /*eslint func-name-matching: ["error", { "considerPropertyDescriptor": true }]*/ -/*eslint func-name-matching: ["error", "always", { "considerPropertyDescriptor": true }]*/ // these are equivalent +// equivalent to /*eslint func-name-matching: ["error", "always", { "considerPropertyDescriptor": true }]*/ var obj = {}; Object.create(obj, {foo:{value: function bar() {}}}); Object.defineProperty(obj, 'bar', {value: function baz() {}}); @@ -193,7 +192,7 @@ Examples of **incorrect** code for the `{ includeCommonJSModuleExports: true }` ```js /*eslint func-name-matching: ["error", { "includeCommonJSModuleExports": true }]*/ -/*eslint func-name-matching: ["error", "always", { "includeCommonJSModuleExports": true }]*/ // these are equivalent +// equivalent to /*eslint func-name-matching: ["error", "always", { "includeCommonJSModuleExports": true }]*/ module.exports = function foo(name) {}; module['exports'] = function foo(name) {}; diff --git a/docs/src/rules/lines-around-comment.md b/docs/src/rules/lines-around-comment.md index 23582fefd60..7378c4882a2 100644 --- a/docs/src/rules/lines-around-comment.md +++ b/docs/src/rules/lines-around-comment.md @@ -653,9 +653,9 @@ const [ ### ignorePattern -By default this rule ignores comments starting with the following words: `eslint`, `jshint`, `jslint`, `istanbul`, `global`, `exported`, `jscs`. To ignore more comments in addition to the defaults, set the `ignorePattern` option to a string pattern that will be passed to the [`RegExp` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp). +By default this rule ignores comments starting with the following words: `eslint`, `jshint`, `jslint`, `istanbul`, `global`, `exported`, `jscs`. -Examples of **correct** code for the `ignorePattern` option: +Examples of **correct** code for this rule: ::: correct @@ -665,9 +665,23 @@ Examples of **correct** code for the `ignorePattern` option: foo(); /* jshint mentioned in this comment */ bar(); +``` + +::: + +To ignore more comments in addition to the defaults, set the `ignorePattern` option to a string pattern that will be passed to the [`RegExp` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp). + +Examples of **correct** code for the `ignorePattern` option: +::: correct + +```js /*eslint lines-around-comment: ["error", { "ignorePattern": "pragma" }] */ +foo(); +/* jshint mentioned in this comment */ +bar(); + foo(); /* a valid comment using pragma in it */ ``` diff --git a/tests/fixtures/bad-examples.md b/tests/fixtures/bad-examples.md index 872a1f79bf8..147dfe38e15 100644 --- a/tests/fixtures/bad-examples.md +++ b/tests/fixtures/bad-examples.md @@ -42,3 +42,13 @@ const foo = "baz"; ``` ::: + +:::correct + +```js +/* eslint no-restricted-syntax: "error" */ + +/* eslint no-restricted-syntax: ["error", "ArrayPattern"] */ +``` + +::: diff --git a/tests/tools/check-rule-examples.js b/tests/tools/check-rule-examples.js index d93bcbe1931..0326ee91633 100644 --- a/tests/tools/check-rule-examples.js +++ b/tests/tools/check-rule-examples.js @@ -77,8 +77,9 @@ describe("check-rule-examples", () => { "\x1B[0m \x1B[2m23:7\x1B[22m \x1B[31merror\x1B[39m Syntax error: Identifier 'foo' has already been declared\x1B[0m\n" + "\x1B[0m \x1B[2m31:1\x1B[22m \x1B[31merror\x1B[39m Example code should contain a configuration comment like /* eslint no-restricted-syntax: \"error\" */\x1B[0m\n" + "\x1B[0m \x1B[2m41:1\x1B[22m \x1B[31merror\x1B[39m Failed to parse JSON from ' doesn't allow this comment'\x1B[0m\n" + + "\x1B[0m \x1B[2m51:1\x1B[22m \x1B[31merror\x1B[39m Duplicate /* eslint no-restricted-syntax */ configuration comment. Each example should contain only one. Split this example into multiple examples\x1B[0m\n" + "\x1B[0m\x1B[0m\n" + - "\x1B[0m\x1B[31m\x1B[1mβœ– 6 problems (6 errors, 0 warnings)\x1B[22m\x1B[39m\x1B[0m\n" + + "\x1B[0m\x1B[31m\x1B[1mβœ– 7 problems (7 errors, 0 warnings)\x1B[22m\x1B[39m\x1B[0m\n" + "\x1B[0m\x1B[31m\x1B[1m\x1B[22m\x1B[39m\x1B[0m\n"; assert.strictEqual(normalizedStderr, expectedStderr); diff --git a/tools/check-rule-examples.js b/tools/check-rule-examples.js index b475d46b872..4365da71fb2 100644 --- a/tools/check-rule-examples.js +++ b/tools/check-rule-examples.js @@ -96,6 +96,15 @@ async function findProblems(filename) { parseError.line += codeBlockToken.map[0] + 1; problems.push(parseError); } else if (Object.hasOwn(parseResult.config, title)) { + if (hasRuleConfigComment) { + problems.push({ + fatal: false, + severity: 2, + message: `Duplicate /* eslint ${title} */ configuration comment. Each example should contain only one. Split this example into multiple examples.`, + line: codeBlockToken.map[0] + 1 + comment.loc.start.line, + column: comment.loc.start.column + 1 + }); + } hasRuleConfigComment = true; } } From 1a65d3e4a6ee16e3f607d69b998a08c3fed505ca Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 14 Feb 2024 18:57:20 +0100 Subject: [PATCH 24/50] chore: export `base` config from `eslint-config-eslint` (#18119) --- packages/eslint-config-eslint/README.md | 22 ++++++++++++++++++++++ packages/eslint-config-eslint/package.json | 1 + 2 files changed, 23 insertions(+) diff --git a/packages/eslint-config-eslint/README.md b/packages/eslint-config-eslint/README.md index 645a1938260..f3125565c6f 100644 --- a/packages/eslint-config-eslint/README.md +++ b/packages/eslint-config-eslint/README.md @@ -51,6 +51,28 @@ module.exports = [ ]; ``` +### Base config + +Note that the above configurations are intended for files that will run in Node.js. For files that will not run in Node.js, you should use the `base` config. + +Here's an example of an `eslint.config.js` file for a website project with scripts that run in browser and CommonJS configuration files and tools that run in Node.js: + +```js +const eslintConfigESLintBase = require("eslint-config-eslint/base"); +const eslintConfigESLintCJS = require("eslint-config-eslint/cjs"); + +module.exports = [ + ...eslintConfigESLintBase.map(config => ({ + ...config, + files: ["scripts/*.js"] + })), + ...eslintConfigESLintCJS.map(config => ({ + ...config, + files: ["eslint.config.js", ".eleventy.js", "tools/*.js"] + })) +]; +``` + ### Where to ask for help? Open a [discussion](https://github.com/eslint/eslint/discussions) or stop by our [Discord server](https://eslint.org/chat) instead of filing an issue. diff --git a/packages/eslint-config-eslint/package.json b/packages/eslint-config-eslint/package.json index 64872df6be1..f3632fad9f3 100644 --- a/packages/eslint-config-eslint/package.json +++ b/packages/eslint-config-eslint/package.json @@ -6,6 +6,7 @@ "exports": { "./package.json": "./package.json", ".": "./index.js", + "./base": "./base.js", "./cjs": "./cjs.js", "./eslintrc": "./eslintrc.js" }, From 74124c20287fac1995c3f4e553f0723c066f311d Mon Sep 17 00:00:00 2001 From: StyleShit <32631382+StyleShit@users.noreply.github.com> Date: Wed, 14 Feb 2024 20:01:48 +0200 Subject: [PATCH 25/50] feat: add suggestions to `use-isnan` in `indexOf` & `lastIndexOf` calls (#18063) * feat: add suggestions to `use-isnan` in `indexOf` & `lastIndexOf` calls Closes #17978 * wip * wip * shorten names * fix text * use const * wip * isSuggestable * computed --- lib/rules/use-isnan.js | 37 +++++- tests/lib/rules/use-isnan.js | 230 +++++++++++++++++++++++++++++++---- 2 files changed, 241 insertions(+), 26 deletions(-) diff --git a/lib/rules/use-isnan.js b/lib/rules/use-isnan.js index 357c51d13d6..06b2284ecd5 100644 --- a/lib/rules/use-isnan.js +++ b/lib/rules/use-isnan.js @@ -74,7 +74,8 @@ module.exports = { caseNaN: "'case NaN' can never match. Use Number.isNaN before the switch.", indexOfNaN: "Array prototype method '{{ methodName }}' cannot find NaN.", replaceWithIsNaN: "Replace with Number.isNaN.", - replaceWithCastingAndIsNaN: "Replace with Number.isNaN cast to a Number." + replaceWithCastingAndIsNaN: "Replace with Number.isNaN and cast to a Number.", + replaceWithFindIndex: "Replace with Array.prototype.{{ methodName }}." } }, @@ -126,10 +127,10 @@ module.exports = { const NaNNode = isNaNIdentifier(node.left) ? node.left : node.right; const isSequenceExpression = NaNNode.type === "SequenceExpression"; - const isFixable = fixableOperators.has(node.operator) && !isSequenceExpression; + const isSuggestable = fixableOperators.has(node.operator) && !isSequenceExpression; const isCastable = castableOperators.has(node.operator); - if (isFixable) { + if (isSuggestable) { suggestedFixes.push({ messageId: "replaceWithIsNaN", fix: getBinaryExpressionFixer(node, value => `Number.isNaN(${value})`) @@ -184,7 +185,35 @@ module.exports = { node.arguments.length === 1 && isNaNIdentifier(node.arguments[0]) ) { - context.report({ node, messageId: "indexOfNaN", data: { methodName } }); + + /* + * To retain side effects, it's essential to address `NaN` beforehand, which + * is not possible with fixes like `arr.findIndex(Number.isNaN)`. + */ + const isSuggestable = node.arguments[0].type !== "SequenceExpression"; + const suggestedFixes = []; + + if (isSuggestable) { + const shouldWrap = callee.computed; + const findIndexMethod = methodName === "indexOf" ? "findIndex" : "findLastIndex"; + const propertyName = shouldWrap ? `"${findIndexMethod}"` : findIndexMethod; + + suggestedFixes.push({ + messageId: "replaceWithFindIndex", + data: { methodName: findIndexMethod }, + fix: fixer => [ + fixer.replaceText(callee.property, propertyName), + fixer.replaceText(node.arguments[0], "Number.isNaN") + ] + }); + } + + context.report({ + node, + messageId: "indexOfNaN", + data: { methodName }, + suggest: suggestedFixes + }); } } } diff --git a/tests/lib/rules/use-isnan.js b/tests/lib/rules/use-isnan.js index 337f1f4d146..6cb61821952 100644 --- a/tests/lib/rules/use-isnan.js +++ b/tests/lib/rules/use-isnan.js @@ -987,122 +987,308 @@ ruleTester.run("use-isnan", rule, { { code: "foo.indexOf(NaN)", options: [{ enforceForIndexOf: true }], - errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }] + errors: [{ + messageId: "indexOfNaN", + type: "CallExpression", + data: { methodName: "indexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findIndex" }, + output: "foo.findIndex(Number.isNaN)" + }] + }] }, { code: "foo.lastIndexOf(NaN)", options: [{ enforceForIndexOf: true }], - errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }] + errors: [{ + messageId: "indexOfNaN", + type: "CallExpression", + data: { methodName: "lastIndexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findLastIndex" }, + output: "foo.findLastIndex(Number.isNaN)" + }] + }] }, { code: "foo['indexOf'](NaN)", options: [{ enforceForIndexOf: true }], - errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }] + errors: [{ + messageId: "indexOfNaN", + type: "CallExpression", + data: { methodName: "indexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findIndex" }, + output: 'foo["findIndex"](Number.isNaN)' + }] + }] + }, + { + code: "foo[`indexOf`](NaN)", + options: [{ enforceForIndexOf: true }], + errors: [{ + messageId: "indexOfNaN", + type: "CallExpression", + data: { methodName: "indexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findIndex" }, + output: 'foo["findIndex"](Number.isNaN)' + }] + }] }, { code: "foo['lastIndexOf'](NaN)", options: [{ enforceForIndexOf: true }], - errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }] + errors: [{ + messageId: "indexOfNaN", + type: "CallExpression", + data: { methodName: "lastIndexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findLastIndex" }, + output: 'foo["findLastIndex"](Number.isNaN)' + }] + }] }, { code: "foo().indexOf(NaN)", options: [{ enforceForIndexOf: true }], - errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }] + errors: [{ + messageId: "indexOfNaN", + type: "CallExpression", + data: { methodName: "indexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findIndex" }, + output: "foo().findIndex(Number.isNaN)" + }] + }] }, { code: "foo.bar.lastIndexOf(NaN)", options: [{ enforceForIndexOf: true }], - errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }] + errors: [{ + messageId: "indexOfNaN", + type: "CallExpression", + data: { methodName: "lastIndexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findLastIndex" }, + output: "foo.bar.findLastIndex(Number.isNaN)" + }] + }] }, { code: "foo.indexOf?.(NaN)", options: [{ enforceForIndexOf: true }], languageOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "indexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findIndex" }, + output: "foo.findIndex?.(Number.isNaN)" + }] + }] }, { code: "foo?.indexOf(NaN)", options: [{ enforceForIndexOf: true }], languageOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "indexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findIndex" }, + output: "foo?.findIndex(Number.isNaN)" + }] + }] }, { code: "(foo?.indexOf)(NaN)", options: [{ enforceForIndexOf: true }], languageOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "indexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findIndex" }, + output: "(foo?.findIndex)(Number.isNaN)" + }] + }] }, { code: "foo.indexOf(Number.NaN)", options: [{ enforceForIndexOf: true }], - errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }] + errors: [{ + messageId: "indexOfNaN", + type: "CallExpression", + data: { methodName: "indexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findIndex" }, + output: "foo.findIndex(Number.isNaN)" + }] + }] }, { code: "foo.lastIndexOf(Number.NaN)", options: [{ enforceForIndexOf: true }], - errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }] + errors: [{ + messageId: "indexOfNaN", + type: "CallExpression", + data: { methodName: "lastIndexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findLastIndex" }, + output: "foo.findLastIndex(Number.isNaN)" + }] + }] }, { code: "foo['indexOf'](Number.NaN)", options: [{ enforceForIndexOf: true }], - errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }] + errors: [{ + messageId: "indexOfNaN", + type: "CallExpression", + data: { methodName: "indexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findIndex" }, + output: 'foo["findIndex"](Number.isNaN)' + }] + }] }, { code: "foo['lastIndexOf'](Number.NaN)", options: [{ enforceForIndexOf: true }], - errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }] + errors: [{ + messageId: "indexOfNaN", + type: "CallExpression", + data: { methodName: "lastIndexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findLastIndex" }, + output: 'foo["findLastIndex"](Number.isNaN)' + }] + }] }, { code: "foo().indexOf(Number.NaN)", options: [{ enforceForIndexOf: true }], - errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }] + errors: [{ + messageId: "indexOfNaN", + type: "CallExpression", + data: { methodName: "indexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findIndex" }, + output: "foo().findIndex(Number.isNaN)" + }] + }] }, { code: "foo.bar.lastIndexOf(Number.NaN)", options: [{ enforceForIndexOf: true }], - errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }] + errors: [{ + messageId: "indexOfNaN", + type: "CallExpression", + data: { methodName: "lastIndexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findLastIndex" }, + output: "foo.bar.findLastIndex(Number.isNaN)" + }] + }] }, { code: "foo.indexOf?.(Number.NaN)", options: [{ enforceForIndexOf: true }], languageOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "indexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findIndex" }, + output: "foo.findIndex?.(Number.isNaN)" + }] + }] }, { code: "foo?.indexOf(Number.NaN)", options: [{ enforceForIndexOf: true }], languageOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "indexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findIndex" }, + output: "foo?.findIndex(Number.isNaN)" + }] + }] }, { code: "(foo?.indexOf)(Number.NaN)", options: [{ enforceForIndexOf: true }], languageOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "indexOf" }, + suggestions: [{ + messageId: "replaceWithFindIndex", + data: { methodName: "findIndex" }, + output: "(foo?.findIndex)(Number.isNaN)" + }] + }] }, { code: "foo.indexOf((1, NaN))", options: [{ enforceForIndexOf: true }], languageOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "indexOf" }, + suggestions: [] + }] }, { code: "foo.indexOf((1, Number.NaN))", options: [{ enforceForIndexOf: true }], languageOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "indexOf" }, + suggestions: [] + }] }, { code: "foo.lastIndexOf((1, NaN))", options: [{ enforceForIndexOf: true }], languageOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "indexOfNaN", data: { methodName: "lastIndexOf" } }] + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "lastIndexOf" }, + suggestions: [] + }] }, { code: "foo.lastIndexOf((1, Number.NaN))", options: [{ enforceForIndexOf: true }], languageOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "indexOfNaN", data: { methodName: "lastIndexOf" } }] + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "lastIndexOf" }, + suggestions: [] + }] } ] }); From 73a5f0641b43e169247b0000f44a366ee6bbc4f2 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Thu, 15 Feb 2024 08:06:39 +0000 Subject: [PATCH 26/50] docs: Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ec49748cc2..8819c68686f 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Chrome Frameworks Fund Automattic

Gold Sponsors

Salesforce Airbnb

Silver Sponsors

JetBrains Liftoff American Express Workleap

Bronze Sponsors

-

ThemeIsle Anagram Solver Icons8 Discord Transloadit Ignition Nx HeroCoders Nextbase Starter Kit

+

notion ThemeIsle Anagram Solver Icons8 Discord Transloadit Ignition Nx HeroCoders Nextbase Starter Kit

## Technology Sponsors From cace6d0a3afa5c84b18abee4ef8c598125143461 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Sat, 17 Feb 2024 01:51:08 +0530 Subject: [PATCH 27/50] ci: add PR labeler action (#18109) * ci: standardize auto labeling * chore: fix key name * chore: add ESLint label * chore: update github actions label * ci: update labeler config --- .github/labeler.yml | 29 +++++++++++++++++++++++++++++ .github/workflows/pr-labeler.yml | 12 ++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/pr-labeler.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000000..4fa6f8f24a7 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,29 @@ +documentation: +- any: + - changed-files: + - all-globs-to-all-files: ['docs/**', '!lib/rules/**'] + +rule: +- any: + - changed-files: + - any-glob-to-any-file: ['lib/rules/**'] + +cli: +- any: + - changed-files: + - any-glob-to-any-file: ['lib/cli.js', 'lib/options.js', 'lib/cli-engine/**', 'lib/eslint/**'] + +core: +- any: + - changed-files: + - any-glob-to-any-file: ['lib/{config,eslint,linter,rule-tester,source-code}/**', 'lib/api.js'] + +formatter: +- any: + - changed-files: + - any-glob-to-any-file: ['lib/cli-engine/formatters/**'] + +"github actions": +- any: + - changed-files: + - any-glob-to-any-file: ['.github/workflows/**'] diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml new file mode 100644 index 00000000000..8c9e03ca656 --- /dev/null +++ b/.github/workflows/pr-labeler.yml @@ -0,0 +1,12 @@ +name: "Pull Request Labeler" +on: pull_request_target +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 + with: + sync-labels: .github/labeler.yml From bf0c7effdba51c48b929d06ce1965408a912dc77 Mon Sep 17 00:00:00 2001 From: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> Date: Sun, 18 Feb 2024 16:12:19 +0530 Subject: [PATCH 28/50] ci: fix sync-labels value of pr-labeler (#18124) --- .github/workflows/pr-labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 8c9e03ca656..d58080259b8 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -9,4 +9,4 @@ jobs: steps: - uses: actions/labeler@v5 with: - sync-labels: .github/labeler.yml + sync-labels: true From 66f52e276c31487424bcf54e490c4ac7ef70f77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Sun, 18 Feb 2024 11:34:10 -0500 Subject: [PATCH 29/50] chore: remove unused tools rule-types.json, update-rule-types.js (#18125) * chore: remove unused tools: rule-types.json, update-rule-types.js * Reduce scope of Makefile.js changes --- Makefile.js | 15 -- tools/rule-types.json | 294 ------------------------------------- tools/update-rule-types.js | 58 -------- 3 files changed, 367 deletions(-) delete mode 100644 tools/rule-types.json delete mode 100644 tools/update-rule-types.js diff --git a/Makefile.js b/Makefile.js index 6d3395e1615..ab4d15c1b94 100644 --- a/Makefile.js +++ b/Makefile.js @@ -674,7 +674,6 @@ target.checkRuleFiles = function() { echo("Validating rules"); - const ruleTypes = require("./tools/rule-types.json"); let errors = 0; RULE_FILES.forEach(filename => { @@ -686,14 +685,6 @@ target.checkRuleFiles = function() { const ruleCode = cat(filename); const knownHeaders = ["Rule Details", "Options", "Environments", "Examples", "Known Limitations", "When Not To Use It", "Compatibility"]; - /** - * Check if basename is present in rule-types.json file. - * @returns {boolean} true if present - * @private - */ - function isInRuleTypes() { - return Object.hasOwn(ruleTypes, basename); - } /** * Check if id is present in title @@ -776,12 +767,6 @@ target.checkRuleFiles = function() { } } - // check for recommended configuration - if (!isInRuleTypes()) { - console.error("Missing setting for %s in tools/rule-types.json", basename); - errors++; - } - // check parity between rules index file and rules directory const ruleIdsInIndex = require("./lib/rules/index"); const ruleDef = ruleIdsInIndex.get(basename); diff --git a/tools/rule-types.json b/tools/rule-types.json deleted file mode 100644 index 8a021f04556..00000000000 --- a/tools/rule-types.json +++ /dev/null @@ -1,294 +0,0 @@ -{ - "accessor-pairs": "suggestion", - "array-bracket-newline": "layout", - "array-bracket-spacing": "layout", - "array-callback-return": "problem", - "array-element-newline": "layout", - "arrow-body-style": "suggestion", - "arrow-parens": "layout", - "arrow-spacing": "layout", - "block-scoped-var": "suggestion", - "block-spacing": "layout", - "brace-style": "layout", - "callback-return": "suggestion", - "camelcase": "suggestion", - "capitalized-comments": "suggestion", - "class-methods-use-this": "suggestion", - "comma-dangle": "layout", - "comma-spacing": "layout", - "comma-style": "layout", - "complexity": "suggestion", - "computed-property-spacing": "layout", - "consistent-return": "suggestion", - "consistent-this": "suggestion", - "constructor-super": "problem", - "curly": "suggestion", - "default-case": "suggestion", - "default-case-last": "suggestion", - "default-param-last": "suggestion", - "dot-location": "layout", - "dot-notation": "suggestion", - "eol-last": "layout", - "eqeqeq": "suggestion", - "for-direction": "problem", - "func-call-spacing": "layout", - "func-name-matching": "suggestion", - "func-names": "suggestion", - "func-style": "suggestion", - "function-call-argument-newline": "layout", - "function-paren-newline": "layout", - "generator-star-spacing": "layout", - "getter-return": "problem", - "global-require": "suggestion", - "grouped-accessor-pairs": "suggestion", - "guard-for-in": "suggestion", - "handle-callback-err": "suggestion", - "id-blacklist": "suggestion", - "id-denylist": "suggestion", - "id-length": "suggestion", - "id-match": "suggestion", - "implicit-arrow-linebreak": "layout", - "indent": "layout", - "indent-legacy": "layout", - "init-declarations": "suggestion", - "jsx-quotes": "layout", - "key-spacing": "layout", - "keyword-spacing": "layout", - "line-comment-position": "layout", - "linebreak-style": "layout", - "lines-around-comment": "layout", - "lines-around-directive": "layout", - "lines-between-class-members": "layout", - "logical-assignment-operators": "suggestion", - "max-classes-per-file": "suggestion", - "max-depth": "suggestion", - "max-len": "layout", - "max-lines": "suggestion", - "max-lines-per-function": "suggestion", - "max-nested-callbacks": "suggestion", - "max-params": "suggestion", - "max-statements": "suggestion", - "max-statements-per-line": "layout", - "multiline-comment-style": "suggestion", - "multiline-ternary": "layout", - "new-cap": "suggestion", - "new-parens": "layout", - "newline-after-var": "layout", - "newline-before-return": "layout", - "newline-per-chained-call": "layout", - "no-alert": "suggestion", - "no-array-constructor": "suggestion", - "no-async-promise-executor": "problem", - "no-await-in-loop": "problem", - "no-bitwise": "suggestion", - "no-buffer-constructor": "problem", - "no-caller": "suggestion", - "no-case-declarations": "suggestion", - "no-catch-shadow": "suggestion", - "no-class-assign": "problem", - "no-compare-neg-zero": "problem", - "no-cond-assign": "problem", - "no-confusing-arrow": "suggestion", - "no-console": "suggestion", - "no-const-assign": "problem", - "no-constant-binary-expression": "problem", - "no-constant-condition": "problem", - "no-constructor-return": "problem", - "no-continue": "suggestion", - "no-control-regex": "problem", - "no-debugger": "problem", - "no-delete-var": "suggestion", - "no-div-regex": "suggestion", - "no-dupe-args": "problem", - "no-dupe-class-members": "problem", - "no-dupe-else-if": "problem", - "no-dupe-keys": "problem", - "no-duplicate-case": "problem", - "no-duplicate-imports": "problem", - "no-else-return": "suggestion", - "no-empty": "suggestion", - "no-empty-character-class": "problem", - "no-empty-function": "suggestion", - "no-empty-pattern": "problem", - "no-empty-static-block": "suggestion", - "no-eq-null": "suggestion", - "no-eval": "suggestion", - "no-ex-assign": "problem", - "no-extend-native": "suggestion", - "no-extra-bind": "suggestion", - "no-extra-boolean-cast": "suggestion", - "no-extra-label": "suggestion", - "no-extra-parens": "layout", - "no-extra-semi": "suggestion", - "no-fallthrough": "problem", - "no-floating-decimal": "suggestion", - "no-func-assign": "problem", - "no-global-assign": "suggestion", - "no-implicit-coercion": "suggestion", - "no-implicit-globals": "suggestion", - "no-implied-eval": "suggestion", - "no-import-assign": "problem", - "no-inline-comments": "suggestion", - "no-inner-declarations": "problem", - "no-invalid-regexp": "problem", - "no-invalid-this": "suggestion", - "no-irregular-whitespace": "problem", - "no-iterator": "suggestion", - "no-label-var": "suggestion", - "no-labels": "suggestion", - "no-lone-blocks": "suggestion", - "no-lonely-if": "suggestion", - "no-loop-func": "suggestion", - "no-loss-of-precision": "problem", - "no-magic-numbers": "suggestion", - "no-misleading-character-class": "problem", - "no-mixed-operators": "suggestion", - "no-mixed-requires": "suggestion", - "no-mixed-spaces-and-tabs": "layout", - "no-multi-assign": "suggestion", - "no-multi-spaces": "layout", - "no-multi-str": "suggestion", - "no-multiple-empty-lines": "layout", - "no-native-reassign": "suggestion", - "no-negated-condition": "suggestion", - "no-negated-in-lhs": "problem", - "no-nested-ternary": "suggestion", - "no-new": "suggestion", - "no-new-func": "suggestion", - "no-new-native-nonconstructor": "problem", - "no-new-object": "suggestion", - "no-new-require": "suggestion", - "no-new-symbol": "problem", - "no-new-wrappers": "suggestion", - "no-nonoctal-decimal-escape": "suggestion", - "no-obj-calls": "problem", - "no-object-constructor": "suggestion", - "no-octal": "suggestion", - "no-octal-escape": "suggestion", - "no-param-reassign": "suggestion", - "no-path-concat": "suggestion", - "no-plusplus": "suggestion", - "no-process-env": "suggestion", - "no-process-exit": "suggestion", - "no-promise-executor-return": "problem", - "no-proto": "suggestion", - "no-prototype-builtins": "problem", - "no-redeclare": "suggestion", - "no-regex-spaces": "suggestion", - "no-restricted-exports": "suggestion", - "no-restricted-globals": "suggestion", - "no-restricted-imports": "suggestion", - "no-restricted-modules": "suggestion", - "no-restricted-properties": "suggestion", - "no-restricted-syntax": "suggestion", - "no-return-assign": "suggestion", - "no-return-await": "suggestion", - "no-script-url": "suggestion", - "no-self-assign": "problem", - "no-self-compare": "problem", - "no-sequences": "suggestion", - "no-setter-return": "problem", - "no-shadow": "suggestion", - "no-shadow-restricted-names": "suggestion", - "no-spaced-func": "layout", - "no-sparse-arrays": "problem", - "no-sync": "suggestion", - "no-tabs": "layout", - "no-template-curly-in-string": "problem", - "no-ternary": "suggestion", - "no-this-before-super": "problem", - "no-throw-literal": "suggestion", - "no-trailing-spaces": "layout", - "no-undef": "problem", - "no-undef-init": "suggestion", - "no-undefined": "suggestion", - "no-underscore-dangle": "suggestion", - "no-unexpected-multiline": "problem", - "no-unmodified-loop-condition": "problem", - "no-unneeded-ternary": "suggestion", - "no-unreachable": "problem", - "no-unreachable-loop": "problem", - "no-unsafe-finally": "problem", - "no-unsafe-negation": "problem", - "no-unsafe-optional-chaining": "problem", - "no-unused-expressions": "suggestion", - "no-unused-labels": "suggestion", - "no-unused-private-class-members": "problem", - "no-unused-vars": "problem", - "no-use-before-define": "problem", - "no-useless-assignment": "problem", - "no-useless-backreference": "problem", - "no-useless-call": "suggestion", - "no-useless-catch": "suggestion", - "no-useless-computed-key": "suggestion", - "no-useless-concat": "suggestion", - "no-useless-constructor": "suggestion", - "no-useless-escape": "suggestion", - "no-useless-rename": "suggestion", - "no-useless-return": "suggestion", - "no-var": "suggestion", - "no-void": "suggestion", - "no-warning-comments": "suggestion", - "no-whitespace-before-property": "layout", - "no-with": "suggestion", - "nonblock-statement-body-position": "layout", - "object-curly-newline": "layout", - "object-curly-spacing": "layout", - "object-property-newline": "layout", - "object-shorthand": "suggestion", - "one-var": "suggestion", - "one-var-declaration-per-line": "suggestion", - "operator-assignment": "suggestion", - "operator-linebreak": "layout", - "padded-blocks": "layout", - "padding-line-between-statements": "layout", - "prefer-arrow-callback": "suggestion", - "prefer-const": "suggestion", - "prefer-destructuring": "suggestion", - "prefer-exponentiation-operator": "suggestion", - "prefer-named-capture-group": "suggestion", - "prefer-numeric-literals": "suggestion", - "prefer-object-has-own": "suggestion", - "prefer-object-spread": "suggestion", - "prefer-promise-reject-errors": "suggestion", - "prefer-reflect": "suggestion", - "prefer-regex-literals": "suggestion", - "prefer-rest-params": "suggestion", - "prefer-spread": "suggestion", - "prefer-template": "suggestion", - "quote-props": "suggestion", - "quotes": "layout", - "radix": "suggestion", - "require-atomic-updates": "problem", - "require-await": "suggestion", - "require-jsdoc": "suggestion", - "require-unicode-regexp": "suggestion", - "require-yield": "suggestion", - "rest-spread-spacing": "layout", - "semi": "layout", - "semi-spacing": "layout", - "semi-style": "layout", - "sort-imports": "suggestion", - "sort-keys": "suggestion", - "sort-vars": "suggestion", - "space-before-blocks": "layout", - "space-before-function-paren": "layout", - "space-in-parens": "layout", - "space-infix-ops": "layout", - "space-unary-ops": "layout", - "spaced-comment": "suggestion", - "strict": "suggestion", - "switch-colon-spacing": "layout", - "symbol-description": "suggestion", - "template-curly-spacing": "layout", - "template-tag-spacing": "layout", - "unicode-bom": "layout", - "use-isnan": "problem", - "valid-jsdoc": "suggestion", - "valid-typeof": "problem", - "vars-on-top": "suggestion", - "wrap-iife": "layout", - "wrap-regex": "layout", - "yield-star-spacing": "layout", - "yoda": "suggestion" -} diff --git a/tools/update-rule-types.js b/tools/update-rule-types.js deleted file mode 100644 index 6a68586d6de..00000000000 --- a/tools/update-rule-types.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * JSCodeShift script to update meta.type in rules. - * Run over the rules directory only. Use this command: - * - * jscodeshift -t tools/update-rule-types.js lib/rules/ - * @author Nicholas C. Zakas - */ -"use strict"; - -const path = require("path"); -const ruleTypes = require("./rule-types.json"); - -module.exports = (fileInfo, api) => { - const j = api.jscodeshift; - const source = fileInfo.source; - const ruleName = path.basename(fileInfo.path, ".js"); - - // get the object literal representing the rule - const nodes = j(source).find(j.ObjectExpression).filter(p => p.node.properties.some(node => node.key.name === "meta")); - - // updating logic - return nodes.replaceWith(p => { - - // gather important nodes from the rule - const metaNode = p.node.properties.find(node => node.key.name === "meta"); - - // if there's no properties, just exit - if (!metaNode.value.properties) { - return p.node; - } - - const typeNode = metaNode.value.properties.find(node => node.key.name === "type"); - - let ruleType; - - if (ruleName in ruleTypes) { - ruleType = ruleTypes[ruleName]; - } - - if (typeNode) { - - // update existing type node - typeNode.value = j.literal(ruleType); - } else { - - // add new type node if one doesn't exist - const newProp = j.property( - "init", - j.identifier("type"), - j.literal(ruleType) - ); - - p.node.properties[0].value.properties.unshift(newProp); - } - - return p.node; - }).toSource(); -}; From 8e13a6beb587e624cc95ae16eefe503ad024b11b Mon Sep 17 00:00:00 2001 From: Will Eastcott Date: Mon, 19 Feb 2024 10:53:42 +0000 Subject: [PATCH 30/50] chore: fix spelling mistake in README.md (#18128) Fix spelling mistake in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8819c68686f..0f90821dc67 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ We are now at or near 100% compatibility with JSCS. If you try ESLint and believ ### Does Prettier replace ESLint? -No, ESLint and Prettier have diffent jobs: ESLint is a linter (looking for problematic patterns) and Prettier is a code formatter. Using both tools is common, refer to [Prettier's documentation](https://prettier.io/docs/en/install#eslint-and-other-linters) to learn how to configure them to work well with each other. +No, ESLint and Prettier have different jobs: ESLint is a linter (looking for problematic patterns) and Prettier is a code formatter. Using both tools is common, refer to [Prettier's documentation](https://prettier.io/docs/en/install#eslint-and-other-linters) to learn how to configure them to work well with each other. ### Why can't ESLint find my plugins? From e462524cc318ffacecd266e6fe1038945a0b02e9 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Thu, 22 Feb 2024 15:22:00 +0100 Subject: [PATCH 31/50] chore: upgrade eslint-release@3.2.2 (#18138) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9cf566264fb..0c78bbb20f9 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "eslint-plugin-jsdoc": "^46.9.0", "eslint-plugin-n": "^16.6.0", "eslint-plugin-unicorn": "^49.0.0", - "eslint-release": "^3.2.0", + "eslint-release": "^3.2.2", "eslump": "^3.0.0", "esprima": "^4.0.1", "fast-glob": "^3.2.11", From 7db5bb270f95d1472de0bfed0e33ed5ab294942e Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 22 Feb 2024 12:44:26 -0700 Subject: [PATCH 32/50] docs: Show prerelease version in dropdown (#18135) * docs: Show prerelease version in dropdown fixes #17943 * Update docs/src/_data/eslintNextVersion.js Co-authored-by: Milos Djermanovic * Update docs/src/_includes/components/nav-version-switcher.html Co-authored-by: Milos Djermanovic * Update versions-list.html --------- Co-authored-by: Milos Djermanovic --- docs/package.json | 1 + docs/src/_data/eslintNextVersion.js | 32 +++++++++++++++++++ .../components/nav-version-switcher.html | 2 +- .../components/version-switcher.html | 2 +- .../src/_includes/partials/versions-list.html | 2 +- 5 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 docs/src/_data/eslintNextVersion.js diff --git a/docs/package.json b/docs/package.json index cb11570a689..5f9030f4e59 100644 --- a/docs/package.json +++ b/docs/package.json @@ -24,6 +24,7 @@ }, "devDependencies": { "@11ty/eleventy": "^2.0.1", + "@11ty/eleventy-fetch": "^4.0.0", "@11ty/eleventy-img": "^3.1.1", "@11ty/eleventy-navigation": "^0.3.5", "@11ty/eleventy-plugin-rss": "^1.1.1", diff --git a/docs/src/_data/eslintNextVersion.js b/docs/src/_data/eslintNextVersion.js new file mode 100644 index 00000000000..d3742364c54 --- /dev/null +++ b/docs/src/_data/eslintNextVersion.js @@ -0,0 +1,32 @@ +/** + * @fileoverview + * @author Nicholas C. Zakas + */ + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const eleventyFetch = require("@11ty/eleventy-fetch"); + +//----------------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------------- + +module.exports = async function() { + + // if we're on the next branch, we can just read the package.json file + if (process.env.BRANCH === "next") { + return require("../../package.json").version; + } + + // otherwise, we need to fetch the latest version from the GitHub API + const url = "https://raw.githubusercontent.com/eslint/eslint/next/docs/package.json"; + + const response = await eleventyFetch(url, { + duration: "1d", + type: "json" + }); + + return response.version; +} diff --git a/docs/src/_includes/components/nav-version-switcher.html b/docs/src/_includes/components/nav-version-switcher.html index a01e5ddf30c..b070e90a954 100644 --- a/docs/src/_includes/components/nav-version-switcher.html +++ b/docs/src/_includes/components/nav-version-switcher.html @@ -14,7 +14,7 @@ {% if config.showNextVersion == true %} - + {% endif %} {% for version in versions.items %} diff --git a/docs/src/_includes/partials/versions-list.html b/docs/src/_includes/partials/versions-list.html index 7f07e4fa495..2b859eb9c77 100644 --- a/docs/src/_includes/partials/versions-list.html +++ b/docs/src/_includes/partials/versions-list.html @@ -1,7 +1,7 @@
  • HEAD
  • {% if config.showNextVersion == true %} -
  • NEXT
  • +
  • v{{ eslintNextVersion }}
  • {% endif %}
  • v{{ eslintVersion }}
  • {%- for version in versions.items -%} From 0cb4914ef93cd572ba368d390b1cf0b93f578a9d Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 23 Feb 2024 16:39:26 +0100 Subject: [PATCH 33/50] fix: validate options when comment with just severity enables rule (#18133) --- lib/linter/linter.js | 215 ++++++++++++++++++------------------- tests/lib/linter/linter.js | 111 ++++++++++++++++--- 2 files changed, 204 insertions(+), 122 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 2ece33c8284..7cdcbec21c6 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -43,7 +43,7 @@ const const { getRuleFromConfig } = require("../config/flat-config-helpers"); const { FlatConfigArray } = require("../config/flat-config-array"); const { RuleValidator } = require("../config/rule-validator"); -const { assertIsRuleOptions, assertIsRuleSeverity } = require("../config/flat-config-schema"); +const { assertIsRuleSeverity } = require("../config/flat-config-schema"); const { normalizeSeverityToString } = require("../shared/severity"); const debug = require("debug")("eslint:linter"); const MAX_AUTOFIX_PASSES = 10; @@ -326,10 +326,11 @@ function createDisableDirectives(options) { * @param {SourceCode} sourceCode The SourceCode object to get comments from. * @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules * @param {string|null} warnInlineConfig If a string then it should warn directive comments as disabled. The string value is the config name what the setting came from. + * @param {ConfigData} config Provided config. * @returns {{configuredRules: Object, enabledGlobals: {value:string,comment:Token}[], exportedVariables: Object, problems: LintMessage[], disableDirectives: DisableDirective[]}} * A collection of the directive comments that were found, along with any problems that occurred when parsing */ -function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig) { +function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig, config) { const configuredRules = {}; const enabledGlobals = Object.create(null); const exportedVariables = {}; @@ -438,8 +439,50 @@ function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig) { return; } + let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue]; + + /* + * If the rule was already configured, inline rule configuration that + * only has severity should retain options from the config and just override the severity. + * + * Example: + * + * { + * rules: { + * curly: ["error", "multi"] + * } + * } + * + * /* eslint curly: ["warn"] * / + * + * Results in: + * + * curly: ["warn", "multi"] + */ + if ( + + /* + * If inline config for the rule has only severity + */ + ruleOptions.length === 1 && + + /* + * And the rule was already configured + */ + config.rules && Object.hasOwn(config.rules, name) + ) { + + /* + * Then use severity from the inline config and options from the provided config + */ + ruleOptions = [ + ruleOptions[0], // severity from the inline config + ...Array.isArray(config.rules[name]) ? config.rules[name].slice(1) : [] // options from the provided config + ]; + } + try { - validator.validateRuleOptions(rule, name, ruleValue); + validator.validateRuleOptions(rule, name, ruleOptions); } catch (err) { /* @@ -460,7 +503,7 @@ function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig) { return; } - configuredRules[name] = ruleValue; + configuredRules[name] = ruleOptions; }); } else { problems.push(parseResult.error); @@ -1322,7 +1365,7 @@ class Linter { const sourceCode = slots.lastSourceCode; const commentDirectives = options.allowInlineConfig - ? getDirectiveComments(sourceCode, ruleId => getRule(slots, ruleId), options.warnInlineConfig) + ? getDirectiveComments(sourceCode, ruleId => getRule(slots, ruleId), options.warnInlineConfig, config) : { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] }; // augment global scope with declared global variables @@ -1332,56 +1375,8 @@ class Linter { { exportedVariables: commentDirectives.exportedVariables, enabledGlobals: commentDirectives.enabledGlobals } ); - /* - * Now we determine the final configurations for rules. - * First, let all inline rule configurations override those from the config. - * Then, check for a special case: if a rule is configured in both places, - * inline rule configuration that only has severity should retain options from - * the config and just override the severity. - * - * Example: - * - * { - * rules: { - * curly: ["error", "multi"] - * } - * } - * - * /* eslint curly: ["warn"] * / - * - * Results in: - * - * curly: ["warn", "multi"] - */ const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules); - if (config.rules) { - for (const [ruleId, ruleInlineConfig] of Object.entries(commentDirectives.configuredRules)) { - if ( - - /* - * If inline config for the rule has only severity - */ - (!Array.isArray(ruleInlineConfig) || ruleInlineConfig.length === 1) && - - /* - * And provided config for the rule has options - */ - Object.hasOwn(config.rules, ruleId) && - (Array.isArray(config.rules[ruleId]) && config.rules[ruleId].length > 1) - ) { - - /* - * Then use severity from the inline config and options from the provided config - */ - configuredRules[ruleId] = [ - Array.isArray(ruleInlineConfig) ? ruleInlineConfig[0] : ruleInlineConfig, // severity from the inline config - ...config.rules[ruleId].slice(1) // options from the provided config - ]; - } - } - } - let lintingProblems; try { @@ -1713,17 +1708,67 @@ class Linter { try { - const ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue]; + let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue]; - assertIsRuleOptions(ruleId, ruleValue); assertIsRuleSeverity(ruleId, ruleOptions[0]); - ruleValidator.validate({ - plugins: config.plugins, - rules: { - [ruleId]: ruleOptions + /* + * If the rule was already configured, inline rule configuration that + * only has severity should retain options from the config and just override the severity. + * + * Example: + * + * { + * rules: { + * curly: ["error", "multi"] + * } + * } + * + * /* eslint curly: ["warn"] * / + * + * Results in: + * + * curly: ["warn", "multi"] + */ + + let shouldValidateOptions = true; + + if ( + + /* + * If inline config for the rule has only severity + */ + ruleOptions.length === 1 && + + /* + * And the rule was already configured + */ + config.rules && Object.hasOwn(config.rules, ruleId) + ) { + + /* + * Then use severity from the inline config and options from the provided config + */ + ruleOptions = [ + ruleOptions[0], // severity from the inline config + ...config.rules[ruleId].slice(1) // options from the provided config + ]; + + // if the rule was enabled, the options have already been validated + if (config.rules[ruleId][0] > 0) { + shouldValidateOptions = false; } - }); + } + + if (shouldValidateOptions) { + ruleValidator.validate({ + plugins: config.plugins, + rules: { + [ruleId]: ruleOptions + } + }); + } + mergedInlineConfig.rules[ruleId] = ruleOptions; } catch (err) { @@ -1763,58 +1808,8 @@ class Linter { ) : { problems: [], disableDirectives: [] }; - /* - * Now we determine the final configurations for rules. - * First, let all inline rule configurations override those from the config. - * Then, check for a special case: if a rule is configured in both places, - * inline rule configuration that only has severity should retain options from - * the config and just override the severity. - * - * Example: - * - * { - * rules: { - * curly: ["error", "multi"] - * } - * } - * - * /* eslint curly: ["warn"] * / - * - * Results in: - * - * curly: ["warn", "multi"] - * - * At this point, all rule configurations are normalized to arrays. - */ const configuredRules = Object.assign({}, config.rules, mergedInlineConfig.rules); - if (config.rules) { - for (const [ruleId, ruleInlineConfig] of Object.entries(mergedInlineConfig.rules)) { - if ( - - /* - * If inline config for the rule has only severity - */ - ruleInlineConfig.length === 1 && - - /* - * And provided config for the rule has options - */ - Object.hasOwn(config.rules, ruleId) && - config.rules[ruleId].length > 1 - ) { - - /* - * Then use severity from the inline config and options from the provided config - */ - configuredRules[ruleId] = [ - ruleInlineConfig[0], // severity from the inline config - ...config.rules[ruleId].slice(1) // options from the provided config - ]; - } - } - } - let lintingProblems; sourceCode.finalize(); diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 8cd20c1b6fb..9ead00af1a9 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -1461,20 +1461,42 @@ describe("Linter", () => { describe("when the rule was already configured", () => { beforeEach(() => { - linter.defineRule("my-rule", { - meta: { - schema: [{ - type: "string" - }] - }, - create(context) { - const message = context.options[0] ?? "option not provided"; + linter.defineRules({ + "my-rule": { + meta: { + schema: [{ + type: "string" + }] + }, + create(context) { + const message = context.options[0] ?? "option not provided"; - return { - Program(node) { - context.report({ node, message }); + return { + Program(node) { + context.report({ node, message }); + } + }; + } + }, + "requires-option": { + meta: { + schema: { + type: "array", + items: [{ + type: "string" + }], + minItems: 1 } - }; + }, + create(context) { + const message = context.options[0]; + + return { + Identifier(node) { + context.report({ node, message }); + } + }; + } } }); }); @@ -1540,6 +1562,27 @@ describe("Linter", () => { assert.strictEqual(suppressedMessages.length, 0); }); }); + + it("should validate and use originally configured options when /*eslint*/ comment enables rule that was set to 'off' in the configuration", () => { + const code = "/*eslint my-rule: ['warn'], requires-option: 'warn' */ foo;"; + const config = { + rules: { + "my-rule": ["off", true], // invalid options for this rule + "requires-option": ["off", "Don't use identifier"] // valid options for this rule + } + }; + const messages = linter.verify(code, config); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "my-rule"); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, "Configuration for rule \"my-rule\" is invalid:\n\tValue true should be string.\n"); + assert.strictEqual(messages[1].ruleId, "requires-option"); + assert.strictEqual(messages[1].severity, 1); + assert.strictEqual(messages[1].message, "Don't use identifier"); + assert.strictEqual(suppressedMessages.length, 0); + }); }); }); @@ -10730,6 +10773,26 @@ describe("Linter with FlatConfigArray", () => { } }; } + }, + "requires-option": { + meta: { + schema: { + type: "array", + items: [{ + type: "string" + }], + minItems: 1 + } + }, + create(context) { + const message = context.options[0]; + + return { + Identifier(node) { + context.report({ node, message }); + } + }; + } } } }; @@ -10798,6 +10861,30 @@ describe("Linter with FlatConfigArray", () => { assert.strictEqual(suppressedMessages.length, 0); }); }); + + it("should validate and use originally configured options when /*eslint*/ comment enables rule that was set to 'off' in the configuration", () => { + const code = "/*eslint test/my-rule: ['warn'], test/requires-option: 'warn' */ foo;"; + const config = { + plugins: { + test: plugin + }, + rules: { + "test/my-rule": ["off", true], // invalid options for this rule + "test/requires-option": ["off", "Don't use identifier"] // valid options for this rule + } + }; + const messages = linter.verify(code, config); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "test/my-rule"); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, "Inline configuration for rule \"test/my-rule\" is invalid:\n\tValue true should be string.\n"); + assert.strictEqual(messages[1].ruleId, "test/requires-option"); + assert.strictEqual(messages[1].severity, 1); + assert.strictEqual(messages[1].message, "Don't use identifier"); + assert.strictEqual(suppressedMessages.length, 0); + }); }); }); From 5fe095cf718b063dc5e58089b0a6cbcd53da7925 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 23 Feb 2024 21:49:01 +0100 Subject: [PATCH 34/50] docs: show v8.57.0 as latest version in dropdown (#18142) --- docs/src/_data/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/_data/config.json b/docs/src/_data/config.json index 5f9214a00b8..8ca381bbead 100644 --- a/docs/src/_data/config.json +++ b/docs/src/_data/config.json @@ -1,5 +1,5 @@ { "lang": "en", - "version": "8.56.0", + "version": "8.57.0", "showNextVersion": true } From c9f2f3343e7c197e5e962c68ef202d6a1646866e Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 23 Feb 2024 22:07:31 +0100 Subject: [PATCH 35/50] build: changelog update for 8.57.0 (#18144) --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc36ad3bf74..fb678d5aa94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +v8.57.0 - February 23, 2024 + +* [`1813aec`](https://github.com/eslint/eslint/commit/1813aecc4660582b0678cf32ba466eb9674266c4) chore: upgrade @eslint/js@8.57.0 (#18143) (Milos Djermanovic) +* [`5c356bb`](https://github.com/eslint/eslint/commit/5c356bb0c6f53c570224f8e9f02c4baca8fc6d2f) chore: package.json update for @eslint/js release (Jenkins) +* [`84922d0`](https://github.com/eslint/eslint/commit/84922d0bfa10689a34a447ab8e55975ff1c1c708) docs: Show prerelease version in dropdown (#18139) (Nicholas C. Zakas) +* [`1120b9b`](https://github.com/eslint/eslint/commit/1120b9b7b97f10f059d8b7ede19de2572f892366) feat: Add loadESLint() API method for v8 (#18098) (Nicholas C. Zakas) +* [`5b8c363`](https://github.com/eslint/eslint/commit/5b8c3636a3d7536535a6878eca0e5b773e4829d4) docs: Switch to Ethical Ads (#18117) (Milos Djermanovic) +* [`2196d97`](https://github.com/eslint/eslint/commit/2196d97094ba94d6d750828879a29538d1600de5) fix: handle absolute file paths in `FlatRuleTester` (#18064) (Nitin Kumar) +* [`f4a1fe2`](https://github.com/eslint/eslint/commit/f4a1fe2e45aa1089fe775290bf530de82f34bf16) test: add more tests for ignoring files and directories (#18068) (Nitin Kumar) +* [`69dd1d1`](https://github.com/eslint/eslint/commit/69dd1d1387b7b53617548d1f9f2c149f179e6e17) fix: Ensure config keys are printed for config errors (#18067) (Nitin Kumar) +* [`9852a31`](https://github.com/eslint/eslint/commit/9852a31edcf054bd5d15753ef18e2ad3216b1b71) fix: deep merge behavior in flat config (#18065) (Nitin Kumar) +* [`dca7d0f`](https://github.com/eslint/eslint/commit/dca7d0f1c262bc72310147bcefe1d04ecf60acbc) feat: Enable `eslint.config.mjs` and `eslint.config.cjs` (#18066) (Nitin Kumar) +* [`4c7e9b0`](https://github.com/eslint/eslint/commit/4c7e9b0b539ba879ac1799e81f3b6add2eed4b2f) fix: allow circular references in config (#18056) (Milos Djermanovic) +* [`77dbfd9`](https://github.com/eslint/eslint/commit/77dbfd9887b201a46fc68631cbde50c08e1a8dbf) docs: show NEXT in version selectors (#18052) (Milos Djermanovic) +* [`42c0aef`](https://github.com/eslint/eslint/commit/42c0aefaf6ea8b998b1c6db61906a79c046d301a) ci: Enable CI for `v8.x` branch (#18047) (Milos Djermanovic) + v9.0.0-beta.0 - February 9, 2024 * [`e40d1d7`](https://github.com/eslint/eslint/commit/e40d1d74a5b9788cbec195f4e602b50249f26659) chore: upgrade @eslint/js@9.0.0-beta.0 (#18108) (Milos Djermanovic) From bb3b9c68fe714bb8aa305be5f019a7a42f4374ee Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 23 Feb 2024 22:35:14 +0100 Subject: [PATCH 36/50] chore: upgrade @eslint/eslintrc@3.0.2 (#18145) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0c78bbb20f9..70be1883472 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^3.0.1", + "@eslint/eslintrc": "^3.0.2", "@eslint/js": "9.0.0-beta.0", "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", From e41425b5c3b4c885f2679a3663bd081911a8b570 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 23 Feb 2024 21:42:20 +0000 Subject: [PATCH 37/50] chore: package.json update for @eslint/js release --- packages/js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/package.json b/packages/js/package.json index 068a029a055..330a3e6b49f 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/js", - "version": "9.0.0-beta.0", + "version": "9.0.0-beta.1", "description": "ESLint JavaScript language implementation", "main": "./src/index.js", "scripts": {}, From 32ffdd181aa673ccc596f714d10a2f879ec622a7 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 23 Feb 2024 22:52:46 +0100 Subject: [PATCH 38/50] chore: upgrade @eslint/js@9.0.0-beta.1 (#18146) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 70be1883472..770bc270f87 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^3.0.2", - "@eslint/js": "9.0.0-beta.0", + "@eslint/js": "9.0.0-beta.1", "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", From fd9c0a9f0e50da617fe1f2e60ba3df0276a7f06b Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 23 Feb 2024 22:02:43 +0000 Subject: [PATCH 39/50] Build: changelog update for 9.0.0-beta.1 --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb678d5aa94..3851ec24140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +v9.0.0-beta.1 - February 23, 2024 + +* [`32ffdd1`](https://github.com/eslint/eslint/commit/32ffdd181aa673ccc596f714d10a2f879ec622a7) chore: upgrade @eslint/js@9.0.0-beta.1 (#18146) (Milos Djermanovic) +* [`e41425b`](https://github.com/eslint/eslint/commit/e41425b5c3b4c885f2679a3663bd081911a8b570) chore: package.json update for @eslint/js release (Jenkins) +* [`bb3b9c6`](https://github.com/eslint/eslint/commit/bb3b9c68fe714bb8aa305be5f019a7a42f4374ee) chore: upgrade @eslint/eslintrc@3.0.2 (#18145) (Milos Djermanovic) +* [`c9f2f33`](https://github.com/eslint/eslint/commit/c9f2f3343e7c197e5e962c68ef202d6a1646866e) build: changelog update for 8.57.0 (#18144) (Milos Djermanovic) +* [`5fe095c`](https://github.com/eslint/eslint/commit/5fe095cf718b063dc5e58089b0a6cbcd53da7925) docs: show v8.57.0 as latest version in dropdown (#18142) (Milos Djermanovic) +* [`0cb4914`](https://github.com/eslint/eslint/commit/0cb4914ef93cd572ba368d390b1cf0b93f578a9d) fix: validate options when comment with just severity enables rule (#18133) (Milos Djermanovic) +* [`7db5bb2`](https://github.com/eslint/eslint/commit/7db5bb270f95d1472de0bfed0e33ed5ab294942e) docs: Show prerelease version in dropdown (#18135) (Nicholas C. Zakas) +* [`e462524`](https://github.com/eslint/eslint/commit/e462524cc318ffacecd266e6fe1038945a0b02e9) chore: upgrade eslint-release@3.2.2 (#18138) (Milos Djermanovic) +* [`8e13a6b`](https://github.com/eslint/eslint/commit/8e13a6beb587e624cc95ae16eefe503ad024b11b) chore: fix spelling mistake in README.md (#18128) (Will Eastcott) +* [`66f52e2`](https://github.com/eslint/eslint/commit/66f52e276c31487424bcf54e490c4ac7ef70f77f) chore: remove unused tools rule-types.json, update-rule-types.js (#18125) (Josh Goldberg ✨) +* [`bf0c7ef`](https://github.com/eslint/eslint/commit/bf0c7effdba51c48b929d06ce1965408a912dc77) ci: fix sync-labels value of pr-labeler (#18124) (Tanuj Kanti) +* [`cace6d0`](https://github.com/eslint/eslint/commit/cace6d0a3afa5c84b18abee4ef8c598125143461) ci: add PR labeler action (#18109) (Nitin Kumar) +* [`73a5f06`](https://github.com/eslint/eslint/commit/73a5f0641b43e169247b0000f44a366ee6bbc4f2) docs: Update README (GitHub Actions Bot) +* [`74124c2`](https://github.com/eslint/eslint/commit/74124c20287fac1995c3f4e553f0723c066f311d) feat: add suggestions to `use-isnan` in `indexOf` & `lastIndexOf` calls (#18063) (StyleShit) +* [`1a65d3e`](https://github.com/eslint/eslint/commit/1a65d3e4a6ee16e3f607d69b998a08c3fed505ca) chore: export `base` config from `eslint-config-eslint` (#18119) (Milos Djermanovic) +* [`f95cd27`](https://github.com/eslint/eslint/commit/f95cd27679eef228173e27e170429c9710c939b3) docs: Disallow multiple rule configuration comments in the same example (#18116) (Milos Djermanovic) +* [`9aa4df3`](https://github.com/eslint/eslint/commit/9aa4df3f4d85960eee72923f3b9bfc88e62f04fb) refactor: remove `globals` dependency (#18115) (Milos Djermanovic) +* [`d8068ec`](https://github.com/eslint/eslint/commit/d8068ec70fac050e900dc400510a4ad673e17633) docs: Update link for schema examples (#18112) (Svetlana) + v8.57.0 - February 23, 2024 * [`1813aec`](https://github.com/eslint/eslint/commit/1813aecc4660582b0678cf32ba466eb9674266c4) chore: upgrade @eslint/js@8.57.0 (#18143) (Milos Djermanovic) From 491a1d16a8dbcbe2f0cc82ce7bef580229d09b86 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 23 Feb 2024 22:02:44 +0000 Subject: [PATCH 40/50] 9.0.0-beta.1 --- docs/package.json | 2 +- docs/src/use/formatters/html-formatter-example.html | 2 +- docs/src/use/formatters/index.md | 4 ++-- package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/package.json b/docs/package.json index 5f9030f4e59..5aa7188e18c 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,7 +1,7 @@ { "name": "docs-eslint", "private": true, - "version": "9.0.0-beta.0", + "version": "9.0.0-beta.1", "description": "", "main": "index.js", "keywords": [], diff --git a/docs/src/use/formatters/html-formatter-example.html b/docs/src/use/formatters/html-formatter-example.html index 9949da4e90f..b15b574d70a 100644 --- a/docs/src/use/formatters/html-formatter-example.html +++ b/docs/src/use/formatters/html-formatter-example.html @@ -118,7 +118,7 @@

    ESLint Report

    - 8 problems (4 errors, 4 warnings) - Generated on Fri Feb 09 2024 23:53:44 GMT+0000 (Coordinated Universal Time) + 8 problems (4 errors, 4 warnings) - Generated on Fri Feb 23 2024 22:02:44 GMT+0000 (Coordinated Universal Time)
diff --git a/docs/src/use/formatters/index.md b/docs/src/use/formatters/index.md index 1c842b9d6c1..b728a8f42b8 100644 --- a/docs/src/use/formatters/index.md +++ b/docs/src/use/formatters/index.md @@ -122,7 +122,7 @@ Example output (formatted for easier reading): ], "text": "!Number.isNaN(Number(i))" }, - "desc": "Replace with Number.isNaN cast to a Number." + "desc": "Replace with Number.isNaN and cast to a Number." } ] }, @@ -730,7 +730,7 @@ Example output (formatted for easier reading): ], "text": "!Number.isNaN(Number(i))" }, - "desc": "Replace with Number.isNaN cast to a Number." + "desc": "Replace with Number.isNaN and cast to a Number." } ] }, diff --git a/package.json b/package.json index 770bc270f87..e1c9f3ebb3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "9.0.0-beta.0", + "version": "9.0.0-beta.1", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From 11144a2671b2404b293f656be111221557f3390f Mon Sep 17 00:00:00 2001 From: M Pater Date: Tue, 27 Feb 2024 12:18:49 +0100 Subject: [PATCH 41/50] feat: `no-restricted-imports` option added `allowImportNames` (#16196) * feat: [no-restricted-imports] added option `excludeImportNames` * Fix: Removed question marks from checks in `no-restricted-imports` * Fix: [no-restricted-imports] `excludeImportNames` * docs: Update no-restricted-imports.md with new option Documentation added for option `excludeImportNames`. * docs: Update no-restricted-imports option exludeImportNames to allowImportNames * fix: Renamed `no-restricted-imports` option `excludeImportNames` to `allowImportNames` * fix: `no-restricted-imports` comments for options `allowImportNames` to pass test * fix: `no-restricted-imports` option `allowImportNames` for importing `*` * fix: `no-restricted-imports` rules added for option `allowImportNames` * fix: `no-restricted-imports` tests added for option `allowImportNames` * Lint * Lint Fix * Check Rules * fix: `no-restricted-imports` * fix: `no-restricted-imports`: Add tests for allowImportNamePattern * docs: `no-restricted-imports` updated * feat: refactor no-restricted-imports and add tests * Update docs/src/rules/no-restricted-imports.md Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> * Update docs/src/rules/no-restricted-imports.md Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> * fix: `no-restricted-imports`: requested changes * docs: `no-restricted-imports`: added `allowImportNames` to `patterns` * Update docs/src/rules/no-restricted-imports.md Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> * Update docs/src/rules/no-restricted-imports.md Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> * docs: `no-restricted-imports` message variation * feat: validate schema * docs: `no-restricted-imports`: disallow using both `importNames` and `allowImportNames` * Update docs/src/rules/no-restricted-imports.md Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> * Update docs/src/rules/no-restricted-imports.md Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> * Update docs/src/rules/no-restricted-imports.md Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> * Update docs/src/rules/no-restricted-imports.md Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> * Update docs/src/rules/no-restricted-imports.md Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> * Update docs/src/rules/no-restricted-imports.md Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> * fix: `no-restricted-imports`: Review suggestions * feat: add more validations to schema * docs: add validate options name * Update lib/rules/no-restricted-imports.js Co-authored-by: Milos Djermanovic * Update lib/rules/no-restricted-imports.js Co-authored-by: Milos Djermanovic * Update lib/rules/no-restricted-imports.js Co-authored-by: Milos Djermanovic * Update lib/rules/no-restricted-imports.js Co-authored-by: Milos Djermanovic * Update lib/rules/no-restricted-imports.js Co-authored-by: Milos Djermanovic * Update lib/rules/no-restricted-imports.js Co-authored-by: Milos Djermanovic * fix: `no-restricted-imports`: tests updated accordingly * feat: add return * Update docs/src/rules/no-restricted-imports.md Co-authored-by: Milos Djermanovic * Update lib/rules/no-restricted-imports.js Co-authored-by: Milos Djermanovic * Update lib/rules/no-restricted-imports.js Co-authored-by: Milos Djermanovic * feat: `no-restricted-imports`: added custom message to tests * fix: `no-restricted-imports`: remove test case * fix: `no-restricted-imports` * Update docs/src/rules/no-restricted-imports.md Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> * Update docs/src/rules/no-restricted-imports.md Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> --------- Co-authored-by: Tanuj Kanti Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> Co-authored-by: Milos Djermanovic --- docs/src/rules/no-restricted-imports.md | 145 +++++++++++++ lib/rules/no-restricted-imports.js | 230 ++++++++++++++++----- tests/lib/rules/no-restricted-imports.js | 253 +++++++++++++++++++++++ 3 files changed, 581 insertions(+), 47 deletions(-) diff --git a/docs/src/rules/no-restricted-imports.md b/docs/src/rules/no-restricted-imports.md index 2ce0d8f9d1a..2211fd21c2f 100644 --- a/docs/src/rules/no-restricted-imports.md +++ b/docs/src/rules/no-restricted-imports.md @@ -230,6 +230,58 @@ import { AllowedObject as DisallowedObject } from "foo"; ::: +#### allowImportNames + +This option is an array. Inverse of `importNames`, `allowImportNames` allows the imports that are specified inside this array. So it restricts all imports from a module, except specified allowed ones. + +Note: `allowImportNames` cannot be used in combination with `importNames`. + +```json +"no-restricted-imports": ["error", { + "paths": [{ + "name": "import-foo", + "allowImportNames": ["Bar"], + "message": "Please use only Bar from import-foo." + }] +}] +``` + +Examples of **incorrect** code for `allowImportNames` in `paths`: + +Disallowing all import names except 'AllowedObject'. + +::: incorrect { "sourceType": "module" } + +```js +/*eslint no-restricted-imports: ["error", { paths: [{ + name: "foo", + allowImportNames: ["AllowedObject"], + message: "Please use only 'AllowedObject' from 'foo'." +}]}]*/ + +import { DisallowedObject } from "foo"; +``` + +::: + +Examples of **correct** code for `allowImportNames` in `paths`: + +Disallowing all import names except 'AllowedObject'. + +::: correct { "sourceType": "module" } + +```js +/*eslint no-restricted-imports: ["error", { paths: [{ + name: "foo", + allowImportNames: ["AllowedObject"], + message: "Only use 'AllowedObject' from 'foo'." +}]}]*/ + +import { AllowedObject } from "foo"; +``` + +::: + ### patterns This is also an object option whose value is an array. This option allows you to specify multiple modules to restrict using `gitignore`-style patterns. @@ -445,6 +497,54 @@ import { hasValues } from 'utils/collection-utils'; ::: +#### allowImportNames + +You can also specify `allowImportNames` on objects inside of `patterns`. In this case, the specified names are applied only to the specified `group`. + +Note: `allowImportNames` cannot be used in combination with `importNames`, `importNamePattern` or `allowImportNamePattern`. + +```json +"no-restricted-imports": ["error", { + "patterns": [{ + "group": ["utils/*"], + "allowImportNames": ["isEmpty"], + "message": "Please use only 'isEmpty' from utils." + }] +}] +``` + +Examples of **incorrect** code for `allowImportNames` in `patterns`: + +::: incorrect { "sourceType": "module" } + +```js +/*eslint no-restricted-imports: ["error", { patterns: [{ + group: ["utils/*"], + allowImportNames: ['isEmpty'], + message: "Please use only 'isEmpty' from utils." +}]}]*/ + +import { hasValues } from 'utils/collection-utils'; +``` + +::: + +Examples of **correct** code for `allowImportNames` in `patterns`: + +::: correct { "sourceType": "module" } + +```js +/*eslint no-restricted-imports: ["error", { patterns: [{ + group: ["utils/*"], + allowImportNames: ['isEmpty'], + message: "Please use only 'isEmpty' from utils." +}]}]*/ + +import { isEmpty } from 'utils/collection-utils'; +``` + +::: + #### importNamePattern This option allows you to use regex patterns to restrict import names: @@ -518,6 +618,51 @@ import isEmpty, { hasValue } from 'utils/collection-utils'; ::: +#### allowImportNamePattern + +This is a string option. Inverse of `importNamePattern`, this option allows imports that matches the specified regex pattern. So it restricts all imports from a module, except specified allowed patterns. + +Note: `allowImportNamePattern` cannot be used in combination with `importNames`, `importNamePattern` or `allowImportNames`. + +```json +"no-restricted-imports": ["error", { + "patterns": [{ + "group": ["import-foo/*"], + "allowImportNamePattern": "^foo", + }] +}] +``` + +Examples of **incorrect** code for `allowImportNamePattern` option: + +::: incorrect { "sourceType": "module" } + +```js +/*eslint no-restricted-imports: ["error", { patterns: [{ + group: ["utils/*"], + allowImportNamePattern: '^has' +}]}]*/ + +import { isEmpty } from 'utils/collection-utils'; +``` + +::: + +Examples of **correct** code for `allowImportNamePattern` option: + +::: correct { "sourceType": "module" } + +```js +/*eslint no-restricted-imports: ["error", { patterns: [{ + group: ["utils/*"], + allowImportNamePattern: '^is' +}]}]*/ + +import { isEmpty } from 'utils/collection-utils'; +``` + +::: + ## When Not To Use It Don't use this rule or don't include a module in the list for this rule if you want to be able to import a module in your project without an ESLint error or warning. diff --git a/lib/rules/no-restricted-imports.js b/lib/rules/no-restricted-imports.js index afd0bbb8ba2..062be909ef0 100644 --- a/lib/rules/no-restricted-imports.js +++ b/lib/rules/no-restricted-imports.js @@ -34,10 +34,17 @@ const arrayOfStringsOrObjects = { items: { type: "string" } + }, + allowImportNames: { + type: "array", + items: { + type: "string" + } } }, additionalProperties: false, - required: ["name"] + required: ["name"], + not: { required: ["importNames", "allowImportNames"] } } ] }, @@ -66,6 +73,14 @@ const arrayOfStringsOrObjectPatterns = { minItems: 1, uniqueItems: true }, + allowImportNames: { + type: "array", + items: { + type: "string" + }, + minItems: 1, + uniqueItems: true + }, group: { type: "array", items: { @@ -77,6 +92,9 @@ const arrayOfStringsOrObjectPatterns = { importNamePattern: { type: "string" }, + allowImportNamePattern: { + type: "string" + }, message: { type: "string", minLength: 1 @@ -86,7 +104,16 @@ const arrayOfStringsOrObjectPatterns = { } }, additionalProperties: false, - required: ["group"] + required: ["group"], + not: { + anyOf: [ + { required: ["importNames", "allowImportNames"] }, + { required: ["importNamePattern", "allowImportNamePattern"] }, + { required: ["importNames", "allowImportNamePattern"] }, + { required: ["importNamePattern", "allowImportNames"] }, + { required: ["allowImportNames", "allowImportNamePattern"] } + ] + } }, uniqueItems: true } @@ -131,7 +158,23 @@ module.exports = { importName: "'{{importName}}' import from '{{importSource}}' is restricted.", // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period - importNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}" + importNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}", + + allowedImportName: "'{{importName}}' import from '{{importSource}}' is restricted because only '{{allowedImportNames}}' import(s) is/are allowed.", + // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period + allowedImportNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted because only '{{allowedImportNames}}' import(s) is/are allowed. {{customMessage}}", + + everythingWithAllowImportNames: "* import is invalid because only '{{allowedImportNames}}' from '{{importSource}}' is/are allowed.", + // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period + everythingWithAllowImportNamesAndCustomMessage: "* import is invalid because only '{{allowedImportNames}}' from '{{importSource}}' is/are allowed. {{customMessage}}", + + allowedImportNamePattern: "'{{importName}}' import from '{{importSource}}' is restricted because only imports that match the pattern '{{allowedImportNamePattern}}' are allowed from '{{importSource}}'.", + // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period + allowedImportNamePatternWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted because only imports that match the pattern '{{allowedImportNamePattern}}' are allowed from '{{importSource}}'. {{customMessage}}", + + everythingWithAllowedImportNamePattern: "* import is invalid because only imports that match the pattern '{{allowedImportNamePattern}}' from '{{importSource}}' are allowed.", + // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period + everythingWithAllowedImportNamePatternWithCustomMessage: "* import is invalid because only imports that match the pattern '{{allowedImportNamePattern}}' from '{{importSource}}' are allowed. {{customMessage}}" }, schema: { @@ -175,7 +218,8 @@ module.exports = { } else { memo[path].push({ message: importSource.message, - importNames: importSource.importNames + importNames: importSource.importNames, + allowImportNames: importSource.allowImportNames }); } return memo; @@ -190,12 +234,18 @@ module.exports = { } // relative paths are supported for this rule - const restrictedPatternGroups = restrictedPatterns.map(({ group, message, caseSensitive, importNames, importNamePattern }) => ({ - matcher: ignore({ allowRelativePaths: true, ignorecase: !caseSensitive }).add(group), - customMessage: message, - importNames, - importNamePattern - })); + const restrictedPatternGroups = restrictedPatterns.map( + ({ group, message, caseSensitive, importNames, importNamePattern, allowImportNames, allowImportNamePattern }) => ( + { + matcher: ignore({ allowRelativePaths: true, ignorecase: !caseSensitive }).add(group), + customMessage: message, + importNames, + importNamePattern, + allowImportNames, + allowImportNamePattern + } + ) + ); // if no imports are restricted we don't need to check if (Object.keys(restrictedPaths).length === 0 && restrictedPatternGroups.length === 0) { @@ -218,42 +268,9 @@ module.exports = { groupedRestrictedPaths[importSource].forEach(restrictedPathEntry => { const customMessage = restrictedPathEntry.message; const restrictedImportNames = restrictedPathEntry.importNames; + const allowedImportNames = restrictedPathEntry.allowImportNames; - if (restrictedImportNames) { - if (importNames.has("*")) { - const specifierData = importNames.get("*")[0]; - - context.report({ - node, - messageId: customMessage ? "everythingWithCustomMessage" : "everything", - loc: specifierData.loc, - data: { - importSource, - importNames: restrictedImportNames, - customMessage - } - }); - } - - restrictedImportNames.forEach(importName => { - if (importNames.has(importName)) { - const specifiers = importNames.get(importName); - - specifiers.forEach(specifier => { - context.report({ - node, - messageId: customMessage ? "importNameWithCustomMessage" : "importName", - loc: specifier.loc, - data: { - importSource, - customMessage, - importName - } - }); - }); - } - }); - } else { + if (!restrictedImportNames && !allowedImportNames) { context.report({ node, messageId: customMessage ? "pathWithCustomMessage" : "path", @@ -262,7 +279,72 @@ module.exports = { customMessage } }); + + return; } + + importNames.forEach((specifiers, importName) => { + if (importName === "*") { + const [specifier] = specifiers; + + if (restrictedImportNames) { + context.report({ + node, + messageId: customMessage ? "everythingWithCustomMessage" : "everything", + loc: specifier.loc, + data: { + importSource, + importNames: restrictedImportNames, + customMessage + } + }); + } else if (allowedImportNames) { + context.report({ + node, + messageId: customMessage ? "everythingWithAllowImportNamesAndCustomMessage" : "everythingWithAllowImportNames", + loc: specifier.loc, + data: { + importSource, + allowedImportNames, + customMessage + } + }); + } + + return; + } + + if (restrictedImportNames && restrictedImportNames.includes(importName)) { + specifiers.forEach(specifier => { + context.report({ + node, + messageId: customMessage ? "importNameWithCustomMessage" : "importName", + loc: specifier.loc, + data: { + importSource, + customMessage, + importName + } + }); + }); + } + + if (allowedImportNames && !allowedImportNames.includes(importName)) { + specifiers.forEach(specifier => { + context.report({ + node, + loc: specifier.loc, + messageId: customMessage ? "allowedImportNameWithCustomMessage" : "allowedImportName", + data: { + importSource, + customMessage, + importName, + allowedImportNames + } + }); + }); + } + }); }); } @@ -281,12 +363,14 @@ module.exports = { const customMessage = group.customMessage; const restrictedImportNames = group.importNames; const restrictedImportNamePattern = group.importNamePattern ? new RegExp(group.importNamePattern, "u") : null; + const allowedImportNames = group.allowImportNames; + const allowedImportNamePattern = group.allowImportNamePattern ? new RegExp(group.allowImportNamePattern, "u") : null; - /* + /** * If we are not restricting to any specific import names and just the pattern itself, * report the error and move on */ - if (!restrictedImportNames && !restrictedImportNamePattern) { + if (!restrictedImportNames && !allowedImportNames && !restrictedImportNamePattern && !allowedImportNamePattern) { context.report({ node, messageId: customMessage ? "patternWithCustomMessage" : "patterns", @@ -313,6 +397,28 @@ module.exports = { customMessage } }); + } else if (allowedImportNames) { + context.report({ + node, + messageId: customMessage ? "everythingWithAllowImportNamesAndCustomMessage" : "everythingWithAllowImportNames", + loc: specifier.loc, + data: { + importSource, + allowedImportNames, + customMessage + } + }); + } else if (allowedImportNamePattern) { + context.report({ + node, + messageId: customMessage ? "everythingWithAllowedImportNamePatternWithCustomMessage" : "everythingWithAllowedImportNamePattern", + loc: specifier.loc, + data: { + importSource, + allowedImportNamePattern, + customMessage + } + }); } else { context.report({ node, @@ -346,6 +452,36 @@ module.exports = { }); }); } + + if (allowedImportNames && !allowedImportNames.includes(importName)) { + specifiers.forEach(specifier => { + context.report({ + node, + messageId: customMessage ? "allowedImportNameWithCustomMessage" : "allowedImportName", + loc: specifier.loc, + data: { + importSource, + customMessage, + importName, + allowedImportNames + } + }); + }); + } else if (allowedImportNamePattern && !allowedImportNamePattern.test(importName)) { + specifiers.forEach(specifier => { + context.report({ + node, + messageId: customMessage ? "allowedImportNamePatternWithCustomMessage" : "allowedImportNamePattern", + loc: specifier.loc, + data: { + importSource, + customMessage, + importName, + allowedImportNamePattern + } + }); + }); + } }); } diff --git a/tests/lib/rules/no-restricted-imports.js b/tests/lib/rules/no-restricted-imports.js index af50d44e6e7..e0456247d4b 100644 --- a/tests/lib/rules/no-restricted-imports.js +++ b/tests/lib/rules/no-restricted-imports.js @@ -375,6 +375,61 @@ ruleTester.run("no-restricted-imports", rule, { importNamePattern: "^Foo" }] }] + }, + { + code: "import { AllowedObject } from \"foo\";", + options: [{ + paths: [{ + name: "foo", + allowImportNames: ["AllowedObject"], + message: "Please import anything except 'AllowedObject' from /bar/ instead." + }] + }] + }, + { + code: "import { foo } from 'foo';", + options: [{ + paths: [{ + name: "foo", + allowImportNames: ["foo"] + }] + }] + }, + { + code: "import { foo } from 'foo';", + options: [{ + patterns: [{ + group: ["foo"], + allowImportNames: ["foo"] + }] + }] + }, + { + code: "export { bar } from 'foo';", + options: [{ + paths: [{ + name: "foo", + allowImportNames: ["bar"] + }] + }] + }, + { + code: "export { bar } from 'foo';", + options: [{ + patterns: [{ + group: ["foo"], + allowImportNames: ["bar"] + }] + }] + }, + { + code: "import { Foo } from 'foo';", + options: [{ + patterns: [{ + group: ["foo"], + allowImportNamePattern: "^Foo" + }] + }] } ], invalid: [{ @@ -1953,6 +2008,204 @@ ruleTester.run("no-restricted-imports", rule, { endColumn: 9, message: "* import is invalid because import name matching '/^Foo/u' pattern from 'foo' is restricted from being used." }] + }, + { + code: "export { Bar } from 'foo';", + options: [{ + patterns: [{ + group: ["foo"], + allowImportNamePattern: "^Foo" + }] + }], + errors: [{ + type: "ExportNamedDeclaration", + line: 1, + column: 10, + endColumn: 13, + message: "'Bar' import from 'foo' is restricted because only imports that match the pattern '/^Foo/u' are allowed from 'foo'." + }] + }, + { + code: "export { Bar } from 'foo';", + options: [{ + patterns: [{ + group: ["foo"], + allowImportNamePattern: "^Foo", + message: "Only imports that match the pattern '/^Foo/u' are allowed to be imported from 'foo'." + }] + }], + errors: [{ + type: "ExportNamedDeclaration", + line: 1, + column: 10, + endColumn: 13, + message: "'Bar' import from 'foo' is restricted because only imports that match the pattern '/^Foo/u' are allowed from 'foo'. Only imports that match the pattern '/^Foo/u' are allowed to be imported from 'foo'." + }] + }, + { + code: "import { AllowedObject, DisallowedObject } from \"foo\";", + options: [{ + paths: [{ + name: "foo", + allowImportNames: ["AllowedObject"] + }] + }], + errors: [{ + message: "'DisallowedObject' import from 'foo' is restricted because only 'AllowedObject' import(s) is/are allowed.", + type: "ImportDeclaration", + line: 1, + column: 25, + endColumn: 41 + }] + }, + { + code: "import { AllowedObject, DisallowedObject } from \"foo\";", + options: [{ + paths: [{ + name: "foo", + allowImportNames: ["AllowedObject"], + message: "Only 'AllowedObject' is allowed to be imported from 'foo'." + }] + }], + errors: [{ + message: "'DisallowedObject' import from 'foo' is restricted because only 'AllowedObject' import(s) is/are allowed. Only 'AllowedObject' is allowed to be imported from 'foo'.", + type: "ImportDeclaration", + line: 1, + column: 25, + endColumn: 41 + }] + }, + { + code: "import { AllowedObject, DisallowedObject } from \"foo\";", + options: [{ + patterns: [{ + group: ["foo"], + allowImportNames: ["AllowedObject"] + }] + }], + errors: [{ + message: "'DisallowedObject' import from 'foo' is restricted because only 'AllowedObject' import(s) is/are allowed.", + type: "ImportDeclaration", + line: 1, + column: 25, + endColumn: 41 + }] + }, + { + code: "import { AllowedObject, DisallowedObject } from \"foo\";", + options: [{ + patterns: [{ + group: ["foo"], + allowImportNames: ["AllowedObject"], + message: "Only 'AllowedObject' is allowed to be imported from 'foo'." + }] + }], + errors: [{ + message: "'DisallowedObject' import from 'foo' is restricted because only 'AllowedObject' import(s) is/are allowed. Only 'AllowedObject' is allowed to be imported from 'foo'.", + type: "ImportDeclaration", + line: 1, + column: 25, + endColumn: 41 + }] + }, + { + code: "import * as AllowedObject from \"foo\";", + options: [{ + paths: [{ + name: "foo", + allowImportNames: ["AllowedObject"] + }] + }], + errors: [{ + message: "* import is invalid because only 'AllowedObject' from 'foo' is/are allowed.", + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 26 + }] + }, + { + code: "import * as AllowedObject from \"foo\";", + options: [{ + paths: [{ + name: "foo", + allowImportNames: ["AllowedObject"], + message: "Only 'AllowedObject' is allowed to be imported from 'foo'." + }] + }], + errors: [{ + message: "* import is invalid because only 'AllowedObject' from 'foo' is/are allowed. Only 'AllowedObject' is allowed to be imported from 'foo'.", + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 26 + }] + }, + { + code: "import * as AllowedObject from \"foo/bar\";", + options: [{ + patterns: [{ + group: ["foo/*"], + allowImportNames: ["AllowedObject"] + }] + }], + errors: [{ + message: "* import is invalid because only 'AllowedObject' from 'foo/bar' is/are allowed.", + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 26 + }] + }, + { + code: "import * as AllowedObject from \"foo/bar\";", + options: [{ + patterns: [{ + group: ["foo/*"], + allowImportNames: ["AllowedObject"], + message: "Only 'AllowedObject' is allowed to be imported from 'foo'." + }] + }], + errors: [{ + message: "* import is invalid because only 'AllowedObject' from 'foo/bar' is/are allowed. Only 'AllowedObject' is allowed to be imported from 'foo'.", + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 26 + }] + }, + { + code: "import * as AllowedObject from \"foo/bar\";", + options: [{ + patterns: [{ + group: ["foo/*"], + allowImportNamePattern: "^Allow" + }] + }], + errors: [{ + message: "* import is invalid because only imports that match the pattern '/^Allow/u' from 'foo/bar' are allowed.", + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 26 + }] + }, + { + code: "import * as AllowedObject from \"foo/bar\";", + options: [{ + patterns: [{ + group: ["foo/*"], + allowImportNamePattern: "^Allow", + message: "Only import names starting with 'Allow' are allowed to be imported from 'foo'." + }] + }], + errors: [{ + message: "* import is invalid because only imports that match the pattern '/^Allow/u' from 'foo/bar' are allowed. Only import names starting with 'Allow' are allowed to be imported from 'foo'.", + type: "ImportDeclaration", + line: 1, + column: 8, + endColumn: 26 + }] } ] }); From 450d0f044023843b1790bd497dfca45dcbdb41e4 Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Wed, 28 Feb 2024 08:23:33 +0100 Subject: [PATCH 42/50] docs: fix `ignore` option docs (#18154) --- docs/src/integrate/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/integrate/nodejs-api.md b/docs/src/integrate/nodejs-api.md index 5d081df3d3b..cc01388f852 100644 --- a/docs/src/integrate/nodejs-api.md +++ b/docs/src/integrate/nodejs-api.md @@ -128,7 +128,7 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob * `options.globInputPaths` (`boolean`)
Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't interpret glob patterns. * `options.ignore` (`boolean`)
- Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't respect `.eslintignore` files or `ignorePatterns` in your configuration. + Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't respect `ignorePatterns` in your configuration. * `options.ignorePatterns` (`string[] | null`)
Default is `null`. Ignore file patterns to use in addition to config ignores. These patterns are relative to `cwd`. * `options.passOnNoPatterns` (`boolean`)
From af6e17081fa6c343474959712e7a4a20f8b304e2 Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Wed, 28 Feb 2024 20:08:57 +0100 Subject: [PATCH 43/50] fix: stop linting files after an error (#18155) --- lib/eslint/eslint.js | 9 ++++++++- tests/lib/eslint/eslint.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 97102d3fe0e..42b8ddd2410 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -838,6 +838,7 @@ class ESLint { configs, errorOnUnmatchedPattern }); + const controller = new AbortController(); debug(`${filePaths.length} files found in: ${Date.now() - startTime}ms`); @@ -906,9 +907,12 @@ class ESLint { fixer = message => shouldMessageBeFixed(message, config, fixTypesSet) && originalFix(message); } - return fs.readFile(filePath, "utf8") + return fs.readFile(filePath, { encoding: "utf8", signal: controller.signal }) .then(text => { + // fail immediately if an error occurred in another file + controller.signal.throwIfAborted(); + // do the linting const result = verifyText({ text, @@ -932,6 +936,9 @@ class ESLint { } return result; + }).catch(error => { + controller.abort(error); + throw error; }); }) diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 9360d39449d..a00efc2afad 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -16,6 +16,7 @@ const fs = require("fs"); const fsp = fs.promises; const os = require("os"); const path = require("path"); +const timers = require("node:timers/promises"); const escapeStringRegExp = require("escape-string-regexp"); const fCache = require("file-entry-cache"); const sinon = require("sinon"); @@ -4445,6 +4446,40 @@ describe("ESLint", () => { }); }); + it("should stop linting files if a rule crashes", async () => { + + const cwd = getFixturePath("files"); + let createCallCount = 0; + + eslint = new ESLint({ + cwd, + plugins: { + boom: { + rules: { + boom: { + create() { + createCallCount++; + throw Error("Boom!"); + } + } + } + } + }, + baseConfig: { + rules: { + "boom/boom": "error" + } + } + }); + + await assert.rejects(eslint.lintFiles("*.js")); + + // Wait until all files have been closed. + while (process.getActiveResourcesInfo().includes("CloseReq")) { + await timers.setImmediate(); + } + assert.strictEqual(createCallCount, 1); + }); }); From e5ef3cd6953bb40108556e0465653898ffed8420 Mon Sep 17 00:00:00 2001 From: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> Date: Fri, 1 Mar 2024 02:47:57 +0530 Subject: [PATCH 44/50] docs: add inline cases condition in `no-fallthrough` (#18158) docs: add inline cases condition --- docs/src/rules/no-fallthrough.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/rules/no-fallthrough.md b/docs/src/rules/no-fallthrough.md index c49293cad4a..1f092097330 100644 --- a/docs/src/rules/no-fallthrough.md +++ b/docs/src/rules/no-fallthrough.md @@ -140,6 +140,11 @@ switch(foo) { doSomething(); } +switch(foo) { + case 1: case 2: + doSomething(); +} + switch(foo) { case 1: doSomething(); From c49ed63265fc8e0cccea404810a4c5075d396a15 Mon Sep 17 00:00:00 2001 From: Mathias Schreck Date: Fri, 1 Mar 2024 11:25:15 +0100 Subject: [PATCH 45/50] feat: update complexity rule for optional chaining & default values (#18152) Both, optional chaining expressions and default values (in function parameters or descructuring expressions) increase the branching of the code and thus the complexity is increased. Fixes: #18060 --- docs/src/rules/complexity.md | 8 +++ lib/rules/complexity.js | 13 +++++ tests/lib/rules/complexity.js | 93 ++++++++++++++++++++++++++++++++++- 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/docs/src/rules/complexity.md b/docs/src/rules/complexity.md index 08dd839f5cd..dda6acb45ea 100644 --- a/docs/src/rules/complexity.md +++ b/docs/src/rules/complexity.md @@ -57,6 +57,14 @@ function b() { foo ||= 1; bar &&= 1; } + +function c(a = {}) { // default parameter -> 2nd path + const { b = 'default' } = a; // default value during destructuring -> 3rd path +} + +function d(a) { + return a?.b?.c; // optional chaining with two optional properties creates two additional branches +} ``` ::: diff --git a/lib/rules/complexity.js b/lib/rules/complexity.js index ac114187b2c..647de7b4d3b 100644 --- a/lib/rules/complexity.js +++ b/lib/rules/complexity.js @@ -109,6 +109,7 @@ module.exports = { IfStatement: increaseComplexity, WhileStatement: increaseComplexity, DoWhileStatement: increaseComplexity, + AssignmentPattern: increaseComplexity, // Avoid `default` "SwitchCase[test]": increaseComplexity, @@ -120,6 +121,18 @@ module.exports = { } }, + MemberExpression(node) { + if (node.optional === true) { + increaseComplexity(); + } + }, + + CallExpression(node) { + if (node.optional === true) { + increaseComplexity(); + } + }, + onCodePathEnd(codePath, node) { const complexity = complexities.pop(); diff --git a/tests/lib/rules/complexity.js b/tests/lib/rules/complexity.js index 0c8eb637482..27ac112fcc2 100644 --- a/tests/lib/rules/complexity.js +++ b/tests/lib/rules/complexity.js @@ -124,7 +124,25 @@ ruleTester.run("complexity", rule, { { code: "class C { static { if (a || b) c = d || e; } }", options: [4], languageOptions: { ecmaVersion: 2022 } }, // object property options - { code: "function b(x) {}", options: [{ max: 1 }] } + { code: "function b(x) {}", options: [{ max: 1 }] }, + + // optional chaining + { + code: "function a(b) { b?.c; }", options: [{ max: 2 }] + }, + + // default function parameter values + { + code: "function a(b = '') {}", options: [{ max: 2 }] + }, + + // default destructuring values + { + code: "function a(b) { const { c = '' } = b; }", options: [{ max: 2 }] + }, + { + code: "function a(b) { const [ c = '' ] = b; }", options: [{ max: 2 }] + } ], invalid: [ { code: "function a(x) {}", options: [0], errors: [makeError("Function 'a'", 1, 0)] }, @@ -522,6 +540,77 @@ ruleTester.run("complexity", rule, { }, // object property options - { code: "function a(x) {}", options: [{ max: 0 }], errors: [makeError("Function 'a'", 1, 0)] } + { code: "function a(x) {}", options: [{ max: 0 }], errors: [makeError("Function 'a'", 1, 0)] }, + + // optional chaining + { + code: "function a(b) { b?.c; }", + options: [{ max: 1 }], + errors: [makeError("Function 'a'", 2, 1)] + }, + { + code: "function a(b) { b?.['c']; }", + options: [{ max: 1 }], + errors: [makeError("Function 'a'", 2, 1)] + }, + { + code: "function a(b) { b?.c; d || e; }", + options: [{ max: 2 }], + errors: [makeError("Function 'a'", 3, 2)] + }, + { + code: "function a(b) { b?.c?.d; }", + options: [{ max: 2 }], + errors: [makeError("Function 'a'", 3, 2)] + }, + { + code: "function a(b) { b?.['c']?.['d']; }", + options: [{ max: 2 }], + errors: [makeError("Function 'a'", 3, 2)] + }, + { + code: "function a(b) { b?.c?.['d']; }", + options: [{ max: 2 }], + errors: [makeError("Function 'a'", 3, 2)] + }, + { + code: "function a(b) { b?.c.d?.e; }", + options: [{ max: 2 }], + errors: [makeError("Function 'a'", 3, 2)] + }, + { + code: "function a(b) { b?.c?.(); }", + options: [{ max: 2 }], + errors: [makeError("Function 'a'", 3, 2)] + }, + { + code: "function a(b) { b?.c?.()?.(); }", + options: [{ max: 3 }], + errors: [makeError("Function 'a'", 4, 3)] + }, + + // default function parameter values + { + code: "function a(b = '') {}", + options: [{ max: 1 }], + errors: [makeError("Function 'a'", 2, 1)] + }, + + // default destructuring values + { + code: "function a(b) { const { c = '' } = b; }", + options: [{ max: 1 }], + errors: [makeError("Function 'a'", 2, 1)] + }, + { + code: "function a(b) { const [ c = '' ] = b; }", + options: [{ max: 1 }], + errors: [makeError("Function 'a'", 2, 1)] + }, + { + code: "function a(b) { const [ { c: d = '' } = {} ] = b; }", + options: [{ max: 1 }], + errors: [makeError("Function 'a'", 3, 1)] + } ] }); From e37153f71f173e8667273d6298bef81e0d33f9ba Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Fri, 1 Mar 2024 16:05:56 +0530 Subject: [PATCH 46/50] fix: improve error message for invalid rule config (#18147) * fix: improve error message for invalid rule config * refactor: update indentation & use more strict checks --- lib/config/rule-validator.js | 17 +++- tests/lib/config/flat-config-array.js | 110 ++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/lib/config/rule-validator.js b/lib/config/rule-validator.js index a087718c9b4..3b4ea6122cb 100644 --- a/lib/config/rule-validator.js +++ b/lib/config/rule-validator.js @@ -167,9 +167,22 @@ class RuleValidator { validateRule(ruleOptions.slice(1)); if (validateRule.errors) { - throw new Error(`Key "rules": Key "${ruleId}": ${ + throw new Error(`Key "rules": Key "${ruleId}":\n${ validateRule.errors.map( - error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n` + error => { + if ( + error.keyword === "additionalProperties" && + error.schema === false && + typeof error.parentSchema?.properties === "object" && + typeof error.params?.additionalProperty === "string" + ) { + const expectedProperties = Object.keys(error.parentSchema.properties).map(property => `"${property}"`); + + return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n\t\tUnexpected property "${error.params.additionalProperty}". Expected properties: ${expectedProperties.join(", ")}.\n`; + } + + return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`; + } ).join("") }`); } diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js index cfaa3ebd906..a8bff24417a 100644 --- a/tests/lib/config/flat-config-array.js +++ b/tests/lib/config/flat-config-array.js @@ -44,6 +44,81 @@ const baseConfig = { baz: { }, + "prefer-const": { + meta: { + schema: [ + { + type: "object", + properties: { + destructuring: { enum: ["any", "all"], default: "any" }, + ignoreReadBeforeAssign: { type: "boolean", default: false } + }, + additionalProperties: false + } + ] + } + }, + "prefer-destructuring": { + meta: { + schema: [ + { + oneOf: [ + { + type: "object", + properties: { + VariableDeclarator: { + type: "object", + properties: { + array: { + type: "boolean" + }, + object: { + type: "boolean" + } + }, + additionalProperties: false + }, + AssignmentExpression: { + type: "object", + properties: { + array: { + type: "boolean" + }, + object: { + type: "boolean" + } + }, + additionalProperties: false + } + }, + additionalProperties: false + }, + { + type: "object", + properties: { + array: { + type: "boolean" + }, + object: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + { + type: "object", + properties: { + enforceForRenamedProperties: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + } + }, // old-style boom() {}, @@ -2157,6 +2232,41 @@ describe("FlatConfigArray", () => { } })); + it("should error show expected properties", async () => { + + await assertInvalidConfig([ + { + rules: { + "prefer-const": ["error", { destruct: true }] + } + } + ], "Unexpected property \"destruct\". Expected properties: \"destructuring\", \"ignoreReadBeforeAssign\""); + + await assertInvalidConfig([ + { + rules: { + "prefer-destructuring": ["error", { obj: true }] + } + } + ], "Unexpected property \"obj\". Expected properties: \"VariableDeclarator\", \"AssignmentExpression\""); + + await assertInvalidConfig([ + { + rules: { + "prefer-destructuring": ["error", { obj: true }] + } + } + ], "Unexpected property \"obj\". Expected properties: \"array\", \"object\""); + + await assertInvalidConfig([ + { + rules: { + "prefer-destructuring": ["error", { object: true }, { enforceRenamedProperties: true }] + } + } + ], "Unexpected property \"enforceRenamedProperties\". Expected properties: \"enforceForRenamedProperties\""); + }); + }); describe("Invalid Keys", () => { From 79a95eb7da7fe657b6448c225d4f8ac31117456a Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sun, 3 Mar 2024 08:15:04 +0100 Subject: [PATCH 47/50] feat!: disallow multiple configuration comments for same rule (#18157) * feat!: disallow multiple configuration comments for same rule Fixes #18132 * update lint error message Co-authored-by: Nicholas C. Zakas * update tests Co-authored-by: Nicholas C. Zakas --- docs/src/use/migrate-to-9.0.0.md | 25 +++ lib/linter/linter.js | 16 ++ tests/lib/linter/linter.js | 310 +++++++++++++++++++++++++++++++ 3 files changed, 351 insertions(+) diff --git a/docs/src/use/migrate-to-9.0.0.md b/docs/src/use/migrate-to-9.0.0.md index 9db6f4724ec..e6e0ed58e66 100644 --- a/docs/src/use/migrate-to-9.0.0.md +++ b/docs/src/use/migrate-to-9.0.0.md @@ -25,6 +25,7 @@ The lists below are ordered roughly by the number of users each change is expect * [`--output-file` now writes a file to disk even with an empty output](#output-file) * [Change in behavior when no patterns are passed to CLI](#cli-empty-patterns) * [`/* eslint */` comments with only severity now retain options from the config file](#eslint-comment-options) +* [Multiple `/* eslint */` comments for the same rule are now disallowed](#multiple-eslint-comments) * [Stricter `/* exported */` parsing](#exported-parsing) * [`no-constructor-return` and `no-sequences` rule schemas are stricter](#stricter-rule-schemas) * [New checks in `no-implicit-coercion` by default](#no-implicit-coercion) @@ -190,6 +191,30 @@ Note that this change only affects cases where the same rule is configured in th **Related issue(s):** [#17381](https://github.com/eslint/eslint/issues/17381) +## Multiple `/* eslint */` comments for the same rule are now disallowed + +Prior to ESLint v9.0.0, if the file being linted contained multiple `/* eslint */` configuration comments for the same rule, the last one would be applied, while the others would be silently ignored. For example: + +```js +/* eslint semi: ["error", "always"] */ +/* eslint semi: ["error", "never"] */ + +foo() // valid, because the configuration is "never" +``` + +In ESLint v9.0.0, the first one is applied, while the others are reported as lint errors: + +```js +/* eslint semi: ["error", "always"] */ +/* eslint semi: ["error", "never"] */ // error: Rule "semi" is already configured by another configuration comment in the preceding code. This configuration is ignored. + +foo() // error: Missing semicolon +``` + +**To address:** Remove duplicate `/* eslint */` comments. + +**Related issue(s):** [#18132](https://github.com/eslint/eslint/issues/18132) + ## Stricter `/* exported */` parsing Prior to ESLint v9.0.0, the `/* exported */` directive incorrectly allowed the following syntax: diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 7cdcbec21c6..82417034b38 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -439,6 +439,14 @@ function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig, config) return; } + if (Object.hasOwn(configuredRules, name)) { + problems.push(createLintingProblem({ + message: `Rule "${name}" is already configured by another configuration comment in the preceding code. This configuration is ignored.`, + loc: comment.loc + })); + return; + } + let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue]; /* @@ -1706,6 +1714,14 @@ class Linter { return; } + if (Object.hasOwn(mergedInlineConfig.rules, ruleId)) { + inlineConfigProblems.push(createLintingProblem({ + message: `Rule "${ruleId}" is already configured by another configuration comment in the preceding code. This configuration is ignored.`, + loc: node.loc + })); + return; + } + try { let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue]; diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 9ead00af1a9..8e7c3c5070c 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -1877,6 +1877,156 @@ describe("Linter", () => { }); }); + describe("when evaluating code with multiple configuration comments for same rule", () => { + + beforeEach(() => { + linter.defineRule("no-foo", { + meta: { + schema: [{ + enum: ["bar", "baz", "qux"] + }] + }, + create(context) { + const replacement = context.options[0] ?? "default"; + + return { + "Identifier[name='foo']"(node) { + context.report(node, `Replace 'foo' with '${replacement}'.`); + } + }; + } + }); + }); + + it("should apply the first and report an error for the second when there are two", () => { + const code = "/*eslint no-foo: ['error', 'bar']*/ /*eslint no-foo: ['error', 'baz']*/ foo;"; + + const messages = linter.verify(code); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual(messages, [ + { + ruleId: null, + severity: 2, + message: "Rule \"no-foo\" is already configured by another configuration comment in the preceding code. This configuration is ignored.", + line: 1, + column: 37, + endLine: 1, + endColumn: 72, + nodeType: null + }, + { + ruleId: "no-foo", + severity: 2, + message: "Replace 'foo' with 'bar'.", + line: 1, + column: 73, + endLine: 1, + endColumn: 76, + nodeType: "Identifier" + } + ]); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should apply the first and report an error for each other when there are more than two", () => { + const code = "/*eslint no-foo: ['error', 'bar']*/ /*eslint no-foo: ['error', 'baz']*/ /*eslint no-foo: ['error', 'qux']*/ foo;"; + + const messages = linter.verify(code); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual(messages, [ + { + ruleId: null, + severity: 2, + message: "Rule \"no-foo\" is already configured by another configuration comment in the preceding code. This configuration is ignored.", + line: 1, + column: 37, + endLine: 1, + endColumn: 72, + nodeType: null + }, + { + ruleId: null, + severity: 2, + message: "Rule \"no-foo\" is already configured by another configuration comment in the preceding code. This configuration is ignored.", + line: 1, + column: 73, + endLine: 1, + endColumn: 108, + nodeType: null + }, + { + ruleId: "no-foo", + severity: 2, + message: "Replace 'foo' with 'bar'.", + line: 1, + column: 109, + endLine: 1, + endColumn: 112, + nodeType: "Identifier" + } + ]); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should apply the first and report an error for the second when both just override severity", () => { + const code = "/*eslint no-foo: 'warn'*/ /*eslint no-foo: 'error'*/ foo;"; + + const messages = linter.verify(code, { rules: { "no-foo": ["error", "bar"] } }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual(messages, [ + { + ruleId: null, + severity: 2, + message: "Rule \"no-foo\" is already configured by another configuration comment in the preceding code. This configuration is ignored.", + line: 1, + column: 27, + endLine: 1, + endColumn: 53, + nodeType: null + }, + { + ruleId: "no-foo", + severity: 1, + message: "Replace 'foo' with 'bar'.", + line: 1, + column: 54, + endLine: 1, + endColumn: 57, + nodeType: "Identifier" + } + ]); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should apply the second if the first has an invalid configuration", () => { + const code = "/*eslint no-foo: ['error', 'quux']*/ /*eslint no-foo: ['error', 'bar']*/ foo;"; + + const messages = linter.verify(code); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 2); + assert.include(messages[0].message, "Configuration for rule \"no-foo\" is invalid"); + assert.strictEqual(messages[1].message, "Replace 'foo' with 'bar'."); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should apply configurations for other rules that are in the same comment as the duplicate", () => { + const code = "/*eslint no-foo: ['error', 'bar']*/ /*eslint no-foo: ['error', 'baz'], no-alert: ['error']*/ foo; alert();"; + + const messages = linter.verify(code); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 3); + assert.strictEqual(messages[0].message, "Rule \"no-foo\" is already configured by another configuration comment in the preceding code. This configuration is ignored."); + assert.strictEqual(messages[1].message, "Replace 'foo' with 'bar'."); + assert.strictEqual(messages[2].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); + describe("when evaluating code with comments to enable and disable all reporting", () => { it("should report a violation", () => { @@ -11225,6 +11375,166 @@ describe("Linter with FlatConfigArray", () => { }); }); + describe("when evaluating code with multiple configuration comments for same rule", () => { + + let baseConfig; + + beforeEach(() => { + baseConfig = { + plugins: { + "test-plugin": { + rules: { + "no-foo": { + meta: { + schema: [{ + enum: ["bar", "baz", "qux"] + }] + }, + create(context) { + const replacement = context.options[0] ?? "default"; + + return { + "Identifier[name='foo']"(node) { + context.report(node, `Replace 'foo' with '${replacement}'.`); + } + }; + } + } + } + } + } + }; + }); + + it("should apply the first and report an error for the second when there are two", () => { + const code = "/*eslint test-plugin/no-foo: ['error', 'bar']*/ /*eslint test-plugin/no-foo: ['error', 'baz']*/ foo;"; + + const messages = linter.verify(code, baseConfig); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual(messages, [ + { + ruleId: null, + severity: 2, + message: "Rule \"test-plugin/no-foo\" is already configured by another configuration comment in the preceding code. This configuration is ignored.", + line: 1, + column: 49, + endLine: 1, + endColumn: 96, + nodeType: null + }, + { + ruleId: "test-plugin/no-foo", + severity: 2, + message: "Replace 'foo' with 'bar'.", + line: 1, + column: 97, + endLine: 1, + endColumn: 100, + nodeType: "Identifier" + } + ]); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should apply the first and report an error for each other when there are more than two", () => { + const code = "/*eslint test-plugin/no-foo: ['error', 'bar']*/ /*eslint test-plugin/no-foo: ['error', 'baz']*/ /*eslint test-plugin/no-foo: ['error', 'qux']*/ foo;"; + + const messages = linter.verify(code, baseConfig); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual(messages, [ + { + ruleId: null, + severity: 2, + message: "Rule \"test-plugin/no-foo\" is already configured by another configuration comment in the preceding code. This configuration is ignored.", + line: 1, + column: 49, + endLine: 1, + endColumn: 96, + nodeType: null + }, + { + ruleId: null, + severity: 2, + message: "Rule \"test-plugin/no-foo\" is already configured by another configuration comment in the preceding code. This configuration is ignored.", + line: 1, + column: 97, + endLine: 1, + endColumn: 144, + nodeType: null + }, + { + ruleId: "test-plugin/no-foo", + severity: 2, + message: "Replace 'foo' with 'bar'.", + line: 1, + column: 145, + endLine: 1, + endColumn: 148, + nodeType: "Identifier" + } + ]); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should apply the first and report an error for the second when both just override severity", () => { + const code = "/*eslint test-plugin/no-foo: 'warn'*/ /*eslint test-plugin/no-foo: 'error'*/ foo;"; + + const messages = linter.verify(code, { ...baseConfig, rules: { "test-plugin/no-foo": ["error", "bar"] } }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual(messages, [ + { + ruleId: null, + severity: 2, + message: "Rule \"test-plugin/no-foo\" is already configured by another configuration comment in the preceding code. This configuration is ignored.", + line: 1, + column: 39, + endLine: 1, + endColumn: 77, + nodeType: null + }, + { + ruleId: "test-plugin/no-foo", + severity: 1, + message: "Replace 'foo' with 'bar'.", + line: 1, + column: 78, + endLine: 1, + endColumn: 81, + nodeType: "Identifier" + } + ]); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should apply the second if the first has an invalid configuration", () => { + const code = "/*eslint test-plugin/no-foo: ['error', 'quux']*/ /*eslint test-plugin/no-foo: ['error', 'bar']*/ foo;"; + + const messages = linter.verify(code, baseConfig); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 2); + assert.include(messages[0].message, "Inline configuration for rule \"test-plugin/no-foo\" is invalid"); + assert.strictEqual(messages[1].message, "Replace 'foo' with 'bar'."); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should apply configurations for other rules that are in the same comment as the duplicate", () => { + const code = "/*eslint test-plugin/no-foo: ['error', 'bar']*/ /*eslint test-plugin/no-foo: ['error', 'baz'], no-alert: ['error']*/ foo; alert();"; + + const messages = linter.verify(code, baseConfig); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 3); + assert.strictEqual(messages[0].message, "Rule \"test-plugin/no-foo\" is already configured by another configuration comment in the preceding code. This configuration is ignored."); + assert.strictEqual(messages[1].message, "Replace 'foo' with 'bar'."); + assert.strictEqual(messages[2].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); + describe("when evaluating code with comments to enable and disable all reporting", () => { it("should report a violation", () => { From 1f1260e863f53e2a5891163485a67c55d41993aa Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Mon, 4 Mar 2024 09:54:49 +0100 Subject: [PATCH 48/50] docs: replace HackerOne link with GitHub advisory (#18165) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c3c3a2a2e02..331622770d4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ Before filing an issue, please be sure to read the guidelines for what you're re * [Propose a Rule Change](https://eslint.org/docs/latest/contribute/propose-rule-change) * [Request a Change](https://eslint.org/docs/latest/contribute/request-change) -To report a security vulnerability in ESLint, please use our [HackerOne program](https://hackerone.com/eslint). +To report a security vulnerability in ESLint, please use our [create an advisory form](https://github.com/eslint/eslint/security/advisories/new) on GitHub. ## Contributing Code From 972ef155a94ad2cc85db7d209ad869869222c14c Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Tue, 5 Mar 2024 00:26:10 +0530 Subject: [PATCH 49/50] chore: remove invalid type in @eslint/js (#18164) --- packages/js/src/configs/eslint-recommended.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/js/src/configs/eslint-recommended.js b/packages/js/src/configs/eslint-recommended.js index 2b8ca3a8593..b105595877b 100644 --- a/packages/js/src/configs/eslint-recommended.js +++ b/packages/js/src/configs/eslint-recommended.js @@ -8,7 +8,6 @@ /* eslint sort-keys: ["error", "asc"] -- Long, so make more readable */ -/** @type {import("../lib/shared/types").ConfigData} */ module.exports = Object.freeze({ rules: Object.freeze({ "constructor-super": "error", From a451b32b33535a57b4b7e24291f30760f65460ba Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Tue, 5 Mar 2024 13:05:04 +0100 Subject: [PATCH 50/50] feat: make `no-misleading-character-class` report more granular errors (#18082) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: report granular errors on arbitrary literals * use npm dependency * test with unescaped CRLF * inline `createReportLocationGenerator` * unit test for templates with expressions * restore old name `getNodeReportLocations` * update JSDoc * `charInfos` β†’ `codeUnits` * extract char-source to a utility module * add `read` method to `SourceReader` * add `advance` method and JSDoc * fix logic * `SourceReader` β†’ `TextReader` * handle `RegExp` calls with regex patterns * fix for browser test * fix for Node.js 18 * limit applicability of `getStaticValue` for Node.js 18 compatibility * fix for `RegExp()` without arguments * update JSDoc for `getStaticValueOrRegex` --- lib/rules/no-misleading-character-class.js | 173 ++++--- lib/rules/utils/char-source.js | 240 ++++++++++ .../rules/no-misleading-character-class.js | 423 ++++++++++++++---- tests/lib/rules/utils/char-source.js | 256 +++++++++++ 4 files changed, 945 insertions(+), 147 deletions(-) create mode 100644 lib/rules/utils/char-source.js create mode 100644 tests/lib/rules/utils/char-source.js diff --git a/lib/rules/no-misleading-character-class.js b/lib/rules/no-misleading-character-class.js index 8d818665790..fa50e226f97 100644 --- a/lib/rules/no-misleading-character-class.js +++ b/lib/rules/no-misleading-character-class.js @@ -3,11 +3,18 @@ */ "use strict"; -const { CALL, CONSTRUCT, ReferenceTracker, getStringIfConstant } = require("@eslint-community/eslint-utils"); +const { + CALL, + CONSTRUCT, + ReferenceTracker, + getStaticValue, + getStringIfConstant +} = require("@eslint-community/eslint-utils"); const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp"); const { isCombiningCharacter, isEmojiModifier, isRegionalIndicatorSymbol, isSurrogatePair } = require("./utils/unicode"); const astUtils = require("./utils/ast-utils.js"); const { isValidWithUnicodeFlag } = require("./utils/regular-expressions"); +const { parseStringLiteral, parseTemplateToken } = require("./utils/char-source"); //------------------------------------------------------------------------------ // Helpers @@ -193,6 +200,33 @@ const findCharacterSequences = { const kinds = Object.keys(findCharacterSequences); +/** + * Gets the value of the given node if it's a static value other than a regular expression object, + * or the node's `regex` property. + * The purpose of this method is to provide a replacement for `getStaticValue` in environments where certain regular expressions cannot be evaluated. + * A known example is Node.js 18 which does not support the `v` flag. + * Calling `getStaticValue` on a regular expression node with the `v` flag on Node.js 18 always returns `null`. + * A limitation of this method is that it can only detect a regular expression if the specified node is itself a regular expression literal node. + * @param {ASTNode | undefined} node The node to be inspected. + * @param {Scope} initialScope Scope to start finding variables. This function tries to resolve identifier references which are in the given scope. + * @returns {{ value: any } | { regex: { pattern: string, flags: string } } | null} The static value of the node, or `null`. + */ +function getStaticValueOrRegex(node, initialScope) { + if (!node) { + return null; + } + if (node.type === "Literal" && node.regex) { + return { regex: node.regex }; + } + + const staticValue = getStaticValue(node, initialScope); + + if (staticValue?.value instanceof RegExp) { + return null; + } + return staticValue; +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -225,62 +259,7 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; const parser = new RegExpParser(); - - /** - * Generates a granular loc for context.report, if directly calculable. - * @param {Character[]} chars Individual characters being reported on. - * @param {Node} node Parent string node to report within. - * @returns {Object | null} Granular loc for context.report, if directly calculable. - * @see https://github.com/eslint/eslint/pull/17515 - */ - function generateReportLocation(chars, node) { - - // Limit to to literals and expression-less templates with raw values === their value. - switch (node.type) { - case "TemplateLiteral": - if (node.expressions.length || sourceCode.getText(node).slice(1, -1) !== node.quasis[0].value.cooked) { - return null; - } - break; - - case "Literal": - if (typeof node.value === "string" && node.value !== node.raw.slice(1, -1)) { - return null; - } - break; - - default: - return null; - } - - return { - start: sourceCode.getLocFromIndex(node.range[0] + 1 + chars[0].start), - end: sourceCode.getLocFromIndex(node.range[0] + 1 + chars.at(-1).end) - }; - } - - /** - * Finds the report loc(s) for a range of matches. - * @param {Character[][]} matches Characters that should trigger a report. - * @param {Node} node The node to report. - * @returns {Object | null} Node loc(s) for context.report. - */ - function getNodeReportLocations(matches, node) { - const locs = []; - - for (const chars of matches) { - const loc = generateReportLocation(chars, node); - - // If a report can't match to a range, don't report any others - if (!loc) { - return [node.loc]; - } - - locs.push(loc); - } - - return locs; - } + const checkedPatternNodes = new Set(); /** * Verify a given regular expression. @@ -320,12 +299,58 @@ module.exports = { } else { foundKindMatches.set(kind, [...findCharacterSequences[kind](chars)]); } - } } } }); + let codeUnits = null; + + /** + * Finds the report loc(s) for a range of matches. + * Only literals and expression-less templates generate granular errors. + * @param {Character[][]} matches Lists of individual characters being reported on. + * @returns {Location[]} locs for context.report. + * @see https://github.com/eslint/eslint/pull/17515 + */ + function getNodeReportLocations(matches) { + if (!astUtils.isStaticTemplateLiteral(node) && node.type !== "Literal") { + return matches.length ? [node.loc] : []; + } + return matches.map(chars => { + const firstIndex = chars[0].start; + const lastIndex = chars.at(-1).end - 1; + let start; + let end; + + if (node.type === "TemplateLiteral") { + const source = sourceCode.getText(node); + const offset = node.range[0]; + + codeUnits ??= parseTemplateToken(source); + start = offset + codeUnits[firstIndex].start; + end = offset + codeUnits[lastIndex].end; + } else if (typeof node.value === "string") { // String Literal + const source = node.raw; + const offset = node.range[0]; + + codeUnits ??= parseStringLiteral(source); + start = offset + codeUnits[firstIndex].start; + end = offset + codeUnits[lastIndex].end; + } else { // RegExp Literal + const offset = node.range[0] + 1; // Add 1 to skip the leading slash. + + start = offset + firstIndex; + end = offset + lastIndex + 1; + } + + return { + start: sourceCode.getLocFromIndex(start), + end: sourceCode.getLocFromIndex(end) + }; + }); + } + for (const [kind, matches] of foundKindMatches) { let suggest; @@ -336,7 +361,7 @@ module.exports = { }]; } - const locs = getNodeReportLocations(matches, node); + const locs = getNodeReportLocations(matches); for (const loc of locs) { context.report({ @@ -351,6 +376,9 @@ module.exports = { return { "Literal[regex]"(node) { + if (checkedPatternNodes.has(node)) { + return; + } verify(node, node.regex.pattern, node.regex.flags, fixer => { if (!isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, node.regex.pattern)) { return null; @@ -371,12 +399,31 @@ module.exports = { for (const { node: refNode } of tracker.iterateGlobalReferences({ RegExp: { [CALL]: true, [CONSTRUCT]: true } })) { + let pattern, flags; const [patternNode, flagsNode] = refNode.arguments; - const pattern = getStringIfConstant(patternNode, scope); - const flags = getStringIfConstant(flagsNode, scope); + const evaluatedPattern = getStaticValueOrRegex(patternNode, scope); + + if (!evaluatedPattern) { + continue; + } + if (flagsNode) { + if (evaluatedPattern.regex) { + pattern = evaluatedPattern.regex.pattern; + checkedPatternNodes.add(patternNode); + } else { + pattern = String(evaluatedPattern.value); + } + flags = getStringIfConstant(flagsNode, scope); + } else { + if (evaluatedPattern.regex) { + continue; + } + pattern = String(evaluatedPattern.value); + flags = ""; + } - if (typeof pattern === "string") { - verify(patternNode, pattern, flags || "", fixer => { + if (typeof flags === "string") { + verify(patternNode, pattern, flags, fixer => { if (!isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern)) { return null; diff --git a/lib/rules/utils/char-source.js b/lib/rules/utils/char-source.js new file mode 100644 index 00000000000..70738625b94 --- /dev/null +++ b/lib/rules/utils/char-source.js @@ -0,0 +1,240 @@ +/** + * @fileoverview Utility functions to locate the source text of each code unit in the value of a string literal or template token. + * @author Francesco Trotta + */ + +"use strict"; + +/** + * Represents a code unit produced by the evaluation of a JavaScript common token like a string + * literal or template token. + */ +class CodeUnit { + constructor(start, source) { + this.start = start; + this.source = source; + } + + get end() { + return this.start + this.length; + } + + get length() { + return this.source.length; + } +} + +/** + * An object used to keep track of the position in a source text where the next characters will be read. + */ +class TextReader { + constructor(source) { + this.source = source; + this.pos = 0; + } + + /** + * Advances the reading position of the specified number of characters. + * @param {number} length Number of characters to advance. + * @returns {void} + */ + advance(length) { + this.pos += length; + } + + /** + * Reads characters from the source. + * @param {number} [offset=0] The offset where reading starts, relative to the current position. + * @param {number} [length=1] Number of characters to read. + * @returns {string} A substring of source characters. + */ + read(offset = 0, length = 1) { + const start = offset + this.pos; + + return this.source.slice(start, start + length); + } +} + +const SIMPLE_ESCAPE_SEQUENCES = +{ __proto__: null, b: "\b", f: "\f", n: "\n", r: "\r", t: "\t", v: "\v" }; + +/** + * Reads a hex escape sequence. + * @param {TextReader} reader The reader should be positioned on the first hexadecimal digit. + * @param {number} length The number of hexadecimal digits. + * @returns {string} A code unit. + */ +function readHexSequence(reader, length) { + const str = reader.read(0, length); + const charCode = parseInt(str, 16); + + reader.advance(length); + return String.fromCharCode(charCode); +} + +/** + * Reads a Unicode escape sequence. + * @param {TextReader} reader The reader should be positioned after the "u". + * @returns {string} A code unit. + */ +function readUnicodeSequence(reader) { + const regExp = /\{(?[\dA-Fa-f]+)\}/uy; + + regExp.lastIndex = reader.pos; + const match = regExp.exec(reader.source); + + if (match) { + const codePoint = parseInt(match.groups.hexDigits, 16); + + reader.pos = regExp.lastIndex; + return String.fromCodePoint(codePoint); + } + return readHexSequence(reader, 4); +} + +/** + * Reads an octal escape sequence. + * @param {TextReader} reader The reader should be positioned after the first octal digit. + * @param {number} maxLength The maximum number of octal digits. + * @returns {string} A code unit. + */ +function readOctalSequence(reader, maxLength) { + const [octalStr] = reader.read(-1, maxLength).match(/^[0-7]+/u); + + reader.advance(octalStr.length - 1); + const octal = parseInt(octalStr, 8); + + return String.fromCharCode(octal); +} + +/** + * Reads an escape sequence or line continuation. + * @param {TextReader} reader The reader should be positioned on the backslash. + * @returns {string} A string of zero, one or two code units. + */ +function readEscapeSequenceOrLineContinuation(reader) { + const char = reader.read(1); + + reader.advance(2); + const unitChar = SIMPLE_ESCAPE_SEQUENCES[char]; + + if (unitChar) { + return unitChar; + } + switch (char) { + case "x": + return readHexSequence(reader, 2); + case "u": + return readUnicodeSequence(reader); + case "\r": + if (reader.read() === "\n") { + reader.advance(1); + } + + // fallthrough + case "\n": + case "\u2028": + case "\u2029": + return ""; + case "0": + case "1": + case "2": + case "3": + return readOctalSequence(reader, 3); + case "4": + case "5": + case "6": + case "7": + return readOctalSequence(reader, 2); + default: + return char; + } +} + +/** + * Reads an escape sequence or line continuation and generates the respective `CodeUnit` elements. + * @param {TextReader} reader The reader should be positioned on the backslash. + * @returns {Generator} Zero, one or two `CodeUnit` elements. + */ +function *mapEscapeSequenceOrLineContinuation(reader) { + const start = reader.pos; + const str = readEscapeSequenceOrLineContinuation(reader); + const end = reader.pos; + const source = reader.source.slice(start, end); + + switch (str.length) { + case 0: + break; + case 1: + yield new CodeUnit(start, source); + break; + default: + yield new CodeUnit(start, source); + yield new CodeUnit(start, source); + break; + } +} + +/** + * Parses a string literal. + * @param {string} source The string literal to parse, including the delimiting quotes. + * @returns {CodeUnit[]} A list of code units produced by the string literal. + */ +function parseStringLiteral(source) { + const reader = new TextReader(source); + const quote = reader.read(); + + reader.advance(1); + const codeUnits = []; + + for (;;) { + const char = reader.read(); + + if (char === quote) { + break; + } + if (char === "\\") { + codeUnits.push(...mapEscapeSequenceOrLineContinuation(reader)); + } else { + codeUnits.push(new CodeUnit(reader.pos, char)); + reader.advance(1); + } + } + return codeUnits; +} + +/** + * Parses a template token. + * @param {string} source The template token to parse, including the delimiting sequences `` ` ``, `${` and `}`. + * @returns {CodeUnit[]} A list of code units produced by the template token. + */ +function parseTemplateToken(source) { + const reader = new TextReader(source); + + reader.advance(1); + const codeUnits = []; + + for (;;) { + const char = reader.read(); + + if (char === "`" || char === "$" && reader.read(1) === "{") { + break; + } + if (char === "\\") { + codeUnits.push(...mapEscapeSequenceOrLineContinuation(reader)); + } else { + let unitSource; + + if (char === "\r" && reader.read(1) === "\n") { + unitSource = "\r\n"; + } else { + unitSource = char; + } + codeUnits.push(new CodeUnit(reader.pos, unitSource)); + reader.advance(unitSource.length); + } + } + return codeUnits; +} + +module.exports = { parseStringLiteral, parseTemplateToken }; diff --git a/tests/lib/rules/no-misleading-character-class.js b/tests/lib/rules/no-misleading-character-class.js index 6ad54d42d4a..6a276ae12c2 100644 --- a/tests/lib/rules/no-misleading-character-class.js +++ b/tests/lib/rules/no-misleading-character-class.js @@ -40,6 +40,13 @@ ruleTester.run("no-misleading-character-class", rule, { "var r = /πŸ‡―πŸ‡΅/", "var r = /[JP]/", "var r = /πŸ‘¨β€πŸ‘©β€πŸ‘¦/", + "new RegExp()", + "var r = RegExp(/[πŸ‘]/u)", + "const regex = /[πŸ‘]/u; new RegExp(regex);", + { + code: "new RegExp('[πŸ‘]')", + languageOptions: { globals: { RegExp: "off" } } + }, // Ignore solo lead/tail surrogate. "var r = /[\\uD83D]/", @@ -72,6 +79,16 @@ ruleTester.run("no-misleading-character-class", rule, { { code: "var r = new globalThis.RegExp('[Á] [ ');", languageOptions: { ecmaVersion: 2020 } }, { code: "var r = globalThis.RegExp('{ [Á]', 'u');", languageOptions: { ecmaVersion: 2020 } }, + // don't report on templates with expressions + "var r = RegExp(`${x}[πŸ‘]`)", + + // don't report on unknown flags + "var r = new RegExp('[πŸ‡―πŸ‡΅]', `${foo}`)", + String.raw`var r = new RegExp("[πŸ‘]", flags)`, + + // don't report on spread arguments + "const args = ['[πŸ‘]', 'i']; new RegExp(...args);", + // ES2024 { code: "var r = /[πŸ‘]/v", languageOptions: { ecmaVersion: 2024 } }, { code: String.raw`var r = /^[\q{πŸ‘ΆπŸ»}]$/v`, languageOptions: { ecmaVersion: 2024 } }, @@ -625,23 +642,14 @@ ruleTester.run("no-misleading-character-class", rule, { { code: "var r = new RegExp(`\r\n[❇️]`)", errors: [{ - line: 1, - column: 20, + line: 2, + column: 2, endLine: 2, - endColumn: 6, + endColumn: 4, messageId: "combiningClass", suggestions: null }] }, - { - code: String.raw`var r = new RegExp("[πŸ‘]", flags)`, - errors: [{ - column: 22, - endColumn: 24, - messageId: "surrogatePairWithoutUFlag", - suggestions: null - }] - }, { code: String.raw`const flags = ""; var r = new RegExp("[πŸ‘]", flags)`, errors: [{ @@ -654,8 +662,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = RegExp("[\\uD83D\\uDC4D]", "")`, errors: [{ - column: 16, - endColumn: 34, + column: 18, + endColumn: 32, messageId: "surrogatePairWithoutUFlag", suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = RegExp("[\\uD83D\\uDC4D]", "u")` }] }] @@ -663,8 +671,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = RegExp("before[\\uD83D\\uDC4D]after", "")`, errors: [{ - column: 16, - endColumn: 45, + column: 24, + endColumn: 38, messageId: "surrogatePairWithoutUFlag", suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = RegExp("before[\\uD83D\\uDC4D]after", "u")` }] }] @@ -672,8 +680,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = RegExp("[before\\uD83D\\uDC4Dafter]", "")`, errors: [{ - column: 16, - endColumn: 45, + column: 24, + endColumn: 38, messageId: "surrogatePairWithoutUFlag", suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = RegExp("[before\\uD83D\\uDC4Dafter]", "u")` }] }] @@ -681,8 +689,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = RegExp("\t\t\tπŸ‘[πŸ‘]")`, errors: [{ - column: 16, - endColumn: 30, + column: 26, + endColumn: 28, messageId: "surrogatePairWithoutUFlag", suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = RegExp("\t\t\tπŸ‘[πŸ‘]", "u")` }] }] @@ -690,8 +698,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("\u1234[\\uD83D\\uDC4D]")`, errors: [{ - column: 20, - endColumn: 44, + column: 28, + endColumn: 42, messageId: "surrogatePairWithoutUFlag", suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = new RegExp("\u1234[\\uD83D\\uDC4D]", "u")` }] }] @@ -699,8 +707,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("\\u1234\\u5678πŸ‘Ž[πŸ‘]")`, errors: [{ - column: 20, - endColumn: 42, + column: 38, + endColumn: 40, messageId: "surrogatePairWithoutUFlag", suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = new RegExp("\\u1234\\u5678πŸ‘Ž[πŸ‘]", "u")` }] }] @@ -708,8 +716,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("\\u1234\\u5678πŸ‘[πŸ‘]")`, errors: [{ - column: 20, - endColumn: 42, + column: 38, + endColumn: 40, messageId: "surrogatePairWithoutUFlag", suggestions: [{ messageId: "suggestUnicodeFlag", output: String.raw`var r = new RegExp("\\u1234\\u5678πŸ‘[πŸ‘]", "u")` }] }] @@ -737,8 +745,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("[πŸ‘]\\a", "")`, errors: [{ - column: 20, - endColumn: 29, + column: 22, + endColumn: 24, messageId: "surrogatePairWithoutUFlag", suggestions: null // pattern would be invalid with the 'u' flag }] @@ -784,8 +792,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("[\\u0041\\u0301]", "")`, errors: [{ - column: 20, - endColumn: 38, + column: 22, + endColumn: 36, messageId: "combiningClass", suggestions: null }] @@ -793,8 +801,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("[\\u0041\\u0301]", "u")`, errors: [{ - column: 20, - endColumn: 38, + column: 22, + endColumn: 36, messageId: "combiningClass", suggestions: null }] @@ -802,8 +810,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("[\\u{41}\\u{301}]", "u")`, errors: [{ - column: 20, - endColumn: 39, + column: 22, + endColumn: 37, messageId: "combiningClass", suggestions: null }] @@ -829,8 +837,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`new RegExp("[ \\ufe0f]", "")`, errors: [{ - column: 12, - endColumn: 24, + column: 14, + endColumn: 22, messageId: "combiningClass", suggestions: null }] @@ -838,8 +846,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`new RegExp("[ \\ufe0f]", "u")`, errors: [{ - column: 12, - endColumn: 24, + column: 14, + endColumn: 22, messageId: "combiningClass", suggestions: null }] @@ -848,8 +856,14 @@ ruleTester.run("no-misleading-character-class", rule, { code: String.raw`new RegExp("[ \\ufe0f][ \\ufe0f]")`, errors: [ { - column: 12, - endColumn: 34, + column: 14, + endColumn: 22, + messageId: "combiningClass", + suggestions: null + }, + { + column: 24, + endColumn: 32, messageId: "combiningClass", suggestions: null } @@ -858,8 +872,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("[\\u2747\\uFE0F]", "")`, errors: [{ - column: 20, - endColumn: 38, + column: 22, + endColumn: 36, messageId: "combiningClass", suggestions: null }] @@ -867,8 +881,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("[\\u2747\\uFE0F]", "u")`, errors: [{ - column: 20, - endColumn: 38, + column: 22, + endColumn: 36, messageId: "combiningClass", suggestions: null }] @@ -876,8 +890,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("[\\u{2747}\\u{FE0F}]", "u")`, errors: [{ - column: 20, - endColumn: 42, + column: 22, + endColumn: 40, messageId: "combiningClass", suggestions: null }] @@ -911,8 +925,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("[\\uD83D\\uDC76\\uD83C\\uDFFB]", "u")`, errors: [{ - column: 20, - endColumn: 52, + column: 22, + endColumn: 50, messageId: "emojiModifier", suggestions: null }] @@ -920,8 +934,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("[\\u{1F476}\\u{1F3FB}]", "u")`, errors: [{ - column: 20, - endColumn: 44, + column: 22, + endColumn: 42, messageId: "emojiModifier", suggestions: null }] @@ -938,8 +952,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: "var r = RegExp(`\\t\\t\\tπŸ‘[πŸ‘]`)", errors: [{ - column: 16, - endColumn: 30, + column: 26, + endColumn: 28, messageId: "surrogatePairWithoutUFlag", suggestions: [{ messageId: "suggestUnicodeFlag", output: "var r = RegExp(`\\t\\t\\tπŸ‘[πŸ‘]`, \"u\")" }] }] @@ -995,23 +1009,6 @@ ruleTester.run("no-misleading-character-class", rule, { } ] }, - { - code: "var r = new RegExp('[πŸ‡―πŸ‡΅]', `${foo}`)", - errors: [ - { - column: 22, - endColumn: 24, - messageId: "surrogatePairWithoutUFlag", - suggestions: [{ messageId: "suggestUnicodeFlag", output: "var r = new RegExp('[πŸ‡―πŸ‡΅]', `${foo}u`)" }] - }, - { - column: 24, - endColumn: 26, - messageId: "surrogatePairWithoutUFlag", - suggestions: [{ messageId: "suggestUnicodeFlag", output: "var r = new RegExp('[πŸ‡―πŸ‡΅]', `${foo}u`)" }] - } - ] - }, { code: String.raw`var r = new RegExp("[πŸ‡―πŸ‡΅]")`, errors: [ @@ -1111,8 +1108,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("[\\uD83C\\uDDEF\\uD83C\\uDDF5]", "u")`, errors: [{ - column: 20, - endColumn: 52, + column: 22, + endColumn: 50, messageId: "regionalIndicatorSymbol", suggestions: null }] @@ -1120,8 +1117,8 @@ ruleTester.run("no-misleading-character-class", rule, { { code: String.raw`var r = new RegExp("[\\u{1F1EF}\\u{1F1F5}]", "u")`, errors: [{ - column: 20, - endColumn: 44, + column: 22, + endColumn: 42, messageId: "regionalIndicatorSymbol", suggestions: null }] @@ -1238,8 +1235,8 @@ ruleTester.run("no-misleading-character-class", rule, { code: String.raw`var r = new RegExp("[\\uD83D\\uDC68\\u200D\\uD83D\\uDC69\\u200D\\uD83D\\uDC66]", "u")`, errors: [ { - column: 20, - endColumn: 80, + column: 22, + endColumn: 78, messageId: "zwj", suggestions: null } @@ -1249,8 +1246,8 @@ ruleTester.run("no-misleading-character-class", rule, { code: String.raw`var r = new RegExp("[\\u{1F468}\\u{200D}\\u{1F469}\\u{200D}\\u{1F466}]", "u")`, errors: [ { - column: 20, - endColumn: 72, + column: 22, + endColumn: 70, messageId: "zwj", suggestions: null } @@ -1299,8 +1296,8 @@ ruleTester.run("no-misleading-character-class", rule, { languageOptions: { ecmaVersion: 2020 }, errors: [ { - column: 31, - endColumn: 83, + column: 33, + endColumn: 81, messageId: "zwj", suggestions: null } @@ -1335,8 +1332,242 @@ ruleTester.run("no-misleading-character-class", rule, { }] }, + // no granular reports on templates with expressions + { + code: 'new RegExp(`${"[πŸ‘πŸ‡―πŸ‡΅]"}[😊]`);', + errors: [{ + column: 12, + endColumn: 31, + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ + messageId: "suggestUnicodeFlag", + output: 'new RegExp(`${"[πŸ‘πŸ‡―πŸ‡΅]"}[😊]`, "u");' + }] + }] + }, + + // no granular reports on identifiers + { + code: 'const pattern = "[πŸ‘]"; new RegExp(pattern);', + errors: [{ + column: 36, + endColumn: 43, + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ + messageId: "suggestUnicodeFlag", + output: 'const pattern = "[πŸ‘]"; new RegExp(pattern, "u");' + }] + }] + }, + + // second argument in RegExp should override flags in regex literal + { + code: "RegExp(/[aπŸ‘z]/u, '');", + errors: [{ + column: 11, + endColumn: 13, + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ + messageId: "suggestUnicodeFlag", + output: "RegExp(/[aπŸ‘z]/u, 'u');" + }] + }] + }, + + /* + * These test cases have been disabled because of a limitation in Node.js 18, see https://github.com/eslint/eslint/pull/18082#discussion_r1506142421. + * + * { + * code: "const pattern = /[πŸ‘]/u; RegExp(pattern, '');", + * errors: [{ + * column: 33, + * endColumn: 40, + * messageId: "surrogatePairWithoutUFlag", + * suggestions: [{ + * messageId: "suggestUnicodeFlag", + * output: "const pattern = /[πŸ‘]/u; RegExp(pattern, 'u');" + * }] + * }] + * }, + * { + * code: "const pattern = /[πŸ‘]/g; RegExp(pattern, 'i');", + * errors: [{ + * column: 19, + * endColumn: 21, + * messageId: "surrogatePairWithoutUFlag", + * suggestions: [{ + * messageId: "suggestUnicodeFlag", + * output: "const pattern = /[πŸ‘]/gu; RegExp(pattern, 'i');" + * }] + * }, { + * column: 33, + * endColumn: 40, + * messageId: "surrogatePairWithoutUFlag", + * suggestions: [{ + * messageId: "suggestUnicodeFlag", + * output: "const pattern = /[πŸ‘]/g; RegExp(pattern, 'iu');" + * }] + * }] + * }, + */ + + // report only on regex literal if no flags are supplied + { + code: "RegExp(/[πŸ‘]/)", + errors: [{ + column: 10, + endColumn: 12, + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: "RegExp(/[πŸ‘]/u)" }] + }] + }, + + // report only on RegExp call if a regex literal and flags are supplied + { + code: "RegExp(/[πŸ‘]/, 'i');", + errors: [{ + column: 10, + endColumn: 12, + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: "RegExp(/[πŸ‘]/, 'iu');" }] + }] + }, + + // ignore RegExp if not built-in + { + code: "RegExp(/[πŸ‘]/, 'g');", + languageOptions: { globals: { RegExp: "off" } }, + errors: [{ + column: 10, + endColumn: 12, + messageId: "surrogatePairWithoutUFlag", + suggestions: [{ messageId: "suggestUnicodeFlag", output: "RegExp(/[πŸ‘]/u, 'g');" }] + }] + }, + + { + code: String.raw` + + // "[" and "]" escaped as "\x5B" and "\u005D" + new RegExp("\x5B \\ufe0f\u005D") + + `, + errors: [{ + column: 29, + endColumn: 37, + messageId: "combiningClass", + suggestions: null + }] + }, + { + code: String.raw` + + // backslash escaped as "\u{5c}" + new RegExp("[ \u{5c}ufe0f]") + + `, + errors: [{ + column: 26, + endColumn: 38, + messageId: "combiningClass", + suggestions: null + }] + }, + { + code: String.raw` + + // "0" escaped as "\60" + new RegExp("[ \\ufe\60f]") + + `, + languageOptions: { sourceType: "script" }, + errors: [{ + column: 26, + endColumn: 36, + messageId: "combiningClass", + suggestions: null + }] + }, + { + code: String.raw` + + // "e" escaped as "\e" + new RegExp("[ \\uf\e0f]") + + `, + errors: [{ + column: 26, + endColumn: 35, + messageId: "combiningClass", + suggestions: null + }] + }, + { + code: String.raw` + + // line continuation: backslash + + + new RegExp('[ \\ufe0f]') + + `.replace("", "\\\r\n"), + errors: [{ + line: 4, + column: 26, + endLine: 5, + endColumn: 5, + messageId: "combiningClass", + suggestions: null + }] + }, + { + code: String.raw` + + // just a backslash escaped as "\\" + new RegExp([.\\u200D.]) + + `.replaceAll("", "`"), + errors: [{ + column: 26, + endColumn: 35, + messageId: "zwj", + suggestions: null + }] + }, + { + code: String.raw` + + // "u" escaped as "\x75" + new RegExp([.\\\x75200D.]) + + `.replaceAll("", "`"), + errors: [{ + column: 26, + endColumn: 38, + messageId: "zwj", + suggestions: null + }] + }, + + /* eslint-disable lines-around-comment, internal-rules/multiline-comment-style -- see https://github.com/eslint/eslint/issues/18081 */ + + { + code: String.raw` + + // unescaped counts as a single character + new RegExp([\\u200D.]) + + `.replaceAll("", "`").replace("", "\n"), + errors: [{ + line: 4, + column: 26, + endLine: 5, + endColumn: 9, + messageId: "zwj", + suggestions: null + }] + }, // ES2024 + { code: "var r = /[[πŸ‘ΆπŸ»]]/v", languageOptions: { ecmaVersion: 2024 }, @@ -1348,17 +1579,41 @@ ruleTester.run("no-misleading-character-class", rule, { }] }, { - code: "var r = /[πŸ‘]/", + code: "new RegExp(/^[πŸ‘]$/v, '')", languageOptions: { - ecmaVersion: 2015 + ecmaVersion: 2024 }, errors: [{ - column: 11, - endColumn: 13, + column: 15, + endColumn: 17, messageId: "surrogatePairWithoutUFlag", - suggestions: [{ messageId: "suggestUnicodeFlag", output: "var r = /[πŸ‘]/u" }] + suggestions: [{ messageId: "suggestUnicodeFlag", output: "new RegExp(/^[πŸ‘]$/v, 'u')" }] }] } + /* + * This test case has been disabled because of a limitation in Node.js 18, see https://github.com/eslint/eslint/pull/18082#discussion_r1506142421. + * + * { + * code: "var r = /[πŸ‘ΆπŸ»]/v; RegExp(r, 'v');", + * languageOptions: { + * ecmaVersion: 2024 + * }, + * errors: [{ + * column: 11, + * endColumn: 15, + * messageId: "emojiModifier", + * suggestions: null + * }, { + * column: 27, + * endColumn: 28, + * messageId: "emojiModifier", + * suggestions: null + * }] + * } + */ + + /* eslint-enable lines-around-comment, internal-rules/multiline-comment-style -- re-enable rule */ + ] }); diff --git a/tests/lib/rules/utils/char-source.js b/tests/lib/rules/utils/char-source.js new file mode 100644 index 00000000000..2f37d9f3c0f --- /dev/null +++ b/tests/lib/rules/utils/char-source.js @@ -0,0 +1,256 @@ +"use strict"; + +const assertStrict = require("node:assert/strict"); +const { parseStringLiteral, parseTemplateToken } = require("../../../../lib/rules/utils/char-source"); + +describe( + "parseStringLiteral", + () => { + const TESTS = [ + { + description: "works with an empty string", + source: '""', + expectedCodeUnits: [] + }, + { + description: "works with surrogate pairs", + source: '"aπ„žz"', + expectedCodeUnits: [ + { start: 1, source: "a" }, + { start: 2, source: "\ud834" }, + { start: 3, source: "\udd1e" }, + { start: 4, source: "z" } + ] + }, + { + description: "works with escape sequences for single characters", + source: '"a\\x40\\u231Bz"', + expectedCodeUnits: [ + { start: 1, source: "a" }, + { start: 2, source: "\\x40" }, + { start: 6, source: "\\u231B" }, + { start: 12, source: "z" } + ] + }, + { + description: "works with escape sequences for code points", + source: '"a\\u{ffff}\\u{10000}\\u{10ffff}z"', + expectedCodeUnits: [ + { start: 1, source: "a" }, + { start: 2, source: "\\u{ffff}" }, + { start: 10, source: "\\u{10000}" }, + { start: 10, source: "\\u{10000}" }, + { start: 19, source: "\\u{10ffff}" }, + { start: 19, source: "\\u{10ffff}" }, + { start: 29, source: "z" } + ] + }, + { + description: "works with line continuations", + source: '"a\\\n\\\r\n\\\u2028\\\u2029z"', + expectedCodeUnits: [ + { start: 1, source: "a" }, + { start: 11, source: "z" } + ] + }, + { + description: "works with simple escape sequences", + source: '"\\"\\0\\b\\f\\n\\r\\t\\v"', + expectedCodeUnits: ['\\"', "\\0", "\\b", "\\f", "\\n", "\\r", "\\t", "\\v"] + .map((source, index) => ({ source, start: 1 + index * 2 })) + }, + { + description: "works with a character outside of a line continuation", + source: '"a\u2028z"', + expectedCodeUnits: [ + { start: 1, source: "a" }, + { start: 2, source: "\u2028" }, + { start: 3, source: "z" } + ] + }, + { + description: "works with a character outside of a line continuation", + source: '"a\u2029z"', + expectedCodeUnits: [ + { start: 1, source: "a" }, + { start: 2, source: "\u2029" }, + { start: 3, source: "z" } + ] + }, + { + description: "works with octal escape sequences", + source: '"\\0123\\456"', + expectedCodeUnits: [ + { source: "\\012", start: 1 }, + { source: "3", start: 5 }, + { source: "\\45", start: 6 }, + { source: "6", start: 9 } + ] + }, + { + description: "works with an escaped 7", + source: '"\\7"', + expectedCodeUnits: [{ source: "\\7", start: 1 }] + }, + { + description: "works with an escaped 8", + source: '"\\8"', + expectedCodeUnits: [{ source: "\\8", start: 1 }] + }, + { + description: "works with an escaped 9", + source: '"\\9"', + expectedCodeUnits: [{ source: "\\9", start: 1 }] + }, + { + description: 'works with the escaped sequence "00"', + source: '"\\00"', + expectedCodeUnits: [{ source: "\\00", start: 1 }] + }, + { + description: "works with an escaped 0 followed by 8", + source: '"\\08"', + expectedCodeUnits: [ + { source: "\\0", start: 1 }, + { source: "8", start: 3 } + ] + }, + { + description: "works with an escaped 0 followed by 9", + source: '"\\09"', + expectedCodeUnits: [ + { source: "\\0", start: 1 }, + { source: "9", start: 3 } + ] + } + ]; + + for (const { description, source, expectedCodeUnits, only } of TESTS) { + (only ? it.only : it)( + description, + () => { + const codeUnits = parseStringLiteral(source); + const expectedCharCount = expectedCodeUnits.length; + + assertStrict.equal(codeUnits.length, expectedCharCount); + for (let index = 0; index < expectedCharCount; ++index) { + const codeUnit = codeUnits[index]; + const expectedUnit = expectedCodeUnits[index]; + const message = `Expected values to be strictly equal at index ${index}`; + + assertStrict.equal(codeUnit.start, expectedUnit.start, message); + assertStrict.equal(codeUnit.source, expectedUnit.source, message); + } + } + ); + } + } +); + +describe( + "parseTemplateToken", + () => { + const TESTS = + [ + { + description: "works with an empty template", + source: "``", + expectedCodeUnits: [] + }, + { + description: "works with surrogate pairs", + source: "`Aπ„žZ`", + expectedCodeUnits: [ + { start: 1, source: "A" }, + { start: 2, source: "\ud834" }, + { start: 3, source: "\udd1e" }, + { start: 4, source: "Z" } + ] + }, + { + description: "works with escape sequences for single characters", + source: "`A\\x40\\u231BZ${", + expectedCodeUnits: [ + { start: 1, source: "A" }, + { start: 2, source: "\\x40" }, + { start: 6, source: "\\u231B" }, + { start: 12, source: "Z" } + ] + }, + { + description: "works with escape sequences for code points", + source: "}A\\u{FFFF}\\u{10000}\\u{10FFFF}Z${", + expectedCodeUnits: [ + { start: 1, source: "A" }, + { start: 2, source: "\\u{FFFF}" }, + { start: 10, source: "\\u{10000}" }, + { start: 10, source: "\\u{10000}" }, + { start: 19, source: "\\u{10FFFF}" }, + { start: 19, source: "\\u{10FFFF}" }, + { start: 29, source: "Z" } + ] + }, + { + description: "works with line continuations", + source: "}A\\\n\\\r\n\\\u2028\\\u2029Z`", + expectedCodeUnits: [ + { start: 1, source: "A" }, + { start: 11, source: "Z" } + ] + }, + { + description: "works with simple escape sequences", + source: "`\\0\\`\\b\\f\\n\\r\\t\\v`", + expectedCodeUnits: ["\\0", "\\`", "\\b", "\\f", "\\n", "\\r", "\\t", "\\v"] + .map((source, index) => ({ source, start: 1 + index * 2 })) + }, + { + description: "works with a character outside of a line continuation", + source: "`a\u2028z`", + expectedCodeUnits: [ + { start: 1, source: "a" }, + { start: 2, source: "\u2028" }, + { start: 3, source: "z" } + ] + }, + { + description: "works with a character outside of a line continuation", + source: "`a\u2029z`", + expectedCodeUnits: [ + { start: 1, source: "a" }, + { start: 2, source: "\u2029" }, + { start: 3, source: "z" } + ] + }, + { + description: "works with unescaped sequences", + source: "`A\r\nZ`", + expectedCodeUnits: [ + { start: 1, source: "A" }, + { start: 2, source: "\r\n" }, + { start: 4, source: "Z" } + ] + } + ]; + + for (const { description, source, expectedCodeUnits, only } of TESTS) { + (only ? it.only : it)( + description, + () => { + const codeUnits = parseTemplateToken(source); + const expectedCharCount = expectedCodeUnits.length; + + assertStrict.equal(codeUnits.length, expectedCharCount); + for (let index = 0; index < expectedCharCount; ++index) { + const codeUnit = codeUnits[index]; + const expectedUnit = expectedCodeUnits[index]; + const message = `Expected values to be strictly equal at index ${index}`; + + assertStrict.equal(codeUnit.start, expectedUnit.start, message); + assertStrict.equal(codeUnit.source, expectedUnit.source, message); + } + } + ); + } + } +);