From 6a86e5018a3733049c09261bcabae422fbea893d Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 8 May 2021 02:37:10 +0200 Subject: [PATCH 01/67] Chore: remove loose-parser tests (fixes #14315) (#14569) --- package.json | 1 - tests/lib/rules/no-redeclare.js | 108 -------------------------------- tests/tools/loose-parser.js | 30 --------- 3 files changed, 139 deletions(-) delete mode 100644 tests/tools/loose-parser.js diff --git a/package.json b/package.json index 4f87ef6403b..72070e2a0f5 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,6 @@ "devDependencies": { "@babel/core": "^7.4.3", "@babel/preset-env": "^7.4.3", - "acorn": "^7.2.0", "babel-loader": "^8.0.5", "chai": "^4.0.1", "cheerio": "^0.22.0", diff --git a/tests/lib/rules/no-redeclare.js b/tests/lib/rules/no-redeclare.js index f6293b7e152..f89d6853fa3 100644 --- a/tests/lib/rules/no-redeclare.js +++ b/tests/lib/rules/no-redeclare.js @@ -9,7 +9,6 @@ // Requirements //------------------------------------------------------------------------------ -const path = require("path"); const rule = require("../../../lib/rules/no-redeclare"); const { RuleTester } = require("../../../lib/rule-tester"); @@ -17,7 +16,6 @@ const { RuleTester } = require("../../../lib/rule-tester"); // Tests //------------------------------------------------------------------------------ -const looseParserPath = path.resolve(__dirname, "../../tools/loose-parser.js"); const ruleTester = new RuleTester(); ruleTester.run("no-redeclare", rule, { @@ -205,112 +203,6 @@ ruleTester.run("no-redeclare", rule, { ] }, - // let/const - { - code: "let a; let a;", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "let a; let a;", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015, sourceType: "module" }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "let a; let a;", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015, ecmaFeatures: { globalReturn: true } }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "let a; const a = 0;", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "const a = 0; const a = 0;", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "if (test) { let a; let a; }", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "switch (test) { case 0: let a; let a; }", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "for (let a, a;;);", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "for (let [a, a] in xs);", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "for (let [a, a] of xs);", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "function f() { let a; let a; }", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "function f(a) { let a; }", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "function f() { if (test) { let a; let a; } }", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - // Comments and built-ins. { code: "/*globals Array */", diff --git a/tests/tools/loose-parser.js b/tests/tools/loose-parser.js deleted file mode 100644 index 98e97cc3687..00000000000 --- a/tests/tools/loose-parser.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @fileoverview Define a custom parser to ignore recoverable syntax errors. - * @author Toru Nagashima - * - * no-redeclare rule uses this parser to check redeclarations. - */ -"use strict"; - -const acorn = require("acorn"); -const espree = require("espree/lib/espree"); - -/** - * Define the parser which ignores recoverable errors. - * @returns {(parser:acorn.Parser) => acorn.Parser} The function that defines loose parser. - */ -function loose() { - return Parser => class LooseParser extends Parser { - raiseRecoverable() { // eslint-disable-line class-methods-use-this - // ignore - } - }; -} - -const LooseEspree = acorn.Parser.extend(espree(), loose()); - -module.exports = { - parse(code, options) { - return new LooseEspree(options, code).parse(); - } -}; From ae6dbd148aaca83e4bd04b9351b54029c50fac8a Mon Sep 17 00:00:00 2001 From: Patrick Ahmetovic <23729362+patriscus@users.noreply.github.com> Date: Sat, 8 May 2021 02:54:49 +0200 Subject: [PATCH 02/67] Fix: track variables, not names in require-atomic-updates (fixes #14208) (#14282) * Fix: require-atomic-updates false positives in loops (fixes #14208) * Rework & store variables in segmentInfo instead of variable names * Skip unresolved references in createReferenceMap --- lib/rules/require-atomic-updates.js | 43 +++++++++++--------- tests/lib/rules/require-atomic-updates.js | 49 ++++++++++++++++++++++- 2 files changed, 71 insertions(+), 21 deletions(-) diff --git a/lib/rules/require-atomic-updates.js b/lib/rules/require-atomic-updates.js index c552f1bd825..b3df907420c 100644 --- a/lib/rules/require-atomic-updates.js +++ b/lib/rules/require-atomic-updates.js @@ -13,6 +13,10 @@ */ function createReferenceMap(scope, outReferenceMap = new Map()) { for (const reference of scope.references) { + if (reference.resolved === null) { + continue; + } + outReferenceMap.set(reference.identifier, reference); } for (const childScope of scope.childScopes) { @@ -86,42 +90,42 @@ class SegmentInfo { * @returns {void} */ initialize(segment) { - const outdatedReadVariableNames = new Set(); - const freshReadVariableNames = new Set(); + const outdatedReadVariables = new Set(); + const freshReadVariables = new Set(); for (const prevSegment of segment.prevSegments) { const info = this.info.get(prevSegment); if (info) { - info.outdatedReadVariableNames.forEach(Set.prototype.add, outdatedReadVariableNames); - info.freshReadVariableNames.forEach(Set.prototype.add, freshReadVariableNames); + info.outdatedReadVariables.forEach(Set.prototype.add, outdatedReadVariables); + info.freshReadVariables.forEach(Set.prototype.add, freshReadVariables); } } - this.info.set(segment, { outdatedReadVariableNames, freshReadVariableNames }); + this.info.set(segment, { outdatedReadVariables, freshReadVariables }); } /** * Mark a given variable as read on given segments. * @param {PathSegment[]} segments The segments that it read the variable on. - * @param {string} variableName The variable name to be read. + * @param {Variable} variable The variable to be read. * @returns {void} */ - markAsRead(segments, variableName) { + markAsRead(segments, variable) { for (const segment of segments) { const info = this.info.get(segment); if (info) { - info.freshReadVariableNames.add(variableName); + info.freshReadVariables.add(variable); // If a variable is freshly read again, then it's no more out-dated. - info.outdatedReadVariableNames.delete(variableName); + info.outdatedReadVariables.delete(variable); } } } /** - * Move `freshReadVariableNames` to `outdatedReadVariableNames`. + * Move `freshReadVariables` to `outdatedReadVariables`. * @param {PathSegment[]} segments The segments to process. * @returns {void} */ @@ -130,8 +134,8 @@ class SegmentInfo { const info = this.info.get(segment); if (info) { - info.freshReadVariableNames.forEach(Set.prototype.add, info.outdatedReadVariableNames); - info.freshReadVariableNames.clear(); + info.freshReadVariables.forEach(Set.prototype.add, info.outdatedReadVariables); + info.freshReadVariables.clear(); } } } @@ -139,14 +143,14 @@ class SegmentInfo { /** * Check if a given variable is outdated on the current segments. * @param {PathSegment[]} segments The current segments. - * @param {string} variableName The variable name to check. + * @param {Variable} variable The variable to check. * @returns {boolean} `true` if the variable is outdated on the segments. */ - isOutdated(segments, variableName) { + isOutdated(segments, variable) { for (const segment of segments) { const info = this.info.get(segment); - if (info && info.outdatedReadVariableNames.has(variableName)) { + if (info && info.outdatedReadVariables.has(variable)) { return true; } } @@ -214,14 +218,13 @@ module.exports = { if (!reference) { return; } - const name = reference.identifier.name; const variable = reference.resolved; const writeExpr = getWriteExpr(reference); const isMemberAccess = reference.identifier.parent.type === "MemberExpression"; // Add a fresh read variable. if (reference.isRead() && !(writeExpr && writeExpr.parent.operator === "=")) { - segmentInfo.markAsRead(codePath.currentSegments, name); + segmentInfo.markAsRead(codePath.currentSegments, variable); } /* @@ -245,7 +248,7 @@ module.exports = { /* * Verify assignments. - * If the reference exists in `outdatedReadVariableNames` list, report it. + * If the reference exists in `outdatedReadVariables` list, report it. */ ":expression:exit"(node) { const { codePath, referenceMap } = stack; @@ -267,9 +270,9 @@ module.exports = { assignmentReferences.delete(node); for (const reference of references) { - const name = reference.identifier.name; + const variable = reference.resolved; - if (segmentInfo.isOutdated(codePath.currentSegments, name)) { + if (segmentInfo.isOutdated(codePath.currentSegments, variable)) { context.report({ node: node.parent, messageId: "nonAtomicUpdate", diff --git a/tests/lib/rules/require-atomic-updates.js b/tests/lib/rules/require-atomic-updates.js index cd51e38ffbd..bd3738ac979 100644 --- a/tests/lib/rules/require-atomic-updates.js +++ b/tests/lib/rules/require-atomic-updates.js @@ -53,6 +53,7 @@ ruleTester.run("require-atomic-updates", rule, { "let foo; async function x() { foo = condition ? foo : await bar; }", "async function x() { let foo; bar(() => { let foo; blah(foo); }); foo += await result; }", "let foo; async function x() { foo = foo + 1; await bar; }", + "async function x() { foo += await bar; }", /* @@ -162,6 +163,52 @@ ruleTester.run("require-atomic-updates", rule, { count -= 1 return } + `, + + // https://github.com/eslint/eslint/issues/14208 + ` + async function foo(e) { + } + + async function run() { + const input = []; + const props = []; + + for(const entry of input) { + const prop = props.find(a => a.id === entry.id) || null; + await foo(entry); + } + + for(const entry of input) { + const prop = props.find(a => a.id === entry.id) || null; + } + + for(const entry2 of input) { + const prop = props.find(a => a.id === entry2.id) || null; + } + } + `, + + ` + async function run() { + { + let entry; + await entry; + } + { + let entry; + () => entry; + + entry = 1; + } + } + `, + + ` + async function run() { + await a; + b = 1; + } ` ], @@ -251,7 +298,7 @@ ruleTester.run("require-atomic-updates", rule, { errors: [COMPUTED_PROPERTY_ERROR, STATIC_PROPERTY_ERROR] }, { - code: "async function x() { foo += await bar; }", + code: "let foo = ''; async function x() { foo += await bar; }", errors: [VARIABLE_ERROR] }, { From aaf65e629adb74401092c3ccc9cb4e4bd1c8609b Mon Sep 17 00:00:00 2001 From: Brandon Mills Date: Fri, 7 May 2021 21:17:43 -0400 Subject: [PATCH 03/67] Upgrade: eslintrc for ModuleResolver fix (#14577) This incorporates https://github.com/eslint/eslintrc/pull/34, which allows fixing `CLIEngine`'s `ModuleResolver` import. --- lib/cli-engine/cli-engine.js | 9 ++------- package.json | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index b1befaa04fc..ca298f9c356 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -27,16 +27,11 @@ const { naming, CascadingConfigArrayFactory, IgnorePattern, - getUsedExtractedConfigs + getUsedExtractedConfigs, + ModuleResolver } } = require("@eslint/eslintrc"); -/* - * For some reason, ModuleResolver must be included via filepath instead of by - * API exports in order to work properly. That's why this is separated out onto - * its own require() statement. - */ -const ModuleResolver = require("@eslint/eslintrc/lib/shared/relative-module-resolver"); const { FileEnumerator } = require("./file-enumerator"); const { Linter } = require("../linter"); diff --git a/package.json b/package.json index 72070e2a0f5..2e26bf1c7b2 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "bugs": "https://github.com/eslint/eslint/issues/", "dependencies": { "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.0", + "@eslint/eslintrc": "^0.4.1", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", From 53bf14d1725a90dbc696e45d69ab515fc4cca63f Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 7 May 2021 22:14:32 -0400 Subject: [PATCH 04/67] Build: changelog update for 7.26.0 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8df3a479c79..b976ab2ea52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +v7.26.0 - May 7, 2021 + +* [`aaf65e6`](https://github.com/eslint/eslint/commit/aaf65e629adb74401092c3ccc9cb4e4bd1c8609b) Upgrade: eslintrc for ModuleResolver fix (#14577) (Brandon Mills) +* [`ae6dbd1`](https://github.com/eslint/eslint/commit/ae6dbd148aaca83e4bd04b9351b54029c50fac8a) Fix: track variables, not names in require-atomic-updates (fixes #14208) (#14282) (Patrick Ahmetovic) +* [`6a86e50`](https://github.com/eslint/eslint/commit/6a86e5018a3733049c09261bcabae422fbea893d) Chore: remove loose-parser tests (fixes #14315) (#14569) (Milos Djermanovic) +* [`ee3a3ea`](https://github.com/eslint/eslint/commit/ee3a3ead893d185cc4b1ae9041940cb0968767e1) Fix: create `.eslintrc.cjs` for `module` type (#14304) (Nitin Kumar) +* [`6791dec`](https://github.com/eslint/eslint/commit/6791decfc58b7b09cfd0aabd15a3d14148aae073) Docs: fix example for require-atomic-updates (#14562) (Milos Djermanovic) +* [`388eb7e`](https://github.com/eslint/eslint/commit/388eb7e14039b8951462b311d6121002ca5232cb) Sponsors: Sync README with website (ESLint Jenkins) +* [`f071d1e`](https://github.com/eslint/eslint/commit/f071d1ef91286bf2e3fb63d9b679ff7702819a1e) Update: Add automated suggestion to `radix` rule for parsing decimals (#14291) (Bryan Mishkin) +* [`0b6a3f3`](https://github.com/eslint/eslint/commit/0b6a3f31e6e78825114f82d4e0aed9cd72f784ac) New: Include XO style guide in `eslint --init` (#14193) (Federico Brigante) + v7.25.0 - April 23, 2021 * [`5df5e4a`](https://github.com/eslint/eslint/commit/5df5e4a9976964fcf4dc67e241d4e22ec1370fe0) Update: highlight last write reference for no-unused-vars (fixes #14324) (#14335) (Nitin Kumar) From 6e9aad8053a4ea7fb8b5beeb1f1173c740890185 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 7 May 2021 22:14:32 -0400 Subject: [PATCH 05/67] 7.26.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e26bf1c7b2..51a76ec31c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "7.25.0", + "version": "7.26.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From 6a2ced892c0dc43fa4942293b9f1c4b9151c3741 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 7 May 2021 22:20:28 -0400 Subject: [PATCH 06/67] Docs: Update README team and sponsors --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index f53e35ce8ee..a4f4a04ddec 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,11 @@ Milos Djermanovic The people who review and implement new features. - - - - - diff --git a/lib/cli-engine/formatters/html-template-message.js b/lib/cli-engine/formatters/html-template-message.js new file mode 100644 index 00000000000..c259618bf28 --- /dev/null +++ b/lib/cli-engine/formatters/html-template-message.js @@ -0,0 +1,25 @@ +"use strict"; + +module.exports = function(it, encodeHTML) { + const { + parentIndex, + lineNumber, + columnNumber, + severityNumber, + severityName, + message, + ruleUrl, + ruleId + } = it; + + return ` + + + + + + +`.trimLeft(); +}; diff --git a/lib/cli-engine/formatters/html-template-page.html b/lib/cli-engine/formatters/html-template-page.js similarity index 92% rename from lib/cli-engine/formatters/html-template-page.html rename to lib/cli-engine/formatters/html-template-page.js index 4016576fa06..e37a71e2d95 100644 --- a/lib/cli-engine/formatters/html-template-page.html +++ b/lib/cli-engine/formatters/html-template-page.js @@ -1,3 +1,9 @@ +"use strict"; + +module.exports = function(it) { + const { reportColor, reportSummary, date, results } = it; + + return ` @@ -88,15 +94,15 @@ -
+

ESLint Report

- <%= reportSummary %> - Generated on <%= date %> + ${reportSummary} - Generated on ${date}
+ +
+Toru Nagashima +
+

薛定谔的猫 From 086c1d6e8593cf8e7851daa8f2a890c213cf6999 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Thu, 13 May 2021 14:38:25 +0530 Subject: [PATCH 07/67] Chore: add more test cases for `no-sequences` (#14579) --- tests/lib/rules/no-sequences.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/lib/rules/no-sequences.js b/tests/lib/rules/no-sequences.js index f9eb8a6a30f..1ac7c4a6c6e 100644 --- a/tests/lib/rules/no-sequences.js +++ b/tests/lib/rules/no-sequences.js @@ -59,7 +59,11 @@ ruleTester.run("no-sequences", rule, { // valid code with "allowInParentheses" set to `false` { code: "for ((i = 0, j = 0); test; );", options: [{ allowInParentheses: false }] }, - { code: "for (; test; (i++, j++));", options: [{ allowInParentheses: false }] } + { code: "for (; test; (i++, j++));", options: [{ allowInParentheses: false }] }, + + // https://github.com/eslint/eslint/issues/14572 + { code: "const foo = () => { return ((bar = 123), 10) }", env: { es6: true } }, + { code: "const foo = () => (((bar = 123), 10));", env: { es6: true } } ], // Examples of code that should trigger the rule From fe29f18227fd02fd7c3da033417d621275b00d0a Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 14 May 2021 21:11:51 -0400 Subject: [PATCH 08/67] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a4f4a04ddec..1cb56a9bcc8 100644 --- a/README.md +++ b/README.md @@ -286,7 +286,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Platinum Sponsors

Automattic

Gold Sponsors

-

Nx (by Nrwl) Chrome's Web Framework & Tools Performance Fund Salesforce Airbnb Microsoft Substack

Silver Sponsors

+

Nx (by Nrwl) Chrome's Web Framework & Tools Performance Fund Salesforce Airbnb Substack

Silver Sponsors

Retool Liftoff

Bronze Sponsors

Buy.Fineproxy.Org Anagram Solver Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Fire Stick Tricks

From 2466a05160de60958457d984b79fd445c12ebc98 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Sun, 16 May 2021 10:11:47 -0400 Subject: [PATCH 09/67] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1cb56a9bcc8..544da51ebcd 100644 --- a/README.md +++ b/README.md @@ -288,7 +288,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Automattic

Gold Sponsors

Nx (by Nrwl) Chrome's Web Framework & Tools Performance Fund Salesforce Airbnb Substack

Silver Sponsors

Retool Liftoff

Bronze Sponsors

-

Buy.Fineproxy.Org Anagram Solver Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Fire Stick Tricks

+

Anagram Solver Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Fire Stick Tricks

## Technology Sponsors From 967b1c4ceca8f5248378477da94ff118dafaa647 Mon Sep 17 00:00:00 2001 From: Ikko Ashimine Date: Fri, 21 May 2021 23:53:49 +0900 Subject: [PATCH 10/67] Chore: Fix typo in large.js (#14589) wether -> whether --- tests/bench/large.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/bench/large.js b/tests/bench/large.js index c94e88568d4..a3db1ac6c8d 100644 --- a/tests/bench/large.js +++ b/tests/bench/large.js @@ -57078,7 +57078,7 @@ var JSHINT = (function () { FutureReservedWord("transient"); FutureReservedWord("volatile"); - // this function is used to determine wether a squarebracket or a curlybracket + // this function is used to determine whether a squarebracket or a curlybracket // expression is a comprehension array, destructuring assignment or a json value. var lookupBlockType = function () { @@ -60568,4 +60568,4 @@ exports.yui = { },{}]},{},["nr+AlQ"]) JSHINT = require('jshint').JSHINT; if (typeof exports === 'object' && exports) exports.JSHINT = JSHINT; -}()); \ No newline at end of file +}()); From 52655dd54925ee02af2ba3a0ebc09de959ae3101 Mon Sep 17 00:00:00 2001 From: Alex Holden Date: Fri, 21 May 2021 16:00:20 -0400 Subject: [PATCH 11/67] Update: no-restricted-imports custom message for patterns (fixes #11843) (#14580) * Update: no-restricted-imports rule with custom messages (fixes #11843) * Update docs/rules/no-restricted-imports.md Co-authored-by: Milos Djermanovic * Update docs/rules/no-restricted-imports.md Co-authored-by: Milos Djermanovic * Code review feedback - better test cases and schema change * Doc updates * Added correct/incorrect examples to docs Co-authored-by: Milos Djermanovic --- docs/rules/no-restricted-imports.md | 34 +++++++++- lib/rules/no-restricted-imports.js | 85 +++++++++++++++++------- tests/lib/rules/no-restricted-imports.js | 34 ++++++++++ 3 files changed, 128 insertions(+), 25 deletions(-) diff --git a/docs/rules/no-restricted-imports.md b/docs/rules/no-restricted-imports.md index 0f6841340c9..f1577d7d89d 100644 --- a/docs/rules/no-restricted-imports.md +++ b/docs/rules/no-restricted-imports.md @@ -75,7 +75,21 @@ or like this if you need to restrict only certain imports from a module: }] ``` -The custom message will be appended to the default error message. Please note that you may not specify custom error messages for restricted patterns as a particular import may match more than one pattern. +or like this if you want to apply a custom message to pattern matches: + +```json +"no-restricted-imports": ["error", { + "patterns": [{ + "group": ["import1/private/*"], + "message": "usage of import1 private modules not allowed." + }, { + "group": ["import2/*", "!import2/good"], + "message": "import2 is deprecated, except the modules in import2/good." + }] +}] +``` + +The custom message will be appended to the default error message. To restrict the use of all Node.js core imports (via https://github.com/nodejs/node/tree/master/lib): @@ -149,6 +163,15 @@ import { DisallowedObject as AllowedObject } from "foo"; import * as Foo from "foo"; ``` +```js +/*eslint no-restricted-imports: ["error", { patterns: [{ + group: ["lodash/*"], + message: "Please use the default import from 'lodash' instead." +}]}]*/ + +import pick from 'lodash/pick'; +``` + Examples of **correct** code for this rule: ```js @@ -182,6 +205,15 @@ import DisallowedObject from "foo" import { AllowedObject as DisallowedObject } from "foo"; ``` +```js +/*eslint no-restricted-imports: ["error", { patterns: [{ + group: ["lodash/*"], + message: "Please use the default import from 'lodash' instead." +}]}]*/ + +import lodash from 'lodash'; +``` + ## 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 c205dad8bdb..414164d29f7 100644 --- a/lib/rules/no-restricted-imports.js +++ b/lib/rules/no-restricted-imports.js @@ -10,12 +10,6 @@ const ignore = require("ignore"); -const arrayOfStrings = { - type: "array", - items: { type: "string" }, - uniqueItems: true -}; - const arrayOfStringsOrObjects = { type: "array", items: { @@ -44,6 +38,41 @@ const arrayOfStringsOrObjects = { uniqueItems: true }; +const arrayOfStringsOrObjectPatterns = { + anyOf: [ + { + type: "array", + items: { + type: "string" + }, + uniqueItems: true + }, + { + type: "array", + items: { + type: "object", + properties: { + group: { + type: "array", + items: { + type: "string" + }, + minItems: 1, + uniqueItems: true + }, + message: { + type: "string", + minLength: 1 + } + }, + additionalProperties: false, + required: ["group"] + }, + uniqueItems: true + } + ] +}; + module.exports = { meta: { type: "suggestion", @@ -61,6 +90,8 @@ module.exports = { pathWithCustomMessage: "'{{importSource}}' import is restricted from being used. {{customMessage}}", patterns: "'{{importSource}}' import is restricted from being used by a pattern.", + // eslint-disable-next-line eslint-plugin/report-message-format + patternWithCustomMessage: "'{{importSource}}' import is restricted from being used by a pattern. {{customMessage}}", everything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.", // eslint-disable-next-line eslint-plugin/report-message-format @@ -80,7 +111,7 @@ module.exports = { type: "object", properties: { paths: arrayOfStringsOrObjects, - patterns: arrayOfStrings + patterns: arrayOfStringsOrObjectPatterns }, additionalProperties: false }], @@ -98,13 +129,6 @@ module.exports = { (Object.prototype.hasOwnProperty.call(options[0], "paths") || Object.prototype.hasOwnProperty.call(options[0], "patterns")); const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || []; - const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || []; - - // if no imports are restricted we don"t need to check - if (Object.keys(restrictedPaths).length === 0 && restrictedPatterns.length === 0) { - return {}; - } - const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => { if (typeof importSource === "string") { memo[importSource] = { message: null }; @@ -117,7 +141,16 @@ module.exports = { return memo; }, {}); - const restrictedPatternsMatcher = ignore().add(restrictedPatterns); + // Handle patterns too, either as strings or groups + const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || []; + const restrictedPatternGroups = restrictedPatterns.length > 0 && typeof restrictedPatterns[0] === "string" + ? [{ matcher: ignore().add(restrictedPatterns) }] + : restrictedPatterns.map(({ group, message }) => ({ matcher: ignore().add(group), customMessage: message })); + + // if no imports are restricted we don"t need to check + if (Object.keys(restrictedPaths).length === 0 && restrictedPatternGroups.length === 0) { + return {}; + } /** * Report a restricted path. @@ -184,17 +217,19 @@ module.exports = { /** * Report a restricted path specifically for patterns. * @param {node} node representing the restricted path reference + * @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails * @returns {void} * @private */ - function reportPathForPatterns(node) { + function reportPathForPatterns(node, group) { const importSource = node.source.value.trim(); context.report({ node, - messageId: "patterns", + messageId: group.customMessage ? "patternWithCustomMessage" : "patterns", data: { - importSource + importSource, + customMessage: group.customMessage } }); } @@ -202,11 +237,12 @@ module.exports = { /** * Check if the given importSource is restricted by a pattern. * @param {string} importSource path of the import + * @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails * @returns {boolean} whether the variable is a restricted pattern or not * @private */ - function isRestrictedPattern(importSource) { - return restrictedPatterns.length > 0 && restrictedPatternsMatcher.ignores(importSource); + function isRestrictedPattern(importSource, group) { + return group.matcher.ignores(importSource); } /** @@ -249,10 +285,11 @@ module.exports = { } checkRestrictedPathAndReport(importSource, importNames, node); - - if (isRestrictedPattern(importSource)) { - reportPathForPatterns(node); - } + restrictedPatternGroups.forEach(group => { + if (isRestrictedPattern(importSource, group)) { + reportPathForPatterns(node, group); + } + }); } return { diff --git a/tests/lib/rules/no-restricted-imports.js b/tests/lib/rules/no-restricted-imports.js index 363229712c9..f54d4df0354 100644 --- a/tests/lib/rules/no-restricted-imports.js +++ b/tests/lib/rules/no-restricted-imports.js @@ -37,6 +37,10 @@ ruleTester.run("no-restricted-imports", rule, { code: "import withGitignores from \"foo/bar\";", options: [{ patterns: ["foo/*", "!foo/bar"] }] }, + { + code: "import withPatterns from \"foo/bar\";", + options: [{ patterns: [{ group: ["foo/*", "!foo/bar"], message: "foo is forbidden, use bar instead" }] }] + }, { code: "import AllowedObject from \"foo\";", options: [{ @@ -241,6 +245,36 @@ ruleTester.run("no-restricted-imports", rule, { column: 1, endColumn: 36 }] + }, { + code: "import withPatterns from \"foo/baz\";", + options: [{ patterns: [{ group: ["foo/*", "!foo/bar"], message: "foo is forbidden, use foo/bar instead" }] }], + errors: [{ + message: "'foo/baz' import is restricted from being used by a pattern. foo is forbidden, use foo/bar instead", + type: "ImportDeclaration", + line: 1, + column: 1, + endColumn: 36 + }] + }, { + code: "import withPatterns from \"foo/baz\";", + options: [{ patterns: [{ group: ["foo/bar", "foo/baz"], message: "some foo subimports are restricted" }] }], + errors: [{ + message: "'foo/baz' import is restricted from being used by a pattern. some foo subimports are restricted", + type: "ImportDeclaration", + line: 1, + column: 1, + endColumn: 36 + }] + }, { + code: "import withPatterns from \"foo/bar\";", + options: [{ patterns: [{ group: ["foo/bar"] }] }], + errors: [{ + message: "'foo/bar' import is restricted from being used by a pattern.", + type: "ImportDeclaration", + line: 1, + column: 1, + endColumn: 36 + }] }, { code: "import withGitignores from \"foo/bar\";", options: [{ patterns: ["foo/*", "!foo/baz"] }], From c0f418e2476df98519bc156b81d20431984e8704 Mon Sep 17 00:00:00 2001 From: Stephen Wade Date: Fri, 21 May 2021 16:24:19 -0400 Subject: [PATCH 12/67] Chore: Remove lodash (#14287) * Chore: Update table to ^6.0.9 * Chore: Remove lodash.last lodash.last(array) -> array[array.length - 1] * Chore: Remove lodash.get v = lodash.get(a, "b.c") -> if (a && a.b && a.b.c) v = a.b.c * Chore: Remove lodash.noop lodash.noop -> () => {} * Chore: Remove lodash.union https://exploringjs.com/impatient-js/ch_sets.html#union-a-b * Chore: Remove lodash.intersection https://exploringjs.com/impatient-js/ch_sets.html#intersection-a-b * Chore: Remove lodash.findLast lodash.findLast(array) -> [...array].reverse().find(_ =>_) * Chore: Remove lodash.overSome * Chore: Remove lodash.isPlainObject * Chore: Remove lodash.isString lodash.isString(str) -> typeof str === "string"; * Chore: Remove lodash.range * Chore: Remove lodash.sortedLastIndex https://www.30secondsofcode.org/js/s/sorted-last-index * Chore: Remove lodash.sortedIndexBy https://www.30secondsofcode.org/js/s/sorted-index-by * Chore: Remove lodash.sample https://www.30secondsofcode.org/js/s/sample * Chore: Remove lodash.flatMap * Chore: Remove lodash.flatten * Chore: Remove lodash.template * Chore: Remove lodash.escapeRegExp Add the escape-string-regexp package * Chore: Remove lodash.isEqual Add the fast-deep-equal package * Chore: Remove lodash.merge Add the deep-extend package * Chore: Remove lodash.cloneDeep Add the clone package * Chore: Remove lodash.omit Add the omit package * Chore: Remove lodash.upperFirst Add the upper-case-first package * Chore: Remove lodash.memoize Add the fast-memoize package * Chore: Remove lodash.mapValues Add the map-values package * Chore: Remove lodash.flatten * Chore: Remove lodash * Chore: Replace arrays.flat() * Chore: Replace clone with rfdc * Chore: Add comment about map-values * Chore: Remove omit dependency * Chore: Remove rfdc dependency * Chore: Remove upper-case-first dependency * Chore: Remove fast-memoize dependency * Chore: Apply suggestions in lib/linter/node-event-generator.js Co-authored-by: Milos Djermanovic * Chore: Add tests for upperCaseFirst * Chore: Remove map-values dependency * Chore: Apply review suggestions * Chore: Upgrade deep-extend to ^0.6.0 * Chore: Replace deep-extend dependency with lodash.merge * Chore: Apply review suggestion * Chore: Simplify search code * Chore: Apply review suggestions Co-authored-by: Milos Djermanovic --- bin/eslint.js | 14 +----- docs/developer-guide/code-path-analysis.md | 12 ++--- .../code-path-analysis/README.md | 12 ++--- lib/cli-engine/file-enumerator.js | 2 +- .../formatters/html-template-message.html | 8 --- .../formatters/html-template-message.js | 25 ++++++++++ ...mplate-page.html => html-template-page.js} | 14 ++++-- .../formatters/html-template-result.html | 6 --- .../formatters/html-template-result.js | 14 ++++++ lib/cli-engine/formatters/html.js | 36 +++++++++----- lib/init/autoconfig.js | 4 +- lib/linter/apply-disable-directives.js | 18 +++++-- lib/linter/linter.js | 10 ++-- lib/linter/node-event-generator.js | 49 ++++++++++++++++--- lib/rule-tester/rule-tester.js | 24 +++++---- lib/rules/comma-dangle.js | 23 ++++++--- lib/rules/complexity.js | 5 +- lib/rules/consistent-return.js | 4 +- lib/rules/eol-last.js | 9 +--- lib/rules/indent.js | 17 +++---- lib/rules/max-lines-per-function.js | 5 +- lib/rules/max-lines.js | 39 ++++++++++++--- lib/rules/max-params.js | 5 +- lib/rules/max-statements.js | 5 +- lib/rules/no-fallthrough.js | 10 +--- lib/rules/no-useless-backreference.js | 3 +- lib/rules/no-useless-computed-key.js | 10 +++- lib/rules/no-warning-comments.js | 2 +- lib/rules/object-curly-newline.js | 23 +++++++-- lib/rules/spaced-comment.js | 4 +- lib/rules/utils/ast-utils.js | 4 +- lib/shared/deprecation-warnings.js | 15 ++++-- lib/shared/string-utils.js | 22 +++++++++ lib/source-code/source-code.js | 11 +++-- lib/source-code/token-store/utils.js | 16 ++---- ...files-ignored.txt => all-files-ignored.js} | 12 ++++- messages/extend-config-missing.js | 13 +++++ messages/extend-config-missing.txt | 5 -- messages/failed-to-read-json.js | 11 +++++ messages/failed-to-read-json.txt | 3 -- messages/file-not-found.js | 10 ++++ messages/file-not-found.txt | 2 - ...no-config-found.txt => no-config-found.js} | 10 +++- messages/plugin-conflict.js | 22 +++++++++ messages/plugin-conflict.txt | 7 --- messages/plugin-invalid.js | 16 ++++++ messages/plugin-invalid.txt | 8 --- messages/plugin-missing.js | 19 +++++++ messages/plugin-missing.txt | 11 ----- ...xt => print-config-with-directory-path.js} | 6 +++ messages/whitespace-found.js | 11 +++++ messages/whitespace-found.txt | 3 -- package.json | 7 +-- tests/lib/rule-tester/rule-tester.js | 12 ++++- tests/lib/rules/no-invalid-this.js | 10 ++-- tests/lib/shared/string-utils.js | 41 ++++++++++++++++ tools/eslint-fuzzer.js | 24 +++++++-- webpack.config.js | 3 +- 58 files changed, 526 insertions(+), 220 deletions(-) delete mode 100644 lib/cli-engine/formatters/html-template-message.html create mode 100644 lib/cli-engine/formatters/html-template-message.js rename lib/cli-engine/formatters/{html-template-page.html => html-template-page.js} (92%) delete mode 100644 lib/cli-engine/formatters/html-template-result.html create mode 100644 lib/cli-engine/formatters/html-template-result.js create mode 100644 lib/shared/string-utils.js rename messages/{all-files-ignored.txt => all-files-ignored.js} (51%) create mode 100644 messages/extend-config-missing.js delete mode 100644 messages/extend-config-missing.txt create mode 100644 messages/failed-to-read-json.js delete mode 100644 messages/failed-to-read-json.txt create mode 100644 messages/file-not-found.js delete mode 100644 messages/file-not-found.txt rename messages/{no-config-found.txt => no-config-found.js} (52%) create mode 100644 messages/plugin-conflict.js delete mode 100644 messages/plugin-conflict.txt create mode 100644 messages/plugin-invalid.js delete mode 100644 messages/plugin-invalid.txt create mode 100644 messages/plugin-missing.js delete mode 100644 messages/plugin-missing.txt rename messages/{print-config-with-directory-path.txt => print-config-with-directory-path.js} (70%) create mode 100644 messages/whitespace-found.js delete mode 100644 messages/whitespace-found.txt create mode 100644 tests/lib/shared/string-utils.js diff --git a/bin/eslint.js b/bin/eslint.js index 86291b0f527..5fa5766828e 100755 --- a/bin/eslint.js +++ b/bin/eslint.js @@ -66,11 +66,8 @@ function readStdin() { */ function getErrorMessage(error) { - // Lazy loading because those are used only if error happened. - const fs = require("fs"); - const path = require("path"); + // Lazy loading because this is used only if an error happened. const util = require("util"); - const lodash = require("lodash"); // Foolproof -- thirdparty module might throw non-object. if (typeof error !== "object" || error === null) { @@ -80,14 +77,7 @@ function getErrorMessage(error) { // Use templates if `error.messageTemplate` is present. if (typeof error.messageTemplate === "string") { try { - const templateFilePath = path.resolve( - __dirname, - `../messages/${error.messageTemplate}.txt` - ); - - // Use sync API because Node.js should exit at this tick. - const templateText = fs.readFileSync(templateFilePath, "utf-8"); - const template = lodash.template(templateText); + const template = require(`../messages/${error.messageTemplate}.js`); return template(error.messageData || {}); } catch { diff --git a/docs/developer-guide/code-path-analysis.md b/docs/developer-guide/code-path-analysis.md index 7762bc5d652..cd3fc29365a 100644 --- a/docs/developer-guide/code-path-analysis.md +++ b/docs/developer-guide/code-path-analysis.md @@ -195,8 +195,6 @@ bar(); ### To check whether or not this is reachable ```js -var last = require("lodash").last; - function isReachable(segment) { return segment.reachable; } @@ -215,7 +213,7 @@ module.exports = function(context) { // Checks reachable or not. "ExpressionStatement": function(node) { - var codePath = last(codePathStack); + var codePath = codePathStack[codePathStack.length - 1]; // Checks the current code path segments. if (!codePath.currentSegments.some(isReachable)) { @@ -239,8 +237,6 @@ So a rule must not modify those instances. Please use a map of information instead. ```js -var last = require("lodash").last; - function hasCb(node, context) { if (node.type.indexOf("Function") !== -1) { return context.getDeclaredVariables(node).some(function(v) { @@ -285,8 +281,10 @@ module.exports = function(context) { // Manages state of code paths. "onCodePathSegmentStart": function(segment) { + var funcInfo = funcInfoStack[funcInfoStack.length - 1]; + // Ignores if `cb` doesn't exist. - if (!last(funcInfoStack).hasCb) { + if (!funcInfo.hasCb) { return; } @@ -304,7 +302,7 @@ module.exports = function(context) { // Checks reachable or not. "CallExpression": function(node) { - var funcInfo = last(funcInfoStack); + var funcInfo = funcInfoStack[funcInfoStack.length - 1]; // Ignores if `cb` doesn't exist. if (!funcInfo.hasCb) { diff --git a/docs/developer-guide/code-path-analysis/README.md b/docs/developer-guide/code-path-analysis/README.md index c283d51bf91..1c84b2e1f73 100644 --- a/docs/developer-guide/code-path-analysis/README.md +++ b/docs/developer-guide/code-path-analysis/README.md @@ -195,8 +195,6 @@ bar(); ### To check whether or not this is reachable ```js -var last = require("lodash").last; - function isReachable(segment) { return segment.reachable; } @@ -215,7 +213,7 @@ module.exports = function(context) { // Checks reachable or not. "ExpressionStatement": function(node) { - var codePath = last(codePathStack); + var codePath = codePathStack[codePathStack.length - 1]; // Checks the current code path segments. if (!codePath.currentSegments.some(isReachable)) { @@ -239,8 +237,6 @@ So a rule must not modify those instances. Please use a map of information instead. ```js -var last = require("lodash").last; - function hasCb(node, context) { if (node.type.indexOf("Function") !== -1) { return context.getDeclaredVariables(node).some(function(v) { @@ -285,8 +281,10 @@ module.exports = function(context) { // Manages state of code paths. "onCodePathSegmentStart": function(segment) { + var funcInfo = funcInfoStack[funcInfoStack - 1]; + // Ignores if `cb` doesn't exist. - if (!last(funcInfoStack).hasCb) { + if (!funcInfo.hasCb) { return; } @@ -304,7 +302,7 @@ module.exports = function(context) { // Checks reachable or not. "CallExpression": function(node) { - var funcInfo = last(funcInfoStack); + var funcInfo = funcInfoStack[funcInfoStack - 1]; // Ignores if `cb` doesn't exist. if (!funcInfo.hasCb) { diff --git a/lib/cli-engine/file-enumerator.js b/lib/cli-engine/file-enumerator.js index bd89ec7334c..ade28517b42 100644 --- a/lib/cli-engine/file-enumerator.js +++ b/lib/cli-engine/file-enumerator.js @@ -38,7 +38,7 @@ const fs = require("fs"); const path = require("path"); const getGlobParent = require("glob-parent"); const isGlob = require("is-glob"); -const { escapeRegExp } = require("lodash"); +const escapeRegExp = require("escape-string-regexp"); const { Minimatch } = require("minimatch"); const { diff --git a/lib/cli-engine/formatters/html-template-message.html b/lib/cli-engine/formatters/html-template-message.html deleted file mode 100644 index 93795a1bdc8..00000000000 --- a/lib/cli-engine/formatters/html-template-message.html +++ /dev/null @@ -1,8 +0,0 @@ -
- <%= results %> + ${results}
+`.trimLeft(); +}; diff --git a/lib/cli-engine/formatters/html-template-result.html b/lib/cli-engine/formatters/html-template-result.html deleted file mode 100644 index f4a55933c20..00000000000 --- a/lib/cli-engine/formatters/html-template-result.html +++ /dev/null @@ -1,6 +0,0 @@ - - - [+] <%- filePath %> - <%- summary %> - - diff --git a/lib/cli-engine/formatters/html-template-result.js b/lib/cli-engine/formatters/html-template-result.js new file mode 100644 index 00000000000..5048f72e928 --- /dev/null +++ b/lib/cli-engine/formatters/html-template-result.js @@ -0,0 +1,14 @@ +"use strict"; + +module.exports = function(it, encodeHTML) { + const { color, index, filePath, summary } = it; + + return ` + + + [+] ${encodeHTML(filePath)} + ${encodeHTML(summary)} + + +`.trimLeft(); +}; diff --git a/lib/cli-engine/formatters/html.js b/lib/cli-engine/formatters/html.js index 5d4b7e56060..b9739f05e2d 100644 --- a/lib/cli-engine/formatters/html.js +++ b/lib/cli-engine/formatters/html.js @@ -4,17 +4,30 @@ */ "use strict"; -const lodash = require("lodash"); -const fs = require("fs"); -const path = require("path"); - //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ -const pageTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-page.html"), "utf-8")); -const messageTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-message.html"), "utf-8")); -const resultTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-result.html"), "utf-8")); +const encodeHTML = (function() { + const encodeHTMLRules = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'" + }; + const matchHTML = /[&<>"']/ug; + + return function(code) { + return code + ? code.toString().replace(matchHTML, m => encodeHTMLRules[m] || m) + : ""; + }; +}()); + +const pageTemplate = require("./html-template-page.js"); +const messageTemplate = require("./html-template-message.js"); +const resultTemplate = require("./html-template-result.js"); /** * Given a word and a count, append an s if count is not one. @@ -80,7 +93,9 @@ function renderMessages(messages, parentIndex, rulesMeta) { if (rulesMeta) { const meta = rulesMeta[message.ruleId]; - ruleUrl = lodash.get(meta, "docs.url", null); + if (meta && meta.docs && meta.docs.url) { + ruleUrl = meta.docs.url; + } } return messageTemplate({ @@ -92,7 +107,7 @@ function renderMessages(messages, parentIndex, rulesMeta) { message: message.message, ruleId: message.ruleId, ruleUrl - }); + }, encodeHTML); }).join("\n"); } @@ -108,8 +123,7 @@ function renderResults(results, rulesMeta) { color: renderColor(result.errorCount, result.warningCount), filePath: result.filePath, summary: renderSummary(result.errorCount, result.warningCount) - - }) + renderMessages(result.messages, index, rulesMeta)).join("\n"); + }, encodeHTML) + renderMessages(result.messages, index, rulesMeta)).join("\n"); } //------------------------------------------------------------------------------ diff --git a/lib/init/autoconfig.js b/lib/init/autoconfig.js index 19d016a54c1..054c538496f 100644 --- a/lib/init/autoconfig.js +++ b/lib/init/autoconfig.js @@ -9,7 +9,7 @@ // Requirements //------------------------------------------------------------------------------ -const lodash = require("lodash"), +const equal = require("fast-deep-equal"), recConfig = require("../../conf/eslint-recommended"), ConfigOps = require("@eslint/eslintrc/lib/shared/config-ops"), { Linter } = require("../linter"), @@ -329,7 +329,7 @@ function extendFromRecommended(config) { const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId])); recRules.forEach(ruleId => { - if (lodash.isEqual(recConfig.rules[ruleId], newConfig.rules[ruleId])) { + if (equal(recConfig.rules[ruleId], newConfig.rules[ruleId])) { delete newConfig.rules[ruleId]; } }); diff --git a/lib/linter/apply-disable-directives.js b/lib/linter/apply-disable-directives.js index 41d6934abba..0ba69ca9cc4 100644 --- a/lib/linter/apply-disable-directives.js +++ b/lib/linter/apply-disable-directives.js @@ -5,8 +5,6 @@ "use strict"; -const lodash = require("lodash"); - /** * Compares the locations of two objects in a source file * @param {{line: number, column: number}} itemA The first object @@ -124,7 +122,21 @@ module.exports = ({ directives, problems, reportUnusedDisableDirectives = "off" .map(directive => Object.assign({}, directive, { unprocessedDirective: directive })) .sort(compareLocations); - const lineDirectives = lodash.flatMap(directives, directive => { + /** + * Returns a new array formed by applying a given callback function to each element of the array, and then flattening the result by one level. + * TODO(stephenwade): Replace this with array.flatMap when we drop support for Node v10 + * @param {any[]} array The array to process + * @param {Function} fn The function to use + * @returns {any[]} The result array + */ + function flatMap(array, fn) { + const mapped = array.map(fn); + const flattened = [].concat(...mapped); + + return flattened; + } + + const lineDirectives = flatMap(directives, directive => { switch (directive.type) { case "disable": case "enable": diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 576816b5b7b..bdc6c1b1d01 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -15,7 +15,7 @@ const eslintScope = require("eslint-scope"), evk = require("eslint-visitor-keys"), espree = require("espree"), - lodash = require("lodash"), + merge = require("lodash.merge"), BuiltInEnvironments = require("@eslint/eslintrc/conf/environments"), pkg = require("../../package.json"), astUtils = require("../shared/ast-utils"), @@ -529,8 +529,8 @@ function normalizeVerifyOptions(providedOptions, config) { function resolveParserOptions(parserName, providedOptions, enabledEnvironments) { const parserOptionsFromEnv = enabledEnvironments .filter(env => env.parserOptions) - .reduce((parserOptions, env) => lodash.merge(parserOptions, env.parserOptions), {}); - const mergedParserOptions = lodash.merge(parserOptionsFromEnv, providedOptions || {}); + .reduce((parserOptions, env) => merge(parserOptions, env.parserOptions), {}); + const mergedParserOptions = merge(parserOptionsFromEnv, providedOptions || {}); const isModule = mergedParserOptions.sourceType === "module"; if (isModule) { @@ -1286,7 +1286,9 @@ class Linter { const filenameToExpose = normalizeFilename(filename); const text = ensureText(textOrSourceCode); const preprocess = options.preprocess || (rawText => [rawText]); - const postprocess = options.postprocess || lodash.flatten; + + // TODO(stephenwade): Replace this with array.flat() when we drop support for Node v10 + const postprocess = options.postprocess || (array => [].concat(...array)); const filterCodeBlock = options.filterCodeBlock || (blockFilename => blockFilename.endsWith(".js")); diff --git a/lib/linter/node-event-generator.js b/lib/linter/node-event-generator.js index 0b4e50fc4b7..8b619fdff83 100644 --- a/lib/linter/node-event-generator.js +++ b/lib/linter/node-event-generator.js @@ -10,7 +10,6 @@ //------------------------------------------------------------------------------ const esquery = require("esquery"); -const lodash = require("lodash"); //------------------------------------------------------------------------------ // Typedefs @@ -32,6 +31,35 @@ const lodash = require("lodash"); // Helpers //------------------------------------------------------------------------------ +/** + * Computes the union of one or more arrays + * @param {...any[]} arrays One or more arrays to union + * @returns {any[]} The union of the input arrays + */ +function union(...arrays) { + + // TODO(stephenwade): Replace this with arrays.flat() when we drop support for Node v10 + return [...new Set([].concat(...arrays))]; +} + +/** + * Computes the intersection of one or more arrays + * @param {...any[]} arrays One or more arrays to intersect + * @returns {any[]} The intersection of the input arrays + */ +function intersection(...arrays) { + if (arrays.length === 0) { + return []; + } + + let result = [...new Set(arrays[0])]; + + for (const array of arrays.slice(1)) { + result = result.filter(x => array.includes(x)); + } + return result; +} + /** * Gets the possible types of a selector * @param {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector @@ -46,7 +74,7 @@ function getPossibleTypes(parsedSelector) { const typesForComponents = parsedSelector.selectors.map(getPossibleTypes); if (typesForComponents.every(Boolean)) { - return lodash.union(...typesForComponents); + return union(...typesForComponents); } return null; } @@ -63,7 +91,7 @@ function getPossibleTypes(parsedSelector) { * If at least one of the components could only match a particular type, the compound could only match * the intersection of those types. */ - return lodash.intersection(...typesForComponents); + return intersection(...typesForComponents); } case "child": @@ -166,15 +194,21 @@ function tryParseSelector(rawSelector) { } } +const selectorCache = new Map(); + /** * Parses a raw selector string, and returns the parsed selector along with specificity and type information. * @param {string} rawSelector A raw AST selector * @returns {ASTSelector} A selector descriptor */ -const parseSelector = lodash.memoize(rawSelector => { +function parseSelector(rawSelector) { + if (selectorCache.has(rawSelector)) { + return selectorCache.get(rawSelector); + } + const parsedSelector = tryParseSelector(rawSelector); - return { + const result = { rawSelector, isExit: rawSelector.endsWith(":exit"), parsedSelector, @@ -182,7 +216,10 @@ const parseSelector = lodash.memoize(rawSelector => { attributeCount: countClassAttributes(parsedSelector), identifierCount: countIdentifiers(parsedSelector) }; -}); + + selectorCache.set(rawSelector, result); + return result; +} //------------------------------------------------------------------------------ // Public Interface diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 23b78fba279..b08303c62b2 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -44,7 +44,8 @@ const assert = require("assert"), path = require("path"), util = require("util"), - lodash = require("lodash"), + merge = require("lodash.merge"), + equal = require("fast-deep-equal"), Traverser = require("../../lib/shared/traverser"), { getRuleOptionsSchema, validate } = require("../shared/config-validator"), { Linter, SourceCodeFixer, interpolate } = require("../linter"); @@ -324,10 +325,9 @@ class RuleTester { * configuration and the default configuration. * @type {Object} */ - this.testerConfig = lodash.merge( - - // we have to clone because merge uses the first argument for recipient - lodash.cloneDeep(defaultConfig), + this.testerConfig = merge( + {}, + defaultConfig, testerConfig, { rules: { "rule-tester/validate-ast": "error" } } ); @@ -369,7 +369,7 @@ class RuleTester { * @returns {void} */ static resetDefaultConfig() { - defaultConfig = lodash.cloneDeep(testerDefaultConfig); + defaultConfig = merge({}, testerDefaultConfig); } @@ -465,7 +465,7 @@ class RuleTester { * @private */ function runRuleForItem(item) { - let config = lodash.cloneDeep(testerConfig), + let config = merge({}, testerConfig), code, filename, output, beforeAST, afterAST; if (typeof item === "string") { @@ -477,13 +477,17 @@ class RuleTester { * Assumes everything on the item is a config except for the * parameters used by this tester */ - const itemConfig = lodash.omit(item, RuleTesterParameters); + const itemConfig = { ...item }; + + for (const parameter of RuleTesterParameters) { + delete itemConfig[parameter]; + } /* * Create the config object from the tester config and this item * specific configurations. */ - config = lodash.merge( + config = merge( config, itemConfig ); @@ -589,7 +593,7 @@ class RuleTester { * @private */ function assertASTDidntChange(beforeAST, afterAST) { - if (!lodash.isEqual(beforeAST, afterAST)) { + if (!equal(beforeAST, afterAST)) { assert.fail("Rule should not modify AST."); } } diff --git a/lib/rules/comma-dangle.js b/lib/rules/comma-dangle.js index e22b7f3551e..798c111ec3b 100644 --- a/lib/rules/comma-dangle.js +++ b/lib/rules/comma-dangle.js @@ -9,7 +9,6 @@ // Requirements //------------------------------------------------------------------------------ -const lodash = require("lodash"); const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ @@ -144,23 +143,33 @@ module.exports = { * @returns {ASTNode|null} The last node or null. */ function getLastItem(node) { + + /** + * Returns the last element of an array + * @param {any[]} array The input array + * @returns {any} The last element + */ + function last(array) { + return array[array.length - 1]; + } + switch (node.type) { case "ObjectExpression": case "ObjectPattern": - return lodash.last(node.properties); + return last(node.properties); case "ArrayExpression": case "ArrayPattern": - return lodash.last(node.elements); + return last(node.elements); case "ImportDeclaration": case "ExportNamedDeclaration": - return lodash.last(node.specifiers); + return last(node.specifiers); case "FunctionDeclaration": case "FunctionExpression": case "ArrowFunctionExpression": - return lodash.last(node.params); + return last(node.params); case "CallExpression": case "NewExpression": - return lodash.last(node.arguments); + return last(node.arguments); default: return null; } @@ -316,7 +325,7 @@ module.exports = { "always-multiline": forceTrailingCommaIfMultiline, "only-multiline": allowTrailingCommaIfMultiline, never: forbidTrailingComma, - ignore: lodash.noop + ignore: () => {} }; return { diff --git a/lib/rules/complexity.js b/lib/rules/complexity.js index 5d62c6ff44b..116c8ad0a63 100644 --- a/lib/rules/complexity.js +++ b/lib/rules/complexity.js @@ -10,9 +10,8 @@ // Requirements //------------------------------------------------------------------------------ -const lodash = require("lodash"); - const astUtils = require("./utils/ast-utils"); +const { upperCaseFirst } = require("../shared/string-utils"); //------------------------------------------------------------------------------ // Rule Definition @@ -95,7 +94,7 @@ module.exports = { * @private */ function endFunction(node) { - const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(node)); + const name = upperCaseFirst(astUtils.getFunctionNameWithKind(node)); const complexity = fns.pop(); if (complexity > THRESHOLD) { diff --git a/lib/rules/consistent-return.js b/lib/rules/consistent-return.js index 94db253d25b..a250430cb76 100644 --- a/lib/rules/consistent-return.js +++ b/lib/rules/consistent-return.js @@ -8,8 +8,8 @@ // Requirements //------------------------------------------------------------------------------ -const lodash = require("lodash"); const astUtils = require("./utils/ast-utils"); +const { upperCaseFirst } = require("../shared/string-utils"); //------------------------------------------------------------------------------ // Helpers @@ -164,7 +164,7 @@ module.exports = { funcInfo.data = { name: funcInfo.node.type === "Program" ? "Program" - : lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node)) + : upperCaseFirst(astUtils.getFunctionNameWithKind(funcInfo.node)) }; } else if (funcInfo.hasReturnValue !== hasReturnValue) { context.report({ diff --git a/lib/rules/eol-last.js b/lib/rules/eol-last.js index 89c76acb202..24b0c9279c7 100644 --- a/lib/rules/eol-last.js +++ b/lib/rules/eol-last.js @@ -4,12 +4,6 @@ */ "use strict"; -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const lodash = require("lodash"); - //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -48,8 +42,9 @@ module.exports = { Program: function checkBadEOF(node) { const sourceCode = context.getSourceCode(), src = sourceCode.getText(), + lastLine = sourceCode.lines[sourceCode.lines.length - 1], location = { - column: lodash.last(sourceCode.lines).length, + column: lastLine.length, line: sourceCode.lines.length }, LF = "\n", diff --git a/lib/rules/indent.js b/lib/rules/indent.js index 8f4079d31f9..b1af2a73b33 100644 --- a/lib/rules/indent.js +++ b/lib/rules/indent.js @@ -12,10 +12,10 @@ // Requirements //------------------------------------------------------------------------------ -const lodash = require("lodash"); -const astUtils = require("./utils/ast-utils"); const createTree = require("functional-red-black-tree"); +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -1068,7 +1068,7 @@ module.exports = { const baseOffsetListeners = { "ArrayExpression, ArrayPattern"(node) { const openingBracket = sourceCode.getFirstToken(node); - const closingBracket = sourceCode.getTokenAfter(lodash.findLast(node.elements) || openingBracket, astUtils.isClosingBracketToken); + const closingBracket = sourceCode.getTokenAfter([...node.elements].reverse().find(_ => _) || openingBracket, astUtils.isClosingBracketToken); addElementListIndent(node.elements, openingBracket, closingBracket, options.ArrayExpression); }, @@ -1560,8 +1560,9 @@ module.exports = { * 2. Don't set any offsets against the first token of the node. * 3. Call `ignoreNode` on the node sometime after exiting it and before validating offsets. */ - const offsetListeners = lodash.mapValues( - baseOffsetListeners, + const offsetListeners = {}; + + for (const [selector, listener] of Object.entries(baseOffsetListeners)) { /* * Offset listener calls are deferred until traversal is finished, and are called as @@ -1579,10 +1580,8 @@ module.exports = { * To avoid this, the `Identifier` listener isn't called until traversal finishes and all * ignored nodes are known. */ - listener => - node => - listenerCallQueue.push({ listener, node }) - ); + offsetListeners[selector] = node => listenerCallQueue.push({ listener, node }); + } // For each ignored node selector, set up a listener to collect it into the `ignoredNodes` set. const ignoredNodes = new Set(); diff --git a/lib/rules/max-lines-per-function.js b/lib/rules/max-lines-per-function.js index de70cecb47a..60e2e879f54 100644 --- a/lib/rules/max-lines-per-function.js +++ b/lib/rules/max-lines-per-function.js @@ -9,8 +9,7 @@ //------------------------------------------------------------------------------ const astUtils = require("./utils/ast-utils"); - -const lodash = require("lodash"); +const { upperCaseFirst } = require("../shared/string-utils"); //------------------------------------------------------------------------------ // Constants @@ -191,7 +190,7 @@ module.exports = { } if (lineCount > maxLines) { - const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(funcNode)); + const name = upperCaseFirst(astUtils.getFunctionNameWithKind(funcNode)); context.report({ node, diff --git a/lib/rules/max-lines.js b/lib/rules/max-lines.js index ceb014aff71..8bd5a1c95f4 100644 --- a/lib/rules/max-lines.js +++ b/lib/rules/max-lines.js @@ -8,9 +8,22 @@ // Requirements //------------------------------------------------------------------------------ -const lodash = require("lodash"); const astUtils = require("./utils/ast-utils"); +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Creates an array of numbers from `start` up to, but not including, `end` + * @param {number} start The start of the range + * @param {number} end The end of the range + * @returns {number[]} The range of numbers + */ +function range(start, end) { + return [...Array(end - start).keys()].map(x => x + start); +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -119,11 +132,25 @@ module.exports = { } if (start <= end) { - return lodash.range(start, end + 1); + return range(start, end + 1); } return []; } + /** + * Returns a new array formed by applying a given callback function to each element of the array, and then flattening the result by one level. + * TODO(stephenwade): Replace this with array.flatMap when we drop support for Node v10 + * @param {any[]} array The array to process + * @param {Function} fn The function to use + * @returns {any[]} The result array + */ + function flatMap(array, fn) { + const mapped = array.map(fn); + const flattened = [].concat(...mapped); + + return flattened; + } + return { "Program:exit"() { let lines = sourceCode.lines.map((text, i) => ({ @@ -135,7 +162,7 @@ module.exports = { * If file ends with a linebreak, `sourceCode.lines` will have one extra empty line at the end. * That isn't a real line, so we shouldn't count it. */ - if (lines.length > 1 && lodash.last(lines).text === "") { + if (lines.length > 1 && lines[lines.length - 1].text === "") { lines.pop(); } @@ -146,9 +173,7 @@ module.exports = { if (skipComments) { const comments = sourceCode.getAllComments(); - const commentLines = lodash.flatten( - comments.map(comment => getLinesWithoutCode(comment)) - ); + const commentLines = flatMap(comments, comment => getLinesWithoutCode(comment)); lines = lines.filter( l => !commentLines.includes(l.lineNumber) @@ -163,7 +188,7 @@ module.exports = { }, end: { line: sourceCode.lines.length, - column: lodash.last(sourceCode.lines).length + column: sourceCode.lines[sourceCode.lines.length - 1].length } }; diff --git a/lib/rules/max-params.js b/lib/rules/max-params.js index 4eebe2d95a3..8fb798401cb 100644 --- a/lib/rules/max-params.js +++ b/lib/rules/max-params.js @@ -9,9 +9,8 @@ // Requirements //------------------------------------------------------------------------------ -const lodash = require("lodash"); - const astUtils = require("./utils/ast-utils"); +const { upperCaseFirst } = require("../shared/string-utils"); //------------------------------------------------------------------------------ // Rule Definition @@ -85,7 +84,7 @@ module.exports = { node, messageId: "exceed", data: { - name: lodash.upperFirst(astUtils.getFunctionNameWithKind(node)), + name: upperCaseFirst(astUtils.getFunctionNameWithKind(node)), count: node.params.length, max: numParams } diff --git a/lib/rules/max-statements.js b/lib/rules/max-statements.js index 437b393a508..65d5539550d 100644 --- a/lib/rules/max-statements.js +++ b/lib/rules/max-statements.js @@ -9,9 +9,8 @@ // Requirements //------------------------------------------------------------------------------ -const lodash = require("lodash"); - const astUtils = require("./utils/ast-utils"); +const { upperCaseFirst } = require("../shared/string-utils"); //------------------------------------------------------------------------------ // Rule Definition @@ -97,7 +96,7 @@ module.exports = { */ function reportIfTooManyStatements(node, count, max) { if (count > max) { - const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(node)); + const name = upperCaseFirst(astUtils.getFunctionNameWithKind(node)); context.report({ node, diff --git a/lib/rules/no-fallthrough.js b/lib/rules/no-fallthrough.js index dd1f3ed9d9a..e8016e93e59 100644 --- a/lib/rules/no-fallthrough.js +++ b/lib/rules/no-fallthrough.js @@ -4,12 +4,6 @@ */ "use strict"; -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const lodash = require("lodash"); - //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ @@ -25,7 +19,7 @@ const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/iu; */ function hasFallthroughComment(node, context, fallthroughCommentPattern) { const sourceCode = context.getSourceCode(); - const comment = lodash.last(sourceCode.getCommentsBefore(node)); + const comment = sourceCode.getCommentsBefore(node).pop(); return Boolean(comment && fallthroughCommentPattern.test(comment.value)); } @@ -133,7 +127,7 @@ module.exports = { */ if (currentCodePath.currentSegments.some(isReachable) && (node.consequent.length > 0 || hasBlankLinesBetween(node, nextToken)) && - lodash.last(node.parent.cases) !== node) { + node.parent.cases[node.parent.cases.length - 1] !== node) { fallthroughCase = node; } } diff --git a/lib/rules/no-useless-backreference.js b/lib/rules/no-useless-backreference.js index 958e3d5dd4e..529c16439e3 100644 --- a/lib/rules/no-useless-backreference.js +++ b/lib/rules/no-useless-backreference.js @@ -11,7 +11,6 @@ const { CALL, CONSTRUCT, ReferenceTracker, getStringIfConstant } = require("eslint-utils"); const { RegExpParser, visitRegExpAST } = require("regexpp"); -const lodash = require("lodash"); //------------------------------------------------------------------------------ // Helpers @@ -137,7 +136,7 @@ module.exports = { // the opposite of the previous when the regex is matching backward in a lookbehind context. messageId = "backward"; - } else if (lodash.last(groupCut).type === "Alternative") { + } else if (groupCut[groupCut.length - 1].type === "Alternative") { // group's and bref's ancestor nodes below the lowest common ancestor are sibling alternatives => they're disjunctive. messageId = "disjunctive"; diff --git a/lib/rules/no-useless-computed-key.js b/lib/rules/no-useless-computed-key.js index e0505a318ef..a1cacc29612 100644 --- a/lib/rules/no-useless-computed-key.js +++ b/lib/rules/no-useless-computed-key.js @@ -8,7 +8,6 @@ // Requirements //------------------------------------------------------------------------------ -const lodash = require("lodash"); const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ @@ -95,9 +94,16 @@ module.exports = { } } + /** + * A no-op function to act as placeholder for checking a node when the `enforceForClassMembers` option is `false`. + * @returns {void} + * @private + */ + function noop() {} + return { Property: check, - MethodDefinition: enforceForClassMembers ? check : lodash.noop + MethodDefinition: enforceForClassMembers ? check : noop }; } }; diff --git a/lib/rules/no-warning-comments.js b/lib/rules/no-warning-comments.js index 0691a31f77e..e5f702bc7d7 100644 --- a/lib/rules/no-warning-comments.js +++ b/lib/rules/no-warning-comments.js @@ -5,7 +5,7 @@ "use strict"; -const { escapeRegExp } = require("lodash"); +const escapeRegExp = require("escape-string-regexp"); const astUtils = require("./utils/ast-utils"); const CHAR_LIMIT = 40; diff --git a/lib/rules/object-curly-newline.js b/lib/rules/object-curly-newline.js index 9b64a1b5c6a..1fbea00c5d7 100644 --- a/lib/rules/object-curly-newline.js +++ b/lib/rules/object-curly-newline.js @@ -10,7 +10,6 @@ //------------------------------------------------------------------------------ const astUtils = require("./utils/ast-utils"); -const lodash = require("lodash"); //------------------------------------------------------------------------------ // Helpers @@ -69,6 +68,24 @@ function normalizeOptionValue(value) { return { multiline, minProperties, consistent }; } +/** + * Checks if a value is an object. + * @param {any} value The value to check + * @returns {boolean} `true` if the value is an object, otherwise `false` + */ +function isObject(value) { + return typeof value === "object" && value !== null; +} + +/** + * Checks if an option is a node-specific option + * @param {any} option The option to check + * @returns {boolean} `true` if the option is node-specific, otherwise `false` + */ +function isNodeSpecificOption(option) { + return isObject(option) || typeof option === "string"; +} + /** * Normalizes a given option value. * @param {string|Object|undefined} options An option value to parse. @@ -80,9 +97,7 @@ function normalizeOptionValue(value) { * }} Normalized option object. */ function normalizeOptions(options) { - const isNodeSpecificOption = lodash.overSome([lodash.isPlainObject, lodash.isString]); - - if (lodash.isPlainObject(options) && Object.values(options).some(isNodeSpecificOption)) { + if (isObject(options) && Object.values(options).some(isNodeSpecificOption)) { return { ObjectExpression: normalizeOptionValue(options.ObjectExpression), ObjectPattern: normalizeOptionValue(options.ObjectPattern), diff --git a/lib/rules/spaced-comment.js b/lib/rules/spaced-comment.js index d3221f0ea79..226a2d44798 100644 --- a/lib/rules/spaced-comment.js +++ b/lib/rules/spaced-comment.js @@ -4,7 +4,7 @@ */ "use strict"; -const lodash = require("lodash"); +const escapeRegExp = require("escape-string-regexp"); const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ @@ -17,7 +17,7 @@ const astUtils = require("./utils/ast-utils"); * @returns {string} An escaped string. */ function escape(s) { - return `(?:${lodash.escapeRegExp(s)})`; + return `(?:${escapeRegExp(s)})`; } /** diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js index 679eebb4c45..6b853001132 100644 --- a/lib/rules/utils/ast-utils.js +++ b/lib/rules/utils/ast-utils.js @@ -11,7 +11,7 @@ const esutils = require("esutils"); const espree = require("espree"); -const lodash = require("lodash"); +const escapeRegExp = require("escape-string-regexp"); const { breakableTypePattern, createGlobalLinebreakMatcher, @@ -1756,7 +1756,7 @@ module.exports = { * @returns {SourceLocation} The `loc` object. */ getNameLocationInGlobalDirectiveComment(sourceCode, comment, name) { - const namePattern = new RegExp(`[\\s,]${lodash.escapeRegExp(name)}(?:$|[\\s,:])`, "gu"); + const namePattern = new RegExp(`[\\s,]${escapeRegExp(name)}(?:$|[\\s,:])`, "gu"); // To ignore the first text "global". namePattern.lastIndex = comment.value.indexOf("global") + 6; diff --git a/lib/shared/deprecation-warnings.js b/lib/shared/deprecation-warnings.js index 1438eaa69bf..1a0501ab057 100644 --- a/lib/shared/deprecation-warnings.js +++ b/lib/shared/deprecation-warnings.js @@ -9,7 +9,6 @@ //------------------------------------------------------------------------------ const path = require("path"); -const lodash = require("lodash"); //------------------------------------------------------------------------------ // Private @@ -28,6 +27,8 @@ const deprecationWarningMessages = { "projects in order to avoid loading '~/.eslintrc.*' accidentally." }; +const sourceFileErrorCache = new Set(); + /** * Emits a deprecation warning containing a given filepath. A new deprecation warning is emitted * for each unique file path, but repeated invocations with the same file path have no effect. @@ -36,7 +37,15 @@ const deprecationWarningMessages = { * @param {string} errorCode The warning message to show. * @returns {void} */ -const emitDeprecationWarning = lodash.memoize((source, errorCode) => { +function emitDeprecationWarning(source, errorCode) { + const cacheKey = JSON.stringify({ source, errorCode }); + + if (sourceFileErrorCache.has(cacheKey)) { + return; + } + + sourceFileErrorCache.add(cacheKey); + const rel = path.relative(process.cwd(), source); const message = deprecationWarningMessages[errorCode]; @@ -45,7 +54,7 @@ const emitDeprecationWarning = lodash.memoize((source, errorCode) => { "DeprecationWarning", errorCode ); -}, (...args) => JSON.stringify(args)); +} //------------------------------------------------------------------------------ // Public Interface diff --git a/lib/shared/string-utils.js b/lib/shared/string-utils.js new file mode 100644 index 00000000000..e4a55d79931 --- /dev/null +++ b/lib/shared/string-utils.js @@ -0,0 +1,22 @@ +/** + * @fileoverview Utilities to operate on strings. + * @author Stephen Wade + */ + +"use strict"; + +/** + * Converts the first letter of a string to uppercase. + * @param {string} string The string to operate on + * @returns {string} The converted string + */ +function upperCaseFirst(string) { + if (string.length <= 1) { + return string.toUpperCase(); + } + return string[0].toUpperCase() + string.slice(1); +} + +module.exports = { + upperCaseFirst +}; diff --git a/lib/source-code/source-code.js b/lib/source-code/source-code.js index 6b20495b6fc..c13ce29b877 100644 --- a/lib/source-code/source-code.js +++ b/lib/source-code/source-code.js @@ -12,8 +12,7 @@ const { isCommentToken } = require("eslint-utils"), TokenStore = require("./token-store"), astUtils = require("../shared/ast-utils"), - Traverser = require("../shared/traverser"), - lodash = require("lodash"); + Traverser = require("../shared/traverser"); //------------------------------------------------------------------------------ // Private @@ -531,10 +530,12 @@ class SourceCode extends TokenStore { } /* - * To figure out which line rangeIndex is on, determine the last index at which rangeIndex could - * be inserted into lineIndices to keep the list sorted. + * To figure out which line index is on, determine the last place at which index could + * be inserted into lineStartIndices to keep the list sorted. */ - const lineNumber = lodash.sortedLastIndex(this.lineStartIndices, index); + const lineNumber = index >= this.lineStartIndices[this.lineStartIndices.length - 1] + ? this.lineStartIndices.length + : this.lineStartIndices.findIndex(el => index < el); return { line: lineNumber, column: index - this.lineStartIndices[lineNumber - 1] }; } diff --git a/lib/source-code/token-store/utils.js b/lib/source-code/token-store/utils.js index 21e1d6ff7c3..a2bd77de71a 100644 --- a/lib/source-code/token-store/utils.js +++ b/lib/source-code/token-store/utils.js @@ -4,12 +4,6 @@ */ "use strict"; -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const lodash = require("lodash"); - //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ @@ -29,18 +23,16 @@ function getStartLocation(token) { //------------------------------------------------------------------------------ /** - * Binary-searches the index of the first token which is after the given location. + * Finds the index of the first token which is after the given location. * If it was not found, this returns `tokens.length`. * @param {(Token|Comment)[]} tokens It searches the token in this list. * @param {number} location The location to search. * @returns {number} The found index or `tokens.length`. */ exports.search = function search(tokens, location) { - return lodash.sortedIndexBy( - tokens, - { range: [location] }, - getStartLocation - ); + const index = tokens.findIndex(el => location <= getStartLocation(el)); + + return index === -1 ? tokens.length : index; }; /** diff --git a/messages/all-files-ignored.txt b/messages/all-files-ignored.js similarity index 51% rename from messages/all-files-ignored.txt rename to messages/all-files-ignored.js index 3f4c8ced4c9..d85828d36e7 100644 --- a/messages/all-files-ignored.txt +++ b/messages/all-files-ignored.js @@ -1,8 +1,16 @@ -You are linting "<%= pattern %>", but all of the files matching the glob pattern "<%= pattern %>" are ignored. +"use strict"; -If you don't want to lint these files, remove the pattern "<%= pattern %>" from the list of arguments passed to ESLint. +module.exports = function(it) { + const { pattern } = it; + + return ` +You are linting "${pattern}", but all of the files matching the glob pattern "${pattern}" are ignored. + +If you don't want to lint these files, remove the pattern "${pattern}" from the list of arguments passed to ESLint. If you do want to lint these files, try the following solutions: * Check your .eslintignore file, or the eslintIgnore property in package.json, to ensure that the files are not configured to be ignored. * Explicitly list the files from this glob that you'd like to lint on the command-line, rather than providing a glob as an argument. +`.trimLeft(); +}; diff --git a/messages/extend-config-missing.js b/messages/extend-config-missing.js new file mode 100644 index 00000000000..db8a5c64b9f --- /dev/null +++ b/messages/extend-config-missing.js @@ -0,0 +1,13 @@ +"use strict"; + +module.exports = function(it) { + const { configName, importerName } = it; + + return ` +ESLint couldn't find the config "${configName}" to extend from. Please check that the name of the config is correct. + +The config "${configName}" was referenced from the config file in "${importerName}". + +If you still have problems, please stop by https://eslint.org/chat/help to chat with the team. +`.trimLeft(); +}; diff --git a/messages/extend-config-missing.txt b/messages/extend-config-missing.txt deleted file mode 100644 index 4defd7ac4d1..00000000000 --- a/messages/extend-config-missing.txt +++ /dev/null @@ -1,5 +0,0 @@ -ESLint couldn't find the config "<%- configName %>" to extend from. Please check that the name of the config is correct. - -The config "<%- configName %>" was referenced from the config file in "<%- importerName %>". - -If you still have problems, please stop by https://eslint.org/chat/help to chat with the team. diff --git a/messages/failed-to-read-json.js b/messages/failed-to-read-json.js new file mode 100644 index 00000000000..5114de30980 --- /dev/null +++ b/messages/failed-to-read-json.js @@ -0,0 +1,11 @@ +"use strict"; + +module.exports = function(it) { + const { path, message } = it; + + return ` +Failed to read JSON file at ${path}: + +${message} +`.trimLeft(); +}; diff --git a/messages/failed-to-read-json.txt b/messages/failed-to-read-json.txt deleted file mode 100644 index b5e2b861cfc..00000000000 --- a/messages/failed-to-read-json.txt +++ /dev/null @@ -1,3 +0,0 @@ -Failed to read JSON file at <%= path %>: - -<%= message %> diff --git a/messages/file-not-found.js b/messages/file-not-found.js new file mode 100644 index 00000000000..26a5d57eff7 --- /dev/null +++ b/messages/file-not-found.js @@ -0,0 +1,10 @@ +"use strict"; + +module.exports = function(it) { + const { pattern, globDisabled } = it; + + return ` +No files matching the pattern "${pattern}"${globDisabled ? " (with disabling globs)" : ""} were found. +Please check for typing mistakes in the pattern. +`.trimLeft(); +}; diff --git a/messages/file-not-found.txt b/messages/file-not-found.txt deleted file mode 100644 index 639498eb5c6..00000000000 --- a/messages/file-not-found.txt +++ /dev/null @@ -1,2 +0,0 @@ -No files matching the pattern "<%= pattern %>"<% if (globDisabled) { %> (with disabling globs)<% } %> were found. -Please check for typing mistakes in the pattern. diff --git a/messages/no-config-found.txt b/messages/no-config-found.js similarity index 52% rename from messages/no-config-found.txt rename to messages/no-config-found.js index b46a7e5a7a6..1042143f9fd 100644 --- a/messages/no-config-found.txt +++ b/messages/no-config-found.js @@ -1,7 +1,15 @@ +"use strict"; + +module.exports = function(it) { + const { directoryPath } = it; + + return ` ESLint couldn't find a configuration file. To set up a configuration file for this project, please run: eslint --init -ESLint looked for configuration files in <%= directoryPath %> and its ancestors. If it found none, it then looked in your home directory. +ESLint looked for configuration files in ${directoryPath} and its ancestors. If it found none, it then looked in your home directory. If you think you already have a configuration file or if you need more help, please stop by the ESLint chat room: https://eslint.org/chat/help +`.trimLeft(); +}; diff --git a/messages/plugin-conflict.js b/messages/plugin-conflict.js new file mode 100644 index 00000000000..c8c060e2f05 --- /dev/null +++ b/messages/plugin-conflict.js @@ -0,0 +1,22 @@ +"use strict"; + +module.exports = function(it) { + const { pluginId, plugins } = it; + + let result = `ESLint couldn't determine the plugin "${pluginId}" uniquely. +`; + + for (const { filePath, importerName } of plugins) { + result += ` +- ${filePath} (loaded in "${importerName}")`; + } + + result += ` + +Please remove the "plugins" setting from either config or remove either plugin installation. + +If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. +`; + + return result; +}; diff --git a/messages/plugin-conflict.txt b/messages/plugin-conflict.txt deleted file mode 100644 index 3ab4b340ef2..00000000000 --- a/messages/plugin-conflict.txt +++ /dev/null @@ -1,7 +0,0 @@ -ESLint couldn't determine the plugin "<%- pluginId %>" uniquely. -<% for (const { filePath, importerName } of plugins) { %> -- <%= filePath %> (loaded in "<%= importerName %>")<% } %> - -Please remove the "plugins" setting from either config or remove either plugin installation. - -If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. diff --git a/messages/plugin-invalid.js b/messages/plugin-invalid.js new file mode 100644 index 00000000000..7913576f000 --- /dev/null +++ b/messages/plugin-invalid.js @@ -0,0 +1,16 @@ +"use strict"; + +module.exports = function(it) { + const { configName, importerName } = it; + + return ` +"${configName}" is invalid syntax for a config specifier. + +* If your intention is to extend from a configuration exported from the plugin, add the configuration name after a slash: e.g. "${configName}/myConfig". +* If this is the name of a shareable config instead of a plugin, remove the "plugin:" prefix: i.e. "${configName.slice("plugin:".length)}". + +"${configName}" was referenced from the config file in "${importerName}". + +If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. +`.trimLeft(); +}; diff --git a/messages/plugin-invalid.txt b/messages/plugin-invalid.txt deleted file mode 100644 index 3ee251821be..00000000000 --- a/messages/plugin-invalid.txt +++ /dev/null @@ -1,8 +0,0 @@ -"<%- configName %>" is invalid syntax for a config specifier. - -* If your intention is to extend from a configuration exported from the plugin, add the configuration name after a slash: e.g. "<%- configName %>/myConfig". -* If this is the name of a shareable config instead of a plugin, remove the "plugin:" prefix: i.e. "<%- configName.slice("plugin:".length) %>". - -"<%- configName %>" was referenced from the config file in "<%- importerName %>". - -If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. diff --git a/messages/plugin-missing.js b/messages/plugin-missing.js new file mode 100644 index 00000000000..f58c78ceb38 --- /dev/null +++ b/messages/plugin-missing.js @@ -0,0 +1,19 @@ +"use strict"; + +module.exports = function(it) { + const { pluginName, resolvePluginsRelativeTo, importerName } = it; + + return ` +ESLint couldn't find the plugin "${pluginName}". + +(The package "${pluginName}" was not found when loaded as a Node module from the directory "${resolvePluginsRelativeTo}".) + +It's likely that the plugin isn't installed correctly. Try reinstalling by running the following: + + npm install ${pluginName}@latest --save-dev + +The plugin "${pluginName}" was referenced from the config file in "${importerName}". + +If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. +`.trimLeft(); +}; diff --git a/messages/plugin-missing.txt b/messages/plugin-missing.txt deleted file mode 100644 index aa25f59ac44..00000000000 --- a/messages/plugin-missing.txt +++ /dev/null @@ -1,11 +0,0 @@ -ESLint couldn't find the plugin "<%- pluginName %>". - -(The package "<%- pluginName %>" was not found when loaded as a Node module from the directory "<%- resolvePluginsRelativeTo %>".) - -It's likely that the plugin isn't installed correctly. Try reinstalling by running the following: - - npm install <%- pluginName %>@latest --save-dev - -The plugin "<%- pluginName %>" was referenced from the config file in "<%- importerName %>". - -If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. diff --git a/messages/print-config-with-directory-path.txt b/messages/print-config-with-directory-path.js similarity index 70% rename from messages/print-config-with-directory-path.txt rename to messages/print-config-with-directory-path.js index 1afc9b1e88b..6a5d571dd37 100644 --- a/messages/print-config-with-directory-path.txt +++ b/messages/print-config-with-directory-path.js @@ -1,2 +1,8 @@ +"use strict"; + +module.exports = function() { + return ` The '--print-config' CLI option requires a path to a source code file rather than a directory. See also: https://eslint.org/docs/user-guide/command-line-interface#--print-config +`.trimLeft(); +}; diff --git a/messages/whitespace-found.js b/messages/whitespace-found.js new file mode 100644 index 00000000000..4ce49ca3a4e --- /dev/null +++ b/messages/whitespace-found.js @@ -0,0 +1,11 @@ +"use strict"; + +module.exports = function(it) { + const { pluginName } = it; + + return ` +ESLint couldn't find the plugin "${pluginName}". because there is whitespace in the name. Please check your configuration and remove all whitespace from the plugin name. + +If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. +`.trimLeft(); +}; diff --git a/messages/whitespace-found.txt b/messages/whitespace-found.txt deleted file mode 100644 index 3eed1af5866..00000000000 --- a/messages/whitespace-found.txt +++ /dev/null @@ -1,3 +0,0 @@ -ESLint couldn't find the plugin "<%- pluginName %>". because there is whitespace in the name. Please check your configuration and remove all whitespace from the plugin name. - -If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. diff --git a/package.json b/package.json index 51a76ec31c8..31d96fe28cf 100644 --- a/package.json +++ b/package.json @@ -51,12 +51,14 @@ "debug": "^4.0.1", "doctrine": "^3.0.0", "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", "eslint-scope": "^5.1.1", "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^2.0.0", "espree": "^7.3.1", "esquery": "^1.4.0", "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^5.0.0", @@ -68,7 +70,7 @@ "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", - "lodash": "^4.17.21", + "lodash.merge": "^4.6.2", "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", @@ -77,7 +79,7 @@ "semver": "^7.2.1", "strip-ansi": "^6.0.0", "strip-json-comments": "^3.1.0", - "table": "^6.0.4", + "table": "^6.0.9", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, @@ -91,7 +93,6 @@ "core-js": "^3.1.3", "dateformat": "^3.0.3", "ejs": "^3.0.2", - "escape-string-regexp": "^3.0.0", "eslint": "file:.", "eslint-config-eslint": "file:packages/eslint-config-eslint", "eslint-plugin-eslint-plugin": "^2.2.1", diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 3f2393621b6..3aaf6f91f3f 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -11,8 +11,7 @@ const sinon = require("sinon"), EventEmitter = require("events"), { RuleTester } = require("../../../lib/rule-tester"), assert = require("chai").assert, - nodeAssert = require("assert"), - { noop } = require("lodash"); + nodeAssert = require("assert"); const NODE_ASSERT_STRICT_EQUAL_OPERATOR = (() => { try { @@ -23,6 +22,15 @@ const NODE_ASSERT_STRICT_EQUAL_OPERATOR = (() => { throw new Error("unexpected successful assertion"); })(); +/** + * Do nothing. + * @returns {void} + */ +function noop() { + + // do nothing. +} + //------------------------------------------------------------------------------ // Rewire Things //------------------------------------------------------------------------------ diff --git a/tests/lib/rules/no-invalid-this.js b/tests/lib/rules/no-invalid-this.js index 6e1b757a712..3b19d63696f 100644 --- a/tests/lib/rules/no-invalid-this.js +++ b/tests/lib/rules/no-invalid-this.js @@ -9,7 +9,8 @@ // Requirements //------------------------------------------------------------------------------ -const lodash = require("lodash"); +const merge = require("lodash.merge"); + const rule = require("../../../lib/rules/no-invalid-this"); const { RuleTester } = require("../../../lib/rule-tester"); @@ -69,7 +70,7 @@ function extractPatterns(patterns, type) { // Clone and apply the pattern environment. const patternsList = patterns.map(pattern => pattern[type].map(applyCondition => { - const thisPattern = lodash.cloneDeep(pattern); + const thisPattern = merge({}, pattern); applyCondition(thisPattern); @@ -79,7 +80,10 @@ function extractPatterns(patterns, type) { thisPattern.code += " /* should error */"; } - return lodash.omit(thisPattern, ["valid", "invalid"]); + delete thisPattern.valid; + delete thisPattern.invalid; + + return thisPattern; })); // Flatten. diff --git a/tests/lib/shared/string-utils.js b/tests/lib/shared/string-utils.js new file mode 100644 index 00000000000..bc48afaab4e --- /dev/null +++ b/tests/lib/shared/string-utils.js @@ -0,0 +1,41 @@ +/** + * @fileoverview Tests for string utils. + * @author Stephen Wade + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const assert = require("chai").assert; + +const { upperCaseFirst } = require("../../../lib/shared/string-utils"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +describe("upperCaseFirst", () => { + it("uppercases the first letter of a string", () => { + assert(upperCaseFirst("e") === "E"); + assert(upperCaseFirst("alphabet") === "Alphabet"); + assert(upperCaseFirst("one two three") === "One two three"); + }); + + it("only changes the case of the first letter", () => { + assert(upperCaseFirst("alphaBet") === "AlphaBet"); + assert(upperCaseFirst("one TWO three") === "One TWO three"); + }); + + it("does not change the case if the first letter is already uppercase", () => { + assert(upperCaseFirst("E") === "E"); + assert(upperCaseFirst("Alphabet") === "Alphabet"); + assert(upperCaseFirst("One Two Three") === "One Two Three"); + }); + + it("properly handles an empty string", () => { + assert(upperCaseFirst("") === ""); + }); +}); diff --git a/tools/eslint-fuzzer.js b/tools/eslint-fuzzer.js index 4104b8516b2..c9705200eaf 100644 --- a/tools/eslint-fuzzer.js +++ b/tools/eslint-fuzzer.js @@ -10,13 +10,25 @@ //------------------------------------------------------------------------------ const assert = require("assert"); -const lodash = require("lodash"); const eslump = require("eslump"); const espree = require("espree"); const SourceCodeFixer = require("../lib/linter/source-code-fixer"); const ruleConfigs = require("../lib/init/config-rule").createCoreRuleConfigs(true); const sampleMinimizer = require("./code-sample-minimizer"); +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Gets a random item from an array + * @param {any[]} array The array to sample + * @returns {any} The random item + */ +function sample(array) { + return array[Math.floor(Math.random() * array.length)]; +} + //------------------------------------------------------------------------------ // Public API //------------------------------------------------------------------------------ @@ -125,10 +137,16 @@ function fuzz(options) { } for (let i = 0; i < options.count; progressCallback(problems.length), i++) { - const sourceType = lodash.sample(["script", "module"]); + const rules = {}; + + for (const [id, configs] of Object.entries(ruleConfigs)) { + rules[id] = sample(configs); + } + + const sourceType = sample(["script", "module"]); const text = codeGenerator({ sourceType }); const config = { - rules: lodash.mapValues(ruleConfigs, lodash.sample), + rules, parserOptions: { sourceType, ecmaVersion: espree.latestEcmaVersion diff --git a/webpack.config.js b/webpack.config.js index b256f442460..a22c99bf861 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -39,8 +39,7 @@ module.exports = { targets: ">0.5%, not chrome 49, not ie 11, not safari 5.1" }] ] - }, - exclude: /node_modules[\\/]lodash/u + } } ] }, From afe95693e1e4316a1c6f01d39345061d4c5921c7 Mon Sep 17 00:00:00 2001 From: Mikhail Bodrov Date: Sat, 22 May 2021 01:27:55 +0300 Subject: [PATCH 13/67] Chore: use includes instead of indexOf (#14607) --- lib/rules/comma-spacing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/comma-spacing.js b/lib/rules/comma-spacing.js index 73c10a7711b..2bf41a00bb6 100644 --- a/lib/rules/comma-spacing.js +++ b/lib/rules/comma-spacing.js @@ -181,7 +181,7 @@ module.exports = { validateCommaItemSpacing({ comma: token, - left: astUtils.isCommaToken(previousToken) || commaTokensToIgnore.indexOf(token) > -1 ? null : previousToken, + left: astUtils.isCommaToken(previousToken) || commaTokensToIgnore.includes(token) ? null : previousToken, right: astUtils.isCommaToken(nextToken) ? null : nextToken }, token); }); From 9e9b5e07475564813b62cd1d7562a93c5fb4bc74 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Sat, 22 May 2021 04:15:29 +0530 Subject: [PATCH 14/67] Update: no-unused-vars false negative with comma operator (fixes #14325) (#14354) * Fix: report error for sequence expression in no-unused-vars (fixes #14325) * Chore: add tests * Update: suggestions * Chore: refactor * Chore: refactor * Fix: logic * Chore: add tests --- lib/rules/no-unused-vars.js | 30 ++++++++++++-- tests/lib/rules/no-unused-vars.js | 69 +++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js index 7619be331fa..adf465905c2 100644 --- a/lib/rules/no-unused-vars.js +++ b/lib/rules/no-unused-vars.js @@ -410,6 +410,31 @@ module.exports = { ); } + /** + * Checks whether a given node is unused expression or not. + * @param {ASTNode} node The node itself + * @returns {boolean} The node is an unused expression. + * @private + */ + function isUnusedExpression(node) { + const parent = node.parent; + + if (parent.type === "ExpressionStatement") { + return true; + } + + if (parent.type === "SequenceExpression") { + const isLastExpression = parent.expressions[parent.expressions.length - 1] === node; + + if (!isLastExpression) { + return true; + } + return isUnusedExpression(parent); + } + + return false; + } + /** * Checks whether a given reference is a read to update itself or not. * @param {eslint-scope.Reference} ref A reference to check. @@ -420,7 +445,6 @@ module.exports = { function isReadForItself(ref, rhsNode) { const id = ref.identifier; const parent = id.parent; - const grandparent = parent.parent; return ref.isRead() && ( @@ -428,12 +452,12 @@ module.exports = { (// in RHS of an assignment for itself. e.g. `a = a + 1` (( parent.type === "AssignmentExpression" && - grandparent.type === "ExpressionStatement" && + isUnusedExpression(parent) && parent.left === id ) || ( parent.type === "UpdateExpression" && - grandparent.type === "ExpressionStatement" + isUnusedExpression(parent) ) || rhsNode && isInside(id, rhsNode) && !isInsideOfStorableFunction(id, rhsNode))) diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js index 423afa167b8..48ccdb1d42f 100644 --- a/tests/lib/rules/no-unused-vars.js +++ b/tests/lib/rules/no-unused-vars.js @@ -177,6 +177,10 @@ ruleTester.run("no-unused-vars", rule, { { code: "(function(obj) { for ( const name in obj ) { return true } })({})", parserOptions: { ecmaVersion: 6 } }, { code: "(function(obj) { for ( const name in obj ) return true })({})", parserOptions: { ecmaVersion: 6 } }, + // Sequence Expressions (See https://github.com/eslint/eslint/issues/14325) + { code: "let x = 0; foo = (0, x++);", parserOptions: { ecmaVersion: 6 } }, + { code: "let x = 0; foo = (0, x += 1);", parserOptions: { ecmaVersion: 6 } }, + // caughtErrors { code: "try{}catch(err){console.error(err);}", @@ -995,6 +999,71 @@ ruleTester.run("no-unused-vars", rule, { definedError("c") ] }, + + // https://github.com/eslint/eslint/issues/14325 + { + code: `let x = 0; + x++, x = 0;`, + parserOptions: { ecmaVersion: 2015 }, + errors: [{ ...assignedError("x"), line: 2, column: 18 }] + }, + { + code: `let x = 0; + x++, x = 0; + x=3;`, + parserOptions: { ecmaVersion: 2015 }, + errors: [{ ...assignedError("x"), line: 3, column: 13 }] + }, + { + code: "let x = 0; x++, 0;", + parserOptions: { ecmaVersion: 2015 }, + errors: [{ ...assignedError("x"), line: 1, column: 12 }] + }, + { + code: "let x = 0; 0, x++;", + parserOptions: { ecmaVersion: 2015 }, + errors: [{ ...assignedError("x"), line: 1, column: 15 }] + }, + { + code: "let x = 0; 0, (1, x++);", + parserOptions: { ecmaVersion: 2015 }, + errors: [{ ...assignedError("x"), line: 1, column: 19 }] + }, + { + code: "let x = 0; foo = (x++, 0);", + parserOptions: { ecmaVersion: 2015 }, + errors: [{ ...assignedError("x"), line: 1, column: 19 }] + }, + { + code: "let x = 0; foo = ((0, x++), 0);", + parserOptions: { ecmaVersion: 2015 }, + errors: [{ ...assignedError("x"), line: 1, column: 23 }] + }, + { + code: "let x = 0; x += 1, 0;", + parserOptions: { ecmaVersion: 2015 }, + errors: [{ ...assignedError("x"), line: 1, column: 12 }] + }, + { + code: "let x = 0; 0, x += 1;", + parserOptions: { ecmaVersion: 2015 }, + errors: [{ ...assignedError("x"), line: 1, column: 15 }] + }, + { + code: "let x = 0; 0, (1, x += 1);", + parserOptions: { ecmaVersion: 2015 }, + errors: [{ ...assignedError("x"), line: 1, column: 19 }] + }, + { + code: "let x = 0; foo = (x += 1, 0);", + parserOptions: { ecmaVersion: 2015 }, + errors: [{ ...assignedError("x"), line: 1, column: 19 }] + }, + { + code: "let x = 0; foo = ((0, x += 1), 0);", + parserOptions: { ecmaVersion: 2015 }, + errors: [{ ...assignedError("x"), line: 1, column: 23 }] + }, { code: "(function ({ a, b }, { c } ) { return b; })();", parserOptions: { ecmaVersion: 2015 }, From 2c0868cbeadc9f42716fa1178ebdc6b4cee6d31e Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 22 May 2021 02:58:30 +0200 Subject: [PATCH 15/67] Chore: merge all html formatter files into `html.js` (#14612) --- .../formatters/html-template-message.js | 25 --- .../formatters/html-template-page.js | 123 ------------ .../formatters/html-template-result.js | 14 -- lib/cli-engine/formatters/html.js | 180 +++++++++++++++++- 4 files changed, 175 insertions(+), 167 deletions(-) delete mode 100644 lib/cli-engine/formatters/html-template-message.js delete mode 100644 lib/cli-engine/formatters/html-template-page.js delete mode 100644 lib/cli-engine/formatters/html-template-result.js diff --git a/lib/cli-engine/formatters/html-template-message.js b/lib/cli-engine/formatters/html-template-message.js deleted file mode 100644 index c259618bf28..00000000000 --- a/lib/cli-engine/formatters/html-template-message.js +++ /dev/null @@ -1,25 +0,0 @@ -"use strict"; - -module.exports = function(it, encodeHTML) { - const { - parentIndex, - lineNumber, - columnNumber, - severityNumber, - severityName, - message, - ruleUrl, - ruleId - } = it; - - return ` - - ${lineNumber}:${columnNumber} - ${severityName} - ${encodeHTML(message)} - - ${ruleId ? ruleId : ""} - - -`.trimLeft(); -}; diff --git a/lib/cli-engine/formatters/html-template-page.js b/lib/cli-engine/formatters/html-template-page.js deleted file mode 100644 index e37a71e2d95..00000000000 --- a/lib/cli-engine/formatters/html-template-page.js +++ /dev/null @@ -1,123 +0,0 @@ -"use strict"; - -module.exports = function(it) { - const { reportColor, reportSummary, date, results } = it; - - return ` - - - - - ESLint Report - - - -
-

ESLint Report

-
- ${reportSummary} - Generated on ${date} -
-
- - - ${results} - -
- - - -`.trimLeft(); -}; diff --git a/lib/cli-engine/formatters/html-template-result.js b/lib/cli-engine/formatters/html-template-result.js deleted file mode 100644 index 5048f72e928..00000000000 --- a/lib/cli-engine/formatters/html-template-result.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; - -module.exports = function(it, encodeHTML) { - const { color, index, filePath, summary } = it; - - return ` - - - [+] ${encodeHTML(filePath)} - ${encodeHTML(summary)} - - -`.trimLeft(); -}; diff --git a/lib/cli-engine/formatters/html.js b/lib/cli-engine/formatters/html.js index b9739f05e2d..baddb63079d 100644 --- a/lib/cli-engine/formatters/html.js +++ b/lib/cli-engine/formatters/html.js @@ -25,9 +25,132 @@ const encodeHTML = (function() { }; }()); -const pageTemplate = require("./html-template-page.js"); -const messageTemplate = require("./html-template-message.js"); -const resultTemplate = require("./html-template-result.js"); +/** + * Get the final HTML document. + * @param {Object} it data for the document. + * @returns {string} HTML document. + */ +function pageTemplate(it) { + const { reportColor, reportSummary, date, results } = it; + + return ` + + + + + ESLint Report + + + +
+

ESLint Report

+
+ ${reportSummary} - Generated on ${date} +
+
+ + + ${results} + +
+ + + +`.trimLeft(); +} /** * Given a word and a count, append an s if count is not one. @@ -71,6 +194,35 @@ function renderColor(totalErrors, totalWarnings) { return 0; } +/** + * Get HTML (table row) describing a single message. + * @param {Object} it data for the message. + * @returns {string} HTML (table row) describing the message. + */ +function messageTemplate(it) { + const { + parentIndex, + lineNumber, + columnNumber, + severityNumber, + severityName, + message, + ruleUrl, + ruleId + } = it; + + return ` + + ${lineNumber}:${columnNumber} + ${severityName} + ${encodeHTML(message)} + + ${ruleId ? ruleId : ""} + + +`.trimLeft(); +} + /** * Get HTML (table rows) describing the messages. * @param {Array} messages Messages. @@ -107,10 +259,28 @@ function renderMessages(messages, parentIndex, rulesMeta) { message: message.message, ruleId: message.ruleId, ruleUrl - }, encodeHTML); + }); }).join("\n"); } +/** + * Get HTML (table row) describing the result for a single file. + * @param {Object} it data for the file. + * @returns {string} HTML (table row) describing the result for the file. + */ +function resultTemplate(it) { + const { color, index, filePath, summary } = it; + + return ` + + + [+] ${encodeHTML(filePath)} + ${encodeHTML(summary)} + + +`.trimLeft(); +} + // eslint-disable-next-line jsdoc/require-description /** * @param {Array} results Test results. @@ -123,7 +293,7 @@ function renderResults(results, rulesMeta) { color: renderColor(result.errorCount, result.warningCount), filePath: result.filePath, summary: renderSummary(result.errorCount, result.warningCount) - }, encodeHTML) + renderMessages(result.messages, index, rulesMeta)).join("\n"); + }) + renderMessages(result.messages, index, rulesMeta)).join("\n"); } //------------------------------------------------------------------------------ From ebd1292454ef99180a50934462b205e064645eb5 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 21 May 2021 21:16:57 -0400 Subject: [PATCH 16/67] Build: changelog update for 7.27.0 --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b976ab2ea52..54f33b6d442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +v7.27.0 - May 21, 2021 + +* [`2c0868c`](https://github.com/eslint/eslint/commit/2c0868cbeadc9f42716fa1178ebdc6b4cee6d31e) Chore: merge all html formatter files into `html.js` (#14612) (Milos Djermanovic) +* [`9e9b5e0`](https://github.com/eslint/eslint/commit/9e9b5e07475564813b62cd1d7562a93c5fb4bc74) Update: no-unused-vars false negative with comma operator (fixes #14325) (#14354) (Nitin Kumar) +* [`afe9569`](https://github.com/eslint/eslint/commit/afe95693e1e4316a1c6f01d39345061d4c5921c7) Chore: use includes instead of indexOf (#14607) (Mikhail Bodrov) +* [`c0f418e`](https://github.com/eslint/eslint/commit/c0f418e2476df98519bc156b81d20431984e8704) Chore: Remove lodash (#14287) (Stephen Wade) +* [`52655dd`](https://github.com/eslint/eslint/commit/52655dd54925ee02af2ba3a0ebc09de959ae3101) Update: no-restricted-imports custom message for patterns (fixes #11843) (#14580) (Alex Holden) +* [`967b1c4`](https://github.com/eslint/eslint/commit/967b1c4ceca8f5248378477da94ff118dafaa647) Chore: Fix typo in large.js (#14589) (Ikko Ashimine) +* [`2466a05`](https://github.com/eslint/eslint/commit/2466a05160de60958457d984b79fd445c12ebc98) Sponsors: Sync README with website (ESLint Jenkins) +* [`fe29f18`](https://github.com/eslint/eslint/commit/fe29f18227fd02fd7c3da033417d621275b00d0a) Sponsors: Sync README with website (ESLint Jenkins) +* [`086c1d6`](https://github.com/eslint/eslint/commit/086c1d6e8593cf8e7851daa8f2a890c213cf6999) Chore: add more test cases for `no-sequences` (#14579) (Nitin Kumar) +* [`6a2ced8`](https://github.com/eslint/eslint/commit/6a2ced892c0dc43fa4942293b9f1c4b9151c3741) Docs: Update README team and sponsors (ESLint Jenkins) + v7.26.0 - May 7, 2021 * [`aaf65e6`](https://github.com/eslint/eslint/commit/aaf65e629adb74401092c3ccc9cb4e4bd1c8609b) Upgrade: eslintrc for ModuleResolver fix (#14577) (Brandon Mills) From 18c791ad51f83a622834c2ba7db8f47956c4f18e Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 21 May 2021 21:16:58 -0400 Subject: [PATCH 17/67] 7.27.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31d96fe28cf..77016c21db4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "7.26.0", + "version": "7.27.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From 85a2725b1fade5538e727102d9701ccb503e54d4 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 21 May 2021 21:24:52 -0400 Subject: [PATCH 18/67] Docs: Update README team and sponsors --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 544da51ebcd..438a087ff23 100644 --- a/README.md +++ b/README.md @@ -223,11 +223,6 @@ Nicholas C. Zakas Brandon Mills - -
-Toru Nagashima -
-
Milos Djermanovic From ec28b5a2bdc69f34ce29d670f5e84d2446774a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=9B=E5=AE=9A=E8=B0=94=E7=9A=84=E7=8C=AB?= Date: Wed, 26 May 2021 10:46:44 +0800 Subject: [PATCH 19/67] Chore: upgrade eslint-plugin-eslint-plugin (#14590) * Chore: upgrade eslint-plugin-eslint-plugin * chore: revert generator fixers --- .eslintrc.js | 17 +---------------- package.json | 2 +- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 1457d9b553c..668bc2747c5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -71,22 +71,7 @@ module.exports = { "eslint-plugin/report-message-format": ["error", "[^a-z].*\\.$"], "eslint-plugin/require-meta-docs-description": "error", "eslint-plugin/require-meta-type": "error", - "eslint-plugin/test-case-property-ordering": [ - "error", - - // https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/79 - [ - "filename", - "code", - "output", - "options", - "parser", - "parserOptions", - "globals", - "env", - "errors" - ] - ], + "eslint-plugin/test-case-property-ordering": "error", "eslint-plugin/test-case-shorthand-strings": "error", "internal-rules/multiline-comment-style": "error" }, diff --git a/package.json b/package.json index 77016c21db4..46c0860105a 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "ejs": "^3.0.2", "eslint": "file:.", "eslint-config-eslint": "file:packages/eslint-config-eslint", - "eslint-plugin-eslint-plugin": "^2.2.1", + "eslint-plugin-eslint-plugin": "^3.0.3", "eslint-plugin-internal-rules": "file:tools/internal-rules", "eslint-plugin-jsdoc": "^25.4.3", "eslint-plugin-node": "^11.1.0", From e4f111b67d114adbf76a9c9dbb18fa4f49bc91b6 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 28 May 2021 04:26:42 +0200 Subject: [PATCH 20/67] Fix: arrow-body-style crash with object pattern (fixes #14633) (#14635) --- lib/rules/arrow-body-style.js | 32 +++++++++++++++++++---------- tests/lib/rules/arrow-body-style.js | 8 ++++++++ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/rules/arrow-body-style.js b/lib/rules/arrow-body-style.js index b2167fde77b..5b8a5f01167 100644 --- a/lib/rules/arrow-body-style.js +++ b/lib/rules/arrow-body-style.js @@ -87,17 +87,17 @@ module.exports = { } /** - * Gets the closing parenthesis which is the pair of the given opening parenthesis. - * @param {Token} token The opening parenthesis token to get. + * Gets the closing parenthesis by the given node. + * @param {ASTNode} node first node after an opening parenthesis. * @returns {Token} The found closing parenthesis token. */ - function findClosingParen(token) { - let node = sourceCode.getNodeByRangeIndex(token.range[0]); + function findClosingParen(node) { + let nodeToCheck = node; - while (!astUtils.isParenthesised(sourceCode, node)) { - node = node.parent; + while (!astUtils.isParenthesised(sourceCode, nodeToCheck)) { + nodeToCheck = nodeToCheck.parent; } - return sourceCode.getTokenAfter(node); + return sourceCode.getTokenAfter(nodeToCheck); } /** @@ -226,12 +226,22 @@ module.exports = { const arrowToken = sourceCode.getTokenBefore(arrowBody, astUtils.isArrowToken); const [firstTokenAfterArrow, secondTokenAfterArrow] = sourceCode.getTokensAfter(arrowToken, { count: 2 }); const lastToken = sourceCode.getLastToken(node); - const isParenthesisedObjectLiteral = + + let parenthesisedObjectLiteral = null; + + if ( astUtils.isOpeningParenToken(firstTokenAfterArrow) && - astUtils.isOpeningBraceToken(secondTokenAfterArrow); + astUtils.isOpeningBraceToken(secondTokenAfterArrow) + ) { + const braceNode = sourceCode.getNodeByRangeIndex(secondTokenAfterArrow.range[0]); + + if (braceNode.type === "ObjectExpression") { + parenthesisedObjectLiteral = braceNode; + } + } // If the value is object literal, remove parentheses which were forced by syntax. - if (isParenthesisedObjectLiteral) { + if (parenthesisedObjectLiteral) { const openingParenToken = firstTokenAfterArrow; const openingBraceToken = secondTokenAfterArrow; @@ -247,7 +257,7 @@ module.exports = { } // Closing paren for the object doesn't have to be lastToken, e.g.: () => ({}).foo() - fixes.push(fixer.remove(findClosingParen(openingBraceToken))); + fixes.push(fixer.remove(findClosingParen(parenthesisedObjectLiteral))); fixes.push(fixer.insertTextAfter(lastToken, "}")); } else { diff --git a/tests/lib/rules/arrow-body-style.js b/tests/lib/rules/arrow-body-style.js index 7a8de4fe5ef..0ccb440d392 100644 --- a/tests/lib/rules/arrow-body-style.js +++ b/tests/lib/rules/arrow-body-style.js @@ -810,6 +810,14 @@ ruleTester.run("arrow-body-style", rule, { `, options: ["always"], errors: [{ messageId: "expectedBlock" }] + }, + + // https://github.com/eslint/eslint/issues/14633 + { + code: "const createMarker = (color) => ({ latitude, longitude }, index) => {};", + output: "const createMarker = (color) => {return ({ latitude, longitude }, index) => {}};", + options: ["always"], + errors: [{ messageId: "expectedBlock" }] } ] }); From 958ff4e8a5102f204f1484d09985e28a79790996 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Fri, 28 May 2021 07:57:09 +0530 Subject: [PATCH 21/67] Docs: add note for arrow functions in no-seq rule (#14578) --- docs/rules/no-sequences.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/rules/no-sequences.md b/docs/rules/no-sequences.md index d4efc1c5ba5..87ba3c4b9b8 100644 --- a/docs/rules/no-sequences.md +++ b/docs/rules/no-sequences.md @@ -39,8 +39,6 @@ switch (val = foo(), val) {} while (val = foo(), val < 42); with (doSomething(), val) {} - -const foo = (val) => (console.log('bar'), val); ``` Examples of **correct** code for this rule: @@ -63,8 +61,32 @@ switch ((val = foo(), val)) {} while ((val = foo(), val < 42)); with ((doSomething(), val)) {} +``` + +### Note about arrow function bodies + +If an arrow function body is a statement rather than a block, and that statement contains a sequence, you need to use double parentheses around the statement to indicate that the sequence is intentional. + +Examples of **incorrect** code for arrow functions: + +```js +/*eslint no-sequences: "error"*/ +const foo = (val) => (console.log('bar'), val); + +const foo = () => ((bar = 123), 10)); + +const foo = () => { return (bar = 123), 10 } +``` + +Examples of **correct** code for arrow functions: +```js +/*eslint no-sequences: "error"*/ const foo = (val) => ((console.log('bar'), val)); + +const foo = () => (((bar = 123), 10)); + +const foo = () => { return ((bar = 123), 10) } ``` ## Options From 2e43dacd24337a82d4184fac9b44d497675f46ef Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Mon, 31 May 2021 11:41:22 +0530 Subject: [PATCH 22/67] Docs: fix `no-sequences` example (#14643) --- docs/rules/no-sequences.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/no-sequences.md b/docs/rules/no-sequences.md index 87ba3c4b9b8..f12c46573b7 100644 --- a/docs/rules/no-sequences.md +++ b/docs/rules/no-sequences.md @@ -73,7 +73,7 @@ Examples of **incorrect** code for arrow functions: /*eslint no-sequences: "error"*/ const foo = (val) => (console.log('bar'), val); -const foo = () => ((bar = 123), 10)); +const foo = () => ((bar = 123), 10); const foo = () => { return (bar = 123), 10 } ``` From bb66a3d91af426dac9a7ffdbe47bdbbc0ffd4dd7 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Fri, 4 Jun 2021 21:18:02 +0530 Subject: [PATCH 23/67] New: add `getPhysicalFilename()` method to rule context (fixes #11989) (#14616) * New: add `getPhysicalFilename()` method to the rule context object * Docs: update * Chore: add test * Chore: update more instances * Chore: apply suggestions * Chore: apply suggestions * Chore: fix typo Co-authored-by: Milos Djermanovic Co-authored-by: Milos Djermanovic --- docs/developer-guide/working-with-rules.md | 1 + lib/linter/linter.js | 12 +++-- tests/lib/linter/linter.js | 60 ++++++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md index 59e275de527..9dadae00e50 100644 --- a/docs/developer-guide/working-with-rules.md +++ b/docs/developer-guide/working-with-rules.md @@ -139,6 +139,7 @@ Additionally, the `context` object has the following methods: * If the node is an `ImportSpecifier`, `ImportDefaultSpecifier`, or `ImportNamespaceSpecifier`, the declared variable is returned. * Otherwise, if the node does not declare any variables, an empty array is returned. * `getFilename()` - returns the filename associated with the source. +* `getPhysicalFilename()` - when linting a file, it returns the full path of the file on disk without any code block information. When linting text, it returns the value passed to `—stdin-filename` or `` if not specified. * `getScope()` - returns the [scope](./scope-manager-interface.md#scope-interface) of the currently-traversed node. This information can be used to track references to variables. * `getSourceCode()` - returns a [`SourceCode`](#contextgetsourcecode) object that you can use to work with the source that was passed to ESLint. * `markVariableAsUsed(name)` - marks a variable with the given name in the current scope as used. This affects the [no-unused-vars](../rules/no-unused-vars.md) rule. Returns `true` if a variable with the given name was found and marked as used, otherwise `false`. diff --git a/lib/linter/linter.js b/lib/linter/linter.js index bdc6c1b1d01..44e88f845fa 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -828,9 +828,10 @@ const BASE_TRAVERSAL_CONTEXT = Object.freeze( * @param {string} filename The reported filename of the code * @param {boolean} disableFixes If true, it doesn't make `fix` properties. * @param {string | undefined} cwd cwd of the cli + * @param {string} physicalFilename The full path of the file on disk without any code block information * @returns {Problem[]} An array of reported problems */ -function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd) { +function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd, physicalFilename) { const emitter = createEmitter(); const nodeQueue = []; let currentNode = sourceCode.ast; @@ -859,6 +860,7 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parser getDeclaredVariables: sourceCode.scopeManager.getDeclaredVariables.bind(sourceCode.scopeManager), getCwd: () => cwd, getFilename: () => filename, + getPhysicalFilename: () => physicalFilename || filename, getScope: () => getScope(sourceCode.scopeManager, currentNode), getSourceCode: () => sourceCode, markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, parserOptions, name), @@ -1181,7 +1183,8 @@ class Linter { settings, options.filename, options.disableFixes, - slots.cwd + slots.cwd, + providedOptions.physicalFilename ); } catch (err) { err.message += `\nOccurred while linting ${options.filename}`; @@ -1284,6 +1287,7 @@ class Linter { _verifyWithProcessor(textOrSourceCode, config, options, configForRecursive) { const filename = options.filename || ""; const filenameToExpose = normalizeFilename(filename); + const physicalFilename = options.physicalFilename || filenameToExpose; const text = ensureText(textOrSourceCode); const preprocess = options.preprocess || (rawText => [rawText]); @@ -1316,7 +1320,7 @@ class Linter { return this._verifyWithConfigArray( blockText, configForRecursive, - { ...options, filename: blockName } + { ...options, filename: blockName, physicalFilename } ); } @@ -1324,7 +1328,7 @@ class Linter { return this._verifyWithoutProcessors( blockText, config, - { ...options, filename: blockName } + { ...options, filename: blockName, physicalFilename } ); }); diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 976bd765755..bcf3edfb90c 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -1559,6 +1559,22 @@ describe("Linter", () => { assert.strictEqual(messages[0].message, filename); }); + it("has access to the physicalFilename", () => { + linter.defineRule(code, context => ({ + Literal(node) { + context.report(node, context.getPhysicalFilename()); + } + })); + + const config = { rules: {} }; + + config.rules[code] = 1; + + const messages = linter.verify("0", config, filename); + + assert.strictEqual(messages[0].message, filename); + }); + it("defaults filename to ''", () => { linter.defineRule(code, context => ({ Literal(node) { @@ -3408,6 +3424,41 @@ var a = "test2"; }); }); + describe("physicalFilenames", () => { + it("should be same as `filename` passed on options object, if no processors are used", () => { + const physicalFilenameChecker = sinon.spy(context => { + assert.strictEqual(context.getPhysicalFilename(), "foo.js"); + return {}; + }); + + linter.defineRule("checker", physicalFilenameChecker); + linter.verify("foo;", { rules: { checker: "error" } }, { filename: "foo.js" }); + assert(physicalFilenameChecker.calledOnce); + }); + + it("should default physicalFilename to when options object doesn't have filename", () => { + const physicalFilenameChecker = sinon.spy(context => { + assert.strictEqual(context.getPhysicalFilename(), ""); + return {}; + }); + + linter.defineRule("checker", physicalFilenameChecker); + linter.verify("foo;", { rules: { checker: "error" } }, {}); + assert(physicalFilenameChecker.calledOnce); + }); + + it("should default physicalFilename to when only two arguments are passed", () => { + const physicalFilenameChecker = sinon.spy(context => { + assert.strictEqual(context.getPhysicalFilename(), ""); + return {}; + }); + + linter.defineRule("checker", physicalFilenameChecker); + linter.verify("foo;", { rules: { checker: "error" } }); + assert(physicalFilenameChecker.calledOnce); + }); + }); + it("should report warnings in order by line and column when called", () => { const code = "foo()\n alert('test')"; @@ -4783,14 +4834,17 @@ var a = "test2"; describe("processors", () => { let receivedFilenames = []; + let receivedPhysicalFilenames = []; beforeEach(() => { receivedFilenames = []; + receivedPhysicalFilenames = []; // A rule that always reports the AST with a message equal to the source text linter.defineRule("report-original-text", context => ({ Program(ast) { receivedFilenames.push(context.getFilename()); + receivedPhysicalFilenames.push(context.getPhysicalFilename()); context.report({ node: ast, message: context.getSourceCode().text }); } })); @@ -4845,10 +4899,16 @@ var a = "test2"; assert.strictEqual(problems.length, 3); assert.deepStrictEqual(problems.map(problem => problem.message), ["foo", "bar", "baz"]); + + // filename assert.strictEqual(receivedFilenames.length, 3); assert(/^filename\.js[/\\]0_block\.js/u.test(receivedFilenames[0])); assert(/^filename\.js[/\\]1_block\.js/u.test(receivedFilenames[1])); assert(/^filename\.js[/\\]2_block\.js/u.test(receivedFilenames[2])); + + // physical filename + assert.strictEqual(receivedPhysicalFilenames.length, 3); + assert.strictEqual(receivedPhysicalFilenames.every(name => name === filename), true); }); it("should receive text even if a SourceCode object was given.", () => { From e44ce0a8acfaad513c385150c25e76e82a1b8f12 Mon Sep 17 00:00:00 2001 From: Soufiane Boutahlil Date: Fri, 4 Jun 2021 16:58:55 +0100 Subject: [PATCH 24/67] Fix: no-duplicate-imports allow unmergeable (fixes #12758, fixes #12760) (#14238) * Fix: ignore unmergable imports when checking no-duplicate-imports (fixes #13180) & Fix: Ignore re-export all in no-duplicate-imports (fixes #12760) * Fix: ignore unmergable imports when checking no-duplicate-imports (fixes #13180) & Fix: Ignore re-export all in no-duplicate-imports (fixes #12760) * Fix: ignore unmergable imports when checking no-duplicate-imports (fixes #13180) & Fix: Ignore re-export all in no-duplicate-imports (fixes #12760) * Fix: ignore unmergable imports when checking no-duplicate-imports (fixes #13180) & Fix: Ignore re-export all in no-duplicate-imports (fixes #12760) * Fix: ignore unmergable imports when checking no-duplicate-imports (fixes #13180) & Fix: Ignore re-export all in no-duplicate-imports (fixes #12760) * Fix: ignore unmergable imports when checking no-duplicate-imports (fixes #13180) & Fix: Ignore re-export all in no-duplicate-imports (fixes #12760) * Fix: ignore unmergable imports when checking no-duplicate-imports (fixes #13180) & Fix: Ignore re-export all in no-duplicate-imports (fixes #12760) * Fix: ignore unmergable imports when checking no-duplicate-imports (fixes #13180) & Fix: Ignore re-export all in no-duplicate-imports (fixes #12760) * Fix: ignore unmergable imports when checking no-duplicate-imports (fixes #13180) & Fix: Ignore re-export all in no-duplicate-imports (fixes #12760) --- docs/rules/no-duplicate-imports.md | 26 ++- lib/rules/no-duplicate-imports.js | 280 ++++++++++++++++++------ tests/lib/rules/no-duplicate-imports.js | 64 +++++- 3 files changed, 298 insertions(+), 72 deletions(-) diff --git a/docs/rules/no-duplicate-imports.md b/docs/rules/no-duplicate-imports.md index 683f31584a1..437d2c3daa1 100644 --- a/docs/rules/no-duplicate-imports.md +++ b/docs/rules/no-duplicate-imports.md @@ -12,7 +12,7 @@ import { find } from 'module'; ## Rule Details -This rule requires that all imports from a single module exists in a single `import` statement. +This rule requires that all imports from a single module that can be merged exist in a single `import` statement. Example of **incorrect** code for this rule: @@ -33,6 +33,16 @@ import { merge, find } from 'module'; import something from 'another-module'; ``` +Example of **correct** code for this rule: + +```js +/*eslint no-duplicate-imports: "error"*/ + +// not mergeable +import { merge } from 'module'; +import * as something from 'module'; +``` + ## Options This rule takes one optional argument, an object with a single key, `includeExports` which is a `boolean`. It defaults to `false`. @@ -58,3 +68,17 @@ import { merge, find } from 'module'; export { find }; ``` + +Example of **correct** code for this rule with the `{ "includeExports": true }` option: + +```js +/*eslint no-duplicate-imports: ["error", { "includeExports": true }]*/ + +import { merge, find } from 'module'; + +// cannot be merged with the above import +export * as something from 'module'; + +// cannot be written differently +export * from 'module'; +``` diff --git a/lib/rules/no-duplicate-imports.js b/lib/rules/no-duplicate-imports.js index 7218dc64add..cc3da1d5a68 100644 --- a/lib/rules/no-duplicate-imports.js +++ b/lib/rules/no-duplicate-imports.js @@ -4,92 +4,225 @@ */ "use strict"; +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const NAMED_TYPES = ["ImportSpecifier", "ExportSpecifier"]; +const NAMESPACE_TYPES = [ + "ImportNamespaceSpecifier", + "ExportNamespaceSpecifier" +]; + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** - * Returns the name of the module imported or re-exported. + * Check if an import/export type belongs to (ImportSpecifier|ExportSpecifier) or (ImportNamespaceSpecifier|ExportNamespaceSpecifier). + * @param {string} importExportType An import/export type to check. + * @param {string} type Can be "named" or "namespace" + * @returns {boolean} True if import/export type belongs to (ImportSpecifier|ExportSpecifier) or (ImportNamespaceSpecifier|ExportNamespaceSpecifier) and false if it doesn't. + */ +function isImportExportSpecifier(importExportType, type) { + const arrayToCheck = type === "named" ? NAMED_TYPES : NAMESPACE_TYPES; + + return arrayToCheck.includes(importExportType); +} + +/** + * Return the type of (import|export). * @param {ASTNode} node A node to get. - * @returns {string} the name of the module, or empty string if no name. + * @returns {string} The type of the (import|export). */ -function getValue(node) { - if (node && node.source && node.source.value) { - return node.source.value.trim(); +function getImportExportType(node) { + if (node.specifiers && node.specifiers.length > 0) { + const nodeSpecifiers = node.specifiers; + const index = nodeSpecifiers.findIndex( + ({ type }) => + isImportExportSpecifier(type, "named") || + isImportExportSpecifier(type, "namespace") + ); + const i = index > -1 ? index : 0; + + return nodeSpecifiers[i].type; } + if (node.type === "ExportAllDeclaration") { + if (node.exported) { + return "ExportNamespaceSpecifier"; + } + return "ExportAll"; + } + return "SideEffectImport"; +} - return ""; +/** + * Returns a boolean indicates if two (import|export) can be merged + * @param {ASTNode} node1 A node to check. + * @param {ASTNode} node2 A node to check. + * @returns {boolean} True if two (import|export) can be merged, false if they can't. + */ +function isImportExportCanBeMerged(node1, node2) { + const importExportType1 = getImportExportType(node1); + const importExportType2 = getImportExportType(node2); + + if ( + (importExportType1 === "ExportAll" && + importExportType2 !== "ExportAll" && + importExportType2 !== "SideEffectImport") || + (importExportType1 !== "ExportAll" && + importExportType1 !== "SideEffectImport" && + importExportType2 === "ExportAll") + ) { + return false; + } + if ( + (isImportExportSpecifier(importExportType1, "namespace") && + isImportExportSpecifier(importExportType2, "named")) || + (isImportExportSpecifier(importExportType2, "namespace") && + isImportExportSpecifier(importExportType1, "named")) + ) { + return false; + } + return true; } /** - * Checks if the name of the import or export exists in the given array, and reports if so. - * @param {RuleContext} context The ESLint rule context object. - * @param {ASTNode} node A node to get. - * @param {string} value The name of the imported or exported module. - * @param {string[]} array The array containing other imports or exports in the file. - * @param {string} messageId A messageId to be reported after the name of the module - * - * @returns {void} No return value + * Returns a boolean if we should report (import|export). + * @param {ASTNode} node A node to be reported or not. + * @param {[ASTNode]} previousNodes An array contains previous nodes of the module imported or exported. + * @returns {boolean} True if the (import|export) should be reported. */ -function checkAndReport(context, node, value, array, messageId) { - if (array.indexOf(value) !== -1) { - context.report({ - node, - messageId, - data: { - module: value - } - }); +function shouldReportImportExport(node, previousNodes) { + let i = 0; + + while (i < previousNodes.length) { + if (isImportExportCanBeMerged(node, previousNodes[i])) { + return true; + } + i++; } + return false; } /** - * @callback nodeCallback - * @param {ASTNode} node A node to handle. + * Returns array contains only nodes with declarations types equal to type. + * @param {[{node: ASTNode, declarationType: string}]} nodes An array contains objects, each object contains a node and a declaration type. + * @param {string} type Declaration type. + * @returns {[ASTNode]} An array contains only nodes with declarations types equal to type. + */ +function getNodesByDeclarationType(nodes, type) { + return nodes + .filter(({ declarationType }) => declarationType === type) + .map(({ node }) => node); +} + +/** + * Returns the name of the module imported or re-exported. + * @param {ASTNode} node A node to get. + * @returns {string} The name of the module, or empty string if no name. */ +function getModule(node) { + if (node && node.source && node.source.value) { + return node.source.value.trim(); + } + return ""; +} /** - * Returns a function handling the imports of a given file + * Checks if the (import|export) can be merged with at least one import or one export, and reports if so. * @param {RuleContext} context The ESLint rule context object. + * @param {ASTNode} node A node to get. + * @param {Map} modules A Map object contains as a key a module name and as value an array contains objects, each object contains a node and a declaration type. + * @param {string} declarationType A declaration type can be an import or export. * @param {boolean} includeExports Whether or not to check for exports in addition to imports. - * @param {string[]} importsInFile The array containing other imports in the file. - * @param {string[]} exportsInFile The array containing other exports in the file. - * - * @returns {nodeCallback} A function passed to ESLint to handle the statement. + * @returns {void} No return value. */ -function handleImports(context, includeExports, importsInFile, exportsInFile) { - return function(node) { - const value = getValue(node); +function checkAndReport( + context, + node, + modules, + declarationType, + includeExports +) { + const module = getModule(node); - if (value) { - checkAndReport(context, node, value, importsInFile, "import"); + if (modules.has(module)) { + const previousNodes = modules.get(module); + const messagesIds = []; + const importNodes = getNodesByDeclarationType(previousNodes, "import"); + let exportNodes; + if (includeExports) { + exportNodes = getNodesByDeclarationType(previousNodes, "export"); + } + if (declarationType === "import") { + if (shouldReportImportExport(node, importNodes)) { + messagesIds.push("import"); + } if (includeExports) { - checkAndReport(context, node, value, exportsInFile, "importAs"); + if (shouldReportImportExport(node, exportNodes)) { + messagesIds.push("importAs"); + } + } + } else if (declarationType === "export") { + if (shouldReportImportExport(node, exportNodes)) { + messagesIds.push("export"); + } + if (shouldReportImportExport(node, importNodes)) { + messagesIds.push("exportAs"); } - - importsInFile.push(value); } - }; + messagesIds.forEach(messageId => + context.report({ + node, + messageId, + data: { + module + } + })); + } } /** - * Returns a function handling the exports of a given file + * @callback nodeCallback + * @param {ASTNode} node A node to handle. + */ + +/** + * Returns a function handling the (imports|exports) of a given file * @param {RuleContext} context The ESLint rule context object. - * @param {string[]} importsInFile The array containing other imports in the file. - * @param {string[]} exportsInFile The array containing other exports in the file. - * + * @param {Map} modules A Map object contains as a key a module name and as value an array contains objects, each object contains a node and a declaration type. + * @param {string} declarationType A declaration type can be an import or export. + * @param {boolean} includeExports Whether or not to check for exports in addition to imports. * @returns {nodeCallback} A function passed to ESLint to handle the statement. */ -function handleExports(context, importsInFile, exportsInFile) { +function handleImportsExports( + context, + modules, + declarationType, + includeExports +) { return function(node) { - const value = getValue(node); + const module = getModule(node); + + if (module) { + checkAndReport( + context, + node, + modules, + declarationType, + includeExports + ); + const currentNode = { node, declarationType }; + let nodes = [currentNode]; - if (value) { - checkAndReport(context, node, value, exportsInFile, "export"); - checkAndReport(context, node, value, importsInFile, "exportAs"); + if (modules.has(module)) { + const previousNodes = modules.get(module); - exportsInFile.push(value); + nodes = [...previousNodes, currentNode]; + } + modules.set(module, nodes); } }; } @@ -105,16 +238,19 @@ module.exports = { url: "https://eslint.org/docs/rules/no-duplicate-imports" }, - schema: [{ - type: "object", - properties: { - includeExports: { - type: "boolean", - default: false - } - }, - additionalProperties: false - }], + schema: [ + { + type: "object", + properties: { + includeExports: { + type: "boolean", + default: false + } + }, + additionalProperties: false + } + ], + messages: { import: "'{{module}}' import is duplicated.", importAs: "'{{module}}' import is duplicated as export.", @@ -125,18 +261,30 @@ module.exports = { create(context) { const includeExports = (context.options[0] || {}).includeExports, - importsInFile = [], - exportsInFile = []; - + modules = new Map(); const handlers = { - ImportDeclaration: handleImports(context, includeExports, importsInFile, exportsInFile) + ImportDeclaration: handleImportsExports( + context, + modules, + "import", + includeExports + ) }; if (includeExports) { - handlers.ExportNamedDeclaration = handleExports(context, importsInFile, exportsInFile); - handlers.ExportAllDeclaration = handleExports(context, importsInFile, exportsInFile); + handlers.ExportNamedDeclaration = handleImportsExports( + context, + modules, + "export", + includeExports + ); + handlers.ExportAllDeclaration = handleImportsExports( + context, + modules, + "export", + includeExports + ); } - return handlers; } }; diff --git a/tests/lib/rules/no-duplicate-imports.js b/tests/lib/rules/no-duplicate-imports.js index 42d35c85751..e200bbc704d 100644 --- a/tests/lib/rules/no-duplicate-imports.js +++ b/tests/lib/rules/no-duplicate-imports.js @@ -16,7 +16,7 @@ const rule = require("../../../lib/rules/no-duplicate-imports"), // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6, sourceType: "module" } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 12, sourceType: "module" } }); ruleTester.run("no-duplicate-imports", rule, { valid: [ @@ -26,6 +26,9 @@ ruleTester.run("no-duplicate-imports", rule, { "import * as Foobar from \"async\";", "import \"foo\"", "import os from \"os\";\nexport { something } from \"os\";", + "import * as bar from \"os\";\nimport { baz } from \"os\";", + "import foo, * as bar from \"os\";\nimport { baz } from \"os\";", + "import foo, { bar } from \"os\";\nimport * as baz from \"os\";", { code: "import os from \"os\";\nexport { hello } from \"hello\";", options: [{ includeExports: true }] @@ -45,6 +48,26 @@ ruleTester.run("no-duplicate-imports", rule, { { code: "import { merge } from \"lodash-es\";\nexport { merge as lodashMerge }", options: [{ includeExports: true }] + }, + { + code: "export { something } from \"os\";\nexport * as os from \"os\";", + options: [{ includeExports: true }] + }, + { + code: "import { something } from \"os\";\nexport * as os from \"os\";", + options: [{ includeExports: true }] + }, + { + code: "import * as os from \"os\";\nexport { something } from \"os\";", + options: [{ includeExports: true }] + }, + { + code: "import os from \"os\";\nexport * from \"os\";", + options: [{ includeExports: true }] + }, + { + code: "export { something } from \"os\";\nexport * from \"os\";", + options: [{ includeExports: true }] } ], invalid: [ @@ -53,11 +76,22 @@ ruleTester.run("no-duplicate-imports", rule, { errors: [{ messageId: "import", data: { module: "fs" }, type: "ImportDeclaration" }] }, { - code: "import { merge } from \"lodash-es\";import { find } from \"lodash-es\";", + code: "import { merge } from \"lodash-es\";\nimport { find } from \"lodash-es\";", errors: [{ messageId: "import", data: { module: "lodash-es" }, type: "ImportDeclaration" }] }, { - code: "import { merge } from \"lodash-es\";import _ from \"lodash-es\";", + code: "import { merge } from \"lodash-es\";\nimport _ from \"lodash-es\";", + errors: [{ messageId: "import", data: { module: "lodash-es" }, type: "ImportDeclaration" }] + }, + { + code: "import os from \"os\";\nimport { something } from \"os\";\nimport * as foobar from \"os\";", + errors: [ + { messageId: "import", data: { module: "os" }, type: "ImportDeclaration" }, + { messageId: "import", data: { module: "os" }, type: "ImportDeclaration" } + ] + }, + { + code: "import * as modns from \"lodash-es\";\nimport { merge } from \"lodash-es\";\nimport { baz } from \"lodash-es\";", errors: [{ messageId: "import", data: { module: "lodash-es" }, type: "ImportDeclaration" }] }, { @@ -66,7 +100,7 @@ ruleTester.run("no-duplicate-imports", rule, { errors: [{ messageId: "export", data: { module: "os" }, type: "ExportNamedDeclaration" }] }, { - code: "import os from \"os\"; export { os as foobar } from \"os\";\nexport { something } from \"os\";", + code: "import os from \"os\";\nexport { os as foobar } from \"os\";\nexport { something } from \"os\";", options: [{ includeExports: true }], errors: [ { messageId: "exportAs", data: { module: "os" }, type: "ExportNamedDeclaration" }, @@ -80,7 +114,27 @@ ruleTester.run("no-duplicate-imports", rule, { errors: [{ messageId: "exportAs", data: { module: "os" }, type: "ExportNamedDeclaration" }] }, { - code: "import os from \"os\";\nexport * from \"os\";", + code: "import os from \"os\";\nexport * as os from \"os\";", + options: [{ includeExports: true }], + errors: [{ messageId: "exportAs", data: { module: "os" }, type: "ExportAllDeclaration" }] + }, + { + code: "export * as os from \"os\";\nimport os from \"os\";", + options: [{ includeExports: true }], + errors: [{ messageId: "importAs", data: { module: "os" }, type: "ImportDeclaration" }] + }, + { + code: "import * as modns from \"mod\";\nexport * as modns from \"mod\";", + options: [{ includeExports: true }], + errors: [{ messageId: "exportAs", data: { module: "mod" }, type: "ExportAllDeclaration" }] + }, + { + code: "export * from \"os\";\nexport * from \"os\";", + options: [{ includeExports: true }], + errors: [{ messageId: "export", data: { module: "os" }, type: "ExportAllDeclaration" }] + }, + { + code: "import \"os\";\nexport * from \"os\";", options: [{ includeExports: true }], errors: [{ messageId: "exportAs", data: { module: "os" }, type: "ExportAllDeclaration" }] } From d709abfdde087325d4578b6709dc61040b8ca9d8 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 4 Jun 2021 18:08:07 +0200 Subject: [PATCH 25/67] Chore: fix comment location in no-unused-vars (#14648) --- lib/rules/no-unused-vars.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js index adf465905c2..f04818f8e9d 100644 --- a/lib/rules/no-unused-vars.js +++ b/lib/rules/no-unused-vars.js @@ -449,18 +449,24 @@ module.exports = { return ref.isRead() && ( // self update. e.g. `a += 1`, `a++` - (// in RHS of an assignment for itself. e.g. `a = a + 1` - (( + ( + ( parent.type === "AssignmentExpression" && - isUnusedExpression(parent) && - parent.left === id + parent.left === id && + isUnusedExpression(parent) ) || + ( + parent.type === "UpdateExpression" && + isUnusedExpression(parent) + ) + ) || + + // in RHS of an assignment for itself. e.g. `a = a + 1` ( - parent.type === "UpdateExpression" && - isUnusedExpression(parent) - ) || rhsNode && - isInside(id, rhsNode) && - !isInsideOfStorableFunction(id, rhsNode))) + rhsNode && + isInside(id, rhsNode) && + !isInsideOfStorableFunction(id, rhsNode) + ) ); } From 1f048cb0eec660d2052f1758f4b2ad7b1cb424e1 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 4 Jun 2021 18:27:34 +0200 Subject: [PATCH 26/67] Fix: no-implicit-coercion false positive with `String()` (fixes #14623) (#14641) --- lib/rules/no-implicit-coercion.js | 23 +++++++++++++++++++++-- tests/lib/rules/no-implicit-coercion.js | 11 ++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/rules/no-implicit-coercion.js b/lib/rules/no-implicit-coercion.js index b9bb4548986..993b8d1f1c8 100644 --- a/lib/rules/no-implicit-coercion.js +++ b/lib/rules/no-implicit-coercion.js @@ -109,6 +109,20 @@ function getNonNumericOperand(node) { return null; } +/** + * Checks whether an expression evaluates to a string. + * @param {ASTNode} node node that represents the expression to check. + * @returns {boolean} Whether or not the expression evaluates to a string. + */ +function isStringType(node) { + return astUtils.isStringLiteral(node) || + ( + node.type === "CallExpression" && + node.callee.type === "Identifier" && + node.callee.name === "String" + ); +} + /** * Checks whether a node is an empty string literal or not. * @param {ASTNode} node The node to check. @@ -126,8 +140,8 @@ function isEmptyString(node) { */ function isConcatWithEmptyString(node) { return node.operator === "+" && ( - (isEmptyString(node.left) && !astUtils.isStringLiteral(node.right)) || - (isEmptyString(node.right) && !astUtils.isStringLiteral(node.left)) + (isEmptyString(node.left) && !isStringType(node.right)) || + (isEmptyString(node.right) && !isStringType(node.left)) ); } @@ -332,6 +346,11 @@ module.exports = { return; } + // if the expression is already a string, then this isn't a coercion + if (isStringType(node.expressions[0])) { + return; + } + const code = sourceCode.getText(node.expressions[0]); const recommendation = `String(${code})`; diff --git a/tests/lib/rules/no-implicit-coercion.js b/tests/lib/rules/no-implicit-coercion.js index fa0a63824bb..f7ca9dcff3f 100644 --- a/tests/lib/rules/no-implicit-coercion.js +++ b/tests/lib/rules/no-implicit-coercion.js @@ -95,7 +95,16 @@ ruleTester.run("no-implicit-coercion", rule, { { code: "`${foo}`", parserOptions: { ecmaVersion: 6 } }, { code: "`${foo}`", options: [{ }], parserOptions: { ecmaVersion: 6 } }, { code: "`${foo}`", options: [{ disallowTemplateShorthand: false }], parserOptions: { ecmaVersion: 6 } }, - "+42" + "+42", + + // https://github.com/eslint/eslint/issues/14623 + "'' + String(foo)", + "String(foo) + ''", + { code: "`` + String(foo)", parserOptions: { ecmaVersion: 6 } }, + { code: "String(foo) + ``", parserOptions: { ecmaVersion: 6 } }, + { code: "`${'foo'}`", options: [{ disallowTemplateShorthand: true }], parserOptions: { ecmaVersion: 6 } }, + { code: "`${`foo`}`", options: [{ disallowTemplateShorthand: true }], parserOptions: { ecmaVersion: 6 } }, + { code: "`${String(foo)}`", options: [{ disallowTemplateShorthand: true }], parserOptions: { ecmaVersion: 6 } } ], invalid: [ { From 8d1e75a31b3e3d67130709a219bdd07ce6f3cf74 Mon Sep 17 00:00:00 2001 From: Hamza Najeeb <61828976+hamzanajeeb-tp@users.noreply.github.com> Date: Fri, 4 Jun 2021 21:36:45 +0500 Subject: [PATCH 27/67] Upgrade: glob-parent version in package.json (#14658) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 46c0860105a..cb95d5ed67d 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", + "glob-parent": "^5.1.2", "globals": "^13.6.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", From c5451635b4e89827cfc8d8d77083647c74506e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=9B=E5=AE=9A=E8=B0=94=E7=9A=84=E7=8C=AB?= Date: Sat, 5 Jun 2021 00:46:53 +0800 Subject: [PATCH 28/67] Update: support multiline /*eslint-env*/ directives (fixes #14652) (#14660) * Fix: linter ignores multiline /*eslint-env*/ directives (fixes #14652) * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic * Chore: add more tests Co-authored-by: Milos Djermanovic --- lib/linter/linter.js | 2 +- tests/lib/linter/linter.js | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 44e88f845fa..e94b507b5dd 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -444,7 +444,7 @@ function normalizeEcmaVersion(ecmaVersion) { return ecmaVersion >= 2015 ? ecmaVersion - 2009 : ecmaVersion; } -const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)\*\//gu; +const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)\*\//gsu; /** * Checks whether or not there is a comment which has "eslint-env *" in a given text. diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index bcf3edfb90c..e61804d3ae7 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -2973,6 +2973,23 @@ var a = "test2"; assert.strictEqual(messages.length, 0); }); + // https://github.com/eslint/eslint/issues/14652 + it("should not report a violation", () => { + const codes = [ + "/*eslint-env es6\n */ new Promise();", + "/*eslint-env browser,\nes6 */ window;Promise;", + "/*eslint-env\nbrowser,es6 */ window;Promise;" + ]; + const config = { rules: { "no-undef": 1 } }; + + for (const code of codes) { + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + } + + }); + it("should not report a violation", () => { const code = `/*${ESLINT_ENV} mocha,node */ require();describe();`; From 123fb8648731c2c23313c544ffa1872d3024fe68 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 4 Jun 2021 13:35:14 -0700 Subject: [PATCH 29/67] Docs: Add Feedback Needed triage description (#14670) --- docs/maintainer-guide/issues.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/maintainer-guide/issues.md b/docs/maintainer-guide/issues.md index 6273016bcdc..2624a4bda08 100644 --- a/docs/maintainer-guide/issues.md +++ b/docs/maintainer-guide/issues.md @@ -28,6 +28,7 @@ All of ESLint's issues, across all GitHub repositories, are managed on our [Tria * **Triaging** - issues that someone has reviewed but has not been able to fully triage yet * **Ready for Dev Team** - issues that have been triaged and have all of the information necessary for the dev team to take a look * **Evaluating** - the dev team is evaluating these issues to determine whether to move forward or not +* **Feedback Needed** - a team member is requesting more input from the rest of the team before proceeding * **Waiting for RFC** - the next step in the process is for an RFC to be written * **RFC Opened** - an RFC is opened to address these issues * **Blocked** - the issue can't move forward due to some dependency From 1237705dd08c209c5e3136045ec51a4ba87a3abe Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 5 Jun 2021 00:10:31 +0200 Subject: [PATCH 30/67] Upgrade: @eslint/eslintrc to 0.4.2 (#14672) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cb95d5ed67d..3468d626b3d 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "bugs": "https://github.com/eslint/eslint/issues/", "dependencies": { "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.1", + "@eslint/eslintrc": "^0.4.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", From 78d3e5dee494cbc28233e9480743db4779bd9a32 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 4 Jun 2021 18:33:23 -0400 Subject: [PATCH 31/67] Build: changelog update for 7.28.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54f33b6d442..6eaeb9a9a56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +v7.28.0 - June 4, 2021 + +* [`1237705`](https://github.com/eslint/eslint/commit/1237705dd08c209c5e3136045ec51a4ba87a3abe) Upgrade: @eslint/eslintrc to 0.4.2 (#14672) (Milos Djermanovic) +* [`123fb86`](https://github.com/eslint/eslint/commit/123fb8648731c2c23313c544ffa1872d3024fe68) Docs: Add Feedback Needed triage description (#14670) (Nicholas C. Zakas) +* [`c545163`](https://github.com/eslint/eslint/commit/c5451635b4e89827cfc8d8d77083647c74506e42) Update: support multiline /*eslint-env*/ directives (fixes #14652) (#14660) (薛定谔的猫) +* [`8d1e75a`](https://github.com/eslint/eslint/commit/8d1e75a31b3e3d67130709a219bdd07ce6f3cf74) Upgrade: glob-parent version in package.json (#14658) (Hamza Najeeb) +* [`1f048cb`](https://github.com/eslint/eslint/commit/1f048cb0eec660d2052f1758f4b2ad7b1cb424e1) Fix: no-implicit-coercion false positive with `String()` (fixes #14623) (#14641) (Milos Djermanovic) +* [`d709abf`](https://github.com/eslint/eslint/commit/d709abfdde087325d4578b6709dc61040b8ca9d8) Chore: fix comment location in no-unused-vars (#14648) (Milos Djermanovic) +* [`e44ce0a`](https://github.com/eslint/eslint/commit/e44ce0a8acfaad513c385150c25e76e82a1b8f12) Fix: no-duplicate-imports allow unmergeable (fixes #12758, fixes #12760) (#14238) (Soufiane Boutahlil) +* [`bb66a3d`](https://github.com/eslint/eslint/commit/bb66a3d91af426dac9a7ffdbe47bdbbc0ffd4dd7) New: add `getPhysicalFilename()` method to rule context (fixes #11989) (#14616) (Nitin Kumar) +* [`2e43dac`](https://github.com/eslint/eslint/commit/2e43dacd24337a82d4184fac9b44d497675f46ef) Docs: fix `no-sequences` example (#14643) (Nitin Kumar) +* [`958ff4e`](https://github.com/eslint/eslint/commit/958ff4e8a5102f204f1484d09985e28a79790996) Docs: add note for arrow functions in no-seq rule (#14578) (Nitin Kumar) +* [`e4f111b`](https://github.com/eslint/eslint/commit/e4f111b67d114adbf76a9c9dbb18fa4f49bc91b6) Fix: arrow-body-style crash with object pattern (fixes #14633) (#14635) (Milos Djermanovic) +* [`ec28b5a`](https://github.com/eslint/eslint/commit/ec28b5a2bdc69f34ce29d670f5e84d2446774a00) Chore: upgrade eslint-plugin-eslint-plugin (#14590) (薛定谔的猫) +* [`85a2725`](https://github.com/eslint/eslint/commit/85a2725b1fade5538e727102d9701ccb503e54d4) Docs: Update README team and sponsors (ESLint Jenkins) + v7.27.0 - May 21, 2021 * [`2c0868c`](https://github.com/eslint/eslint/commit/2c0868cbeadc9f42716fa1178ebdc6b4cee6d31e) Chore: merge all html formatter files into `html.js` (#14612) (Milos Djermanovic) From 55c1a503404744a2d88ddd553fce6498acea2f99 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 4 Jun 2021 18:33:23 -0400 Subject: [PATCH 32/67] 7.28.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3468d626b3d..601033eb44e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "7.27.0", + "version": "7.28.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From ddbe877c95224e127215d35562a175c6f2b7ba22 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Mon, 7 Jun 2021 13:11:47 -0400 Subject: [PATCH 33/67] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 438a087ff23..a53dd252380 100644 --- a/README.md +++ b/README.md @@ -283,7 +283,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Automattic

Gold Sponsors

Nx (by Nrwl) Chrome's Web Framework & Tools Performance Fund Salesforce Airbnb Substack

Silver Sponsors

Retool Liftoff

Bronze Sponsors

-

Anagram Solver Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Fire Stick Tricks

+

WritePaperFor.Me Anagram Solver Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Fire Stick Tricks

## Technology Sponsors From 8490fb42e559ef0b3c34ac60be4e05e0d879a9cb Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Mon, 7 Jun 2021 14:11:51 -0400 Subject: [PATCH 34/67] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a53dd252380..438a087ff23 100644 --- a/README.md +++ b/README.md @@ -283,7 +283,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Automattic

Gold Sponsors

Nx (by Nrwl) Chrome's Web Framework & Tools Performance Fund Salesforce Airbnb Substack

Silver Sponsors

Retool Liftoff

Bronze Sponsors

-

WritePaperFor.Me Anagram Solver Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Fire Stick Tricks

+

Anagram Solver Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Fire Stick Tricks

## Technology Sponsors From e2bed2ead22b575d55ccaeed94eecd3a979dd871 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Wed, 9 Jun 2021 19:11:56 -0400 Subject: [PATCH 35/67] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 438a087ff23..fa7867aaa74 100644 --- a/README.md +++ b/README.md @@ -281,7 +281,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Platinum Sponsors

Automattic

Gold Sponsors

-

Nx (by Nrwl) Chrome's Web Framework & Tools Performance Fund Salesforce Airbnb Substack

Silver Sponsors

+

Nx (by Nrwl) Chrome's Web Framework & Tools Performance Fund Salesforce Airbnb Coinbase Substack

Silver Sponsors

Retool Liftoff

Bronze Sponsors

Anagram Solver Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Fire Stick Tricks

From 655c1187fc845bac61ae8d06c556f1a59ee2071b Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 11 Jun 2021 00:11:48 -0400 Subject: [PATCH 36/67] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa7867aaa74..2202900d194 100644 --- a/README.md +++ b/README.md @@ -283,7 +283,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Automattic

Gold Sponsors

Nx (by Nrwl) Chrome's Web Framework & Tools Performance Fund Salesforce Airbnb Coinbase Substack

Silver Sponsors

Retool Liftoff

Bronze Sponsors

-

Anagram Solver Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Fire Stick Tricks

+

Anagram Solver Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Fire Stick Tricks Practice Ignition

## Technology Sponsors From c93a222563177a9b5bc7a59aa106bc0a6d31e063 Mon Sep 17 00:00:00 2001 From: Sam Chen Date: Fri, 11 Jun 2021 13:43:12 +0800 Subject: [PATCH 37/67] Docs: fix a broken link (#14697) --- docs/developer-guide/working-with-rules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md index 9dadae00e50..6a019aff199 100644 --- a/docs/developer-guide/working-with-rules.md +++ b/docs/developer-guide/working-with-rules.md @@ -66,7 +66,7 @@ The source file for a rule exports an object with the following properties. In a custom rule or plugin, you can omit `docs` or include any properties that you need in it. -* `fixable` (string) is either `"code"` or `"whitespace"` if the `--fix` option on the [command line](../user-guide/command-line-interface.md#fix) automatically fixes problems reported by the rule +* `fixable` (string) is either `"code"` or `"whitespace"` if the `--fix` option on the [command line](../user-guide/command-line-interface.md#-fix) automatically fixes problems reported by the rule **Important:** the `fixable` property is mandatory for fixable rules. If this property isn't specified, ESLint will throw an error whenever the rule attempts to produce a fix. Omit the `fixable` property if the rule is not fixable. From 831f6b30270a37800e61e6c668bfa71a39064d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=9B=E5=AE=9A=E8=B0=94=E7=9A=84=E7=8C=AB?= Date: Sat, 12 Jun 2021 08:45:15 +0800 Subject: [PATCH 38/67] Update: ecmaVersion defaults to 5, and allows "latest" (#14622) * Update: set default ecmaVersion => 5 and allows "latest" * chore: add more tests --- .../configuring/language-options.md | 4 +- lib/linter/linter.js | 31 +++++---- tests/lib/linter/linter.js | 65 +++++++++++++++++++ 3 files changed, 86 insertions(+), 14 deletions(-) diff --git a/docs/user-guide/configuring/language-options.md b/docs/user-guide/configuring/language-options.md index eb3fe8a0afe..08b62aad571 100644 --- a/docs/user-guide/configuring/language-options.md +++ b/docs/user-guide/configuring/language-options.md @@ -187,7 +187,7 @@ For ES6 syntax, use `{ "parserOptions": { "ecmaVersion": 6 } }`; for new ES6 glo Parser options are set in your `.eslintrc.*` file by using the `parserOptions` property. The available options are: -* `ecmaVersion` - set to 3, 5 (default), 6, 7, 8, 9, 10, 11, or 12 to specify the version of ECMAScript syntax you want to use. You can also set to 2015 (same as 6), 2016 (same as 7), 2017 (same as 8), 2018 (same as 9), 2019 (same as 10), 2020 (same as 11), or 2021 (same as 12) to use the year-based naming. +* `ecmaVersion` - set to 3, 5 (default), 6, 7, 8, 9, 10, 11, or 12 to specify the version of ECMAScript syntax you want to use. You can also set to 2015 (same as 6), 2016 (same as 7), 2017 (same as 8), 2018 (same as 9), 2019 (same as 10), 2020 (same as 11), or 2021 (same as 12) to use the year-based naming. You can also set "latest" to use the most recently supported version. * `sourceType` - set to `"script"` (default) or `"module"` if your code is in ECMAScript modules. * `ecmaFeatures` - an object indicating which additional language features you'd like to use: * `globalReturn` - allow `return` statements in the global scope @@ -199,7 +199,7 @@ Here's an example `.eslintrc.json` file: ```json { "parserOptions": { - "ecmaVersion": 6, + "ecmaVersion": "latest", "sourceType": "module", "ecmaFeatures": { "jsx": true diff --git a/lib/linter/linter.js b/lib/linter/linter.js index e94b507b5dd..77682ea9548 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -37,6 +37,7 @@ const const debug = require("debug")("eslint:linter"); const MAX_AUTOFIX_PASSES = 10; const DEFAULT_PARSER_NAME = "espree"; +const DEFAULT_ECMA_VERSION = 5; const commentParser = new ConfigCommentParser(); const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }; @@ -432,10 +433,20 @@ function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) { /** * Normalize ECMAScript version from the initial config - * @param {number} ecmaVersion ECMAScript version from the initial config + * @param {Parser} parser The parser which uses this options. + * @param {number} ecmaVersion ECMAScript version from the initial config * @returns {number} normalized ECMAScript version */ -function normalizeEcmaVersion(ecmaVersion) { +function normalizeEcmaVersion(parser, ecmaVersion) { + if (parser === espree) { + if (ecmaVersion === void 0) { + return DEFAULT_ECMA_VERSION; + } + + if (ecmaVersion === "latest") { + return espree.latestEcmaVersion; + } + } /* * Calculate ECMAScript edition number from official year version starting with @@ -521,12 +532,13 @@ function normalizeVerifyOptions(providedOptions, config) { /** * Combines the provided parserOptions with the options from environments - * @param {string} parserName The parser name which uses this options. + * @param {Parser} parser The parser which uses this options. * @param {ParserOptions} providedOptions The provided 'parserOptions' key in a config * @param {Environment[]} enabledEnvironments The environments enabled in configuration and with inline comments * @returns {ParserOptions} Resulting parser options after merge */ -function resolveParserOptions(parserName, providedOptions, enabledEnvironments) { +function resolveParserOptions(parser, providedOptions, enabledEnvironments) { + const parserOptionsFromEnv = enabledEnvironments .filter(env => env.parserOptions) .reduce((parserOptions, env) => merge(parserOptions, env.parserOptions), {}); @@ -542,12 +554,7 @@ function resolveParserOptions(parserName, providedOptions, enabledEnvironments) mergedParserOptions.ecmaFeatures = Object.assign({}, mergedParserOptions.ecmaFeatures, { globalReturn: false }); } - /* - * TODO: @aladdin-add - * 1. for a 3rd-party parser, do not normalize parserOptions - * 2. for espree, no need to do this (espree will do it) - */ - mergedParserOptions.ecmaVersion = normalizeEcmaVersion(mergedParserOptions.ecmaVersion); + mergedParserOptions.ecmaVersion = normalizeEcmaVersion(parser, mergedParserOptions.ecmaVersion); return mergedParserOptions; } @@ -606,7 +613,7 @@ function getRuleOptions(ruleConfig) { */ function analyzeScope(ast, parserOptions, visitorKeys) { const ecmaFeatures = parserOptions.ecmaFeatures || {}; - const ecmaVersion = parserOptions.ecmaVersion || 5; + const ecmaVersion = parserOptions.ecmaVersion || DEFAULT_ECMA_VERSION; return eslintScope.analyze(ast, { ignoreEval: true, @@ -1123,7 +1130,7 @@ class Linter { .map(envName => getEnv(slots, envName)) .filter(env => env); - const parserOptions = resolveParserOptions(parserName, config.parserOptions || {}, enabledEnvs); + const parserOptions = resolveParserOptions(parser, config.parserOptions || {}, enabledEnvs); const configuredGlobals = resolveGlobals(config.globals || {}, enabledEnvs); const settings = config.settings || {}; diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index e61804d3ae7..15d310c9ab9 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -11,6 +11,7 @@ const assert = require("chai").assert, sinon = require("sinon"), + espree = require("espree"), esprima = require("esprima"), testParsers = require("../../fixtures/parsers/linter-test-parsers"); @@ -3493,6 +3494,70 @@ var a = "test2"; }); describe("ecmaVersion", () => { + + it("should not support ES6 when no ecmaVersion provided", () => { + const messages = linter.verify("let x = 0;"); + + assert.strictEqual(messages.length, 1); + }); + + it("the default ECMAScript version is 5", () => { + let ecmaVersion = null; + const config = { rules: { "ecma-version": 2 } }; + + linter.defineRule("ecma-version", context => ({ + Program() { + ecmaVersion = context.parserOptions.ecmaVersion; + } + })); + linter.verify("", config); + assert.strictEqual(ecmaVersion, 5); + }); + + it("supports ECMAScript version 'latest'", () => { + const messages = linter.verify("let x = 5 ** 7;", { + parserOptions: { ecmaVersion: "latest" } + }); + + assert.strictEqual(messages.length, 0); + }); + + it("the 'latest' is equal to espree.lastEcmaVersion", () => { + let ecmaVersion = null; + const config = { rules: { "ecma-version": 2 }, parserOptions: { ecmaVersion: "latest" } }; + + linter.defineRule("ecma-version", context => ({ + Program() { + ecmaVersion = context.parserOptions.ecmaVersion; + } + })); + linter.verify("", config); + assert.strictEqual(ecmaVersion, espree.latestEcmaVersion); + }); + + it("should pass normalized ecmaVersion to eslint-scope", () => { + let blockScope = null; + + linter.defineRule("block-scope", context => ({ + BlockStatement() { + blockScope = context.getScope(); + } + })); + + linter.verify("{}", { + rules: { "block-scope": 2 }, + parserOptions: { ecmaVersion: "latest" } + }); + + assert.strictEqual(blockScope.type, "block"); + + linter.verify("{}", { + rules: { "block-scope": 2 }, + parserOptions: {} // ecmaVersion defaults to 5 + }); + assert.strictEqual(blockScope.type, "global"); + }); + describe("it should properly parse let declaration when", () => { it("the ECMAScript version number is 6", () => { const messages = linter.verify("let x = 5;", { From 757c49584a5852c468c1b4a0b74ad3aa39d954e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=9B=E5=AE=9A=E8=B0=94=E7=9A=84=E7=8C=AB?= Date: Sat, 12 Jun 2021 22:21:31 +0800 Subject: [PATCH 39/67] Chore: add some rules to eslint-config-eslint (#14692) * Chore: add some rules to eslint-config-eslint note: this is a breaking for eslint-config-eslint * chore: fix some linting problems * Update lib/eslint/eslint.js Co-authored-by: Milos Djermanovic * chore: update tests Co-authored-by: Milos Djermanovic --- lib/eslint/eslint.js | 7 +++++-- lib/init/npm-utils.js | 3 +-- lib/rule-tester/rule-tester.js | 16 +++++++++++----- lib/rules/comma-style.js | 3 +-- lib/rules/indent.js | 6 ++---- packages/eslint-config-eslint/default.yml | 6 ++++++ tests/lib/eslint/eslint.js | 2 +- 7 files changed, 27 insertions(+), 16 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index ae2d2100861..c387ca72c3a 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -552,9 +552,12 @@ class ESLint { ...unknownOptions } = options || {}; - for (const key of Object.keys(unknownOptions)) { - throw new Error(`'options' must not include the unknown option '${key}'`); + const unknownOptionKeys = Object.keys(unknownOptions); + + if (unknownOptionKeys.length > 0) { + throw new Error(`'options' must not include the unknown option(s): ${unknownOptionKeys.join(", ")}`); } + if (filePath !== void 0 && !isNonEmptyString(filePath)) { throw new Error("'options.filePath' must be a non-empty string or undefined"); } diff --git a/lib/init/npm-utils.js b/lib/init/npm-utils.js index 35191cc0876..b91a824b126 100644 --- a/lib/init/npm-utils.js +++ b/lib/init/npm-utils.js @@ -50,8 +50,7 @@ function findPackageJson(startDir) { */ function installSyncSaveDev(packages) { const packageList = Array.isArray(packages) ? packages : [packages]; - const npmProcess = spawn.sync("npm", ["i", "--save-dev"].concat(packageList), - { stdio: "inherit" }); + const npmProcess = spawn.sync("npm", ["i", "--save-dev"].concat(packageList), { stdio: "inherit" }); const error = npmProcess.error; if (error && error.code === "ENOENT") { diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index b08303c62b2..f177ae3cc17 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -610,7 +610,8 @@ class RuleTester { const messages = result.messages; assert.strictEqual(messages.length, 0, util.format("Should have no errors but had %d: %s", - messages.length, util.inspect(messages))); + messages.length, + util.inspect(messages))); assertASTDidntChange(result.beforeAST, result.afterAST); } @@ -665,13 +666,18 @@ class RuleTester { } assert.strictEqual(messages.length, item.errors, util.format("Should have %d error%s but had %d: %s", - item.errors, item.errors === 1 ? "" : "s", messages.length, util.inspect(messages))); + item.errors, + item.errors === 1 ? "" : "s", + messages.length, + util.inspect(messages))); } else { assert.strictEqual( - messages.length, item.errors.length, - util.format( + messages.length, item.errors.length, util.format( "Should have %d error%s but had %d: %s", - item.errors.length, item.errors.length === 1 ? "" : "s", messages.length, util.inspect(messages) + item.errors.length, + item.errors.length === 1 ? "" : "s", + messages.length, + util.inspect(messages) ) ); diff --git a/lib/rules/comma-style.js b/lib/rules/comma-style.js index f1a23d63b78..fbdecccaac2 100644 --- a/lib/rules/comma-style.js +++ b/lib/rules/comma-style.js @@ -207,8 +207,7 @@ module.exports = { * they are always valid regardless of an undefined item. */ if (astUtils.isCommaToken(commaToken)) { - validateCommaItemSpacing(previousItemToken, commaToken, - currentItemToken, reportItem); + validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem); } if (item) { diff --git a/lib/rules/indent.js b/lib/rules/indent.js index b1af2a73b33..04f41db9e26 100644 --- a/lib/rules/indent.js +++ b/lib/rules/indent.js @@ -1177,8 +1177,7 @@ module.exports = { offsets.setDesiredOffset(questionMarkToken, firstToken, 1); offsets.setDesiredOffset(colonToken, firstToken, 1); - offsets.setDesiredOffset(firstConsequentToken, firstToken, - firstConsequentToken.type === "Punctuator" && + offsets.setDesiredOffset(firstConsequentToken, firstToken, firstConsequentToken.type === "Punctuator" && options.offsetTernaryExpressions ? 2 : 1); /* @@ -1204,8 +1203,7 @@ module.exports = { * If `baz` were aligned with `bar` rather than being offset by 1 from `foo`, `baz` would end up * having no expected indentation. */ - offsets.setDesiredOffset(firstAlternateToken, firstToken, - firstAlternateToken.type === "Punctuator" && + offsets.setDesiredOffset(firstAlternateToken, firstToken, firstAlternateToken.type === "Punctuator" && options.offsetTernaryExpressions ? 2 : 1); } } diff --git a/packages/eslint-config-eslint/default.yml b/packages/eslint-config-eslint/default.yml index 8a85313045c..d2b31d9ed77 100644 --- a/packages/eslint-config-eslint/default.yml +++ b/packages/eslint-config-eslint/default.yml @@ -29,6 +29,7 @@ rules: consistent-return: "error" curly: ["error", "all"] default-case: "error" + default-case-last: "error" default-param-last: "error" dot-location: ["error", "property"] dot-notation: ["error", { allowKeywords: true }] @@ -36,8 +37,10 @@ rules: eqeqeq: "error" func-call-spacing: "error" func-style: ["error", "declaration"] + function-call-argument-newline: ["error", "consistent"] function-paren-newline: ["error", "consistent"] generator-star-spacing: "error" + grouped-accessor-pairs: "error" guard-for-in: "error" jsdoc/check-alignment: "error" jsdoc/check-param-names: "error" @@ -79,6 +82,7 @@ rules: no-caller: "error" no-confusing-arrow: "error" no-console: "error" + no-constructor-return: "error" no-delete-var: "error" no-else-return: ["error", { allowElseIf: false }] no-eval: "error" @@ -133,6 +137,7 @@ rules: no-underscore-dangle: ["error", {allowAfterThis: true}] no-unmodified-loop-condition: "error" no-unneeded-ternary: "error" + no-unreachable-loop: "error" no-unused-expressions: "error" no-unused-vars: ["error", {vars: "all", args: "after-used", caughtErrors: "all"}] no-use-before-define: "error" @@ -174,6 +179,7 @@ rules: ] prefer-arrow-callback: "error" prefer-const: "error" + prefer-exponentiation-operator: "error" prefer-numeric-literals: "error" prefer-promise-reject-errors: "error" prefer-regex-literals: "error" diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index c7783a6c059..0845d697083 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -846,7 +846,7 @@ describe("ESLint", () => { it("should throw if 'options' argument contains unknown key", async () => { eslint = new ESLint(); - await assert.rejects(() => eslint.lintText("var a = 0", { filename: "foo.js" }), /'options' must not include the unknown option 'filename'/u); + await assert.rejects(() => eslint.lintText("var a = 0", { filename: "foo.js" }), /'options' must not include the unknown option\(s\): filename/u); }); it("should throw if non-string value is given to 'options.filePath' option", async () => { From 353ddf965078030794419b089994373e27ffc86e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=9B=E5=AE=9A=E8=B0=94=E7=9A=84=E7=8C=AB?= Date: Tue, 15 Jun 2021 22:55:34 +0800 Subject: [PATCH 40/67] Chore: enable reportUnusedDisableDirectives in eslint-config-eslint (#14699) * Chore: enable reportUnusedDisableDirectives in eslint-config-eslint it can be applied to all eslint repos * Update Makefile.js Co-authored-by: Milos Djermanovic Co-authored-by: Milos Djermanovic --- packages/eslint-config-eslint/default.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-config-eslint/default.yml b/packages/eslint-config-eslint/default.yml index d2b31d9ed77..38061e80155 100644 --- a/packages/eslint-config-eslint/default.yml +++ b/packages/eslint-config-eslint/default.yml @@ -1,3 +1,4 @@ +reportUnusedDisableDirectives: true extends: - "eslint:recommended" - "plugin:node/recommended" From a47e5e30b0da364593b6881f6826c595da8696f5 Mon Sep 17 00:00:00 2001 From: Nicolas Vuillamy Date: Thu, 17 Jun 2021 08:17:30 +0200 Subject: [PATCH 41/67] Docs: Add Mega-Linter to the list of integrations (#14707) --- docs/user-guide/integrations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/user-guide/integrations.md b/docs/user-guide/integrations.md index c7130bb9f80..34ad2e25b95 100644 --- a/docs/user-guide/integrations.md +++ b/docs/user-guide/integrations.md @@ -44,6 +44,7 @@ * [Git Precommit Hook](https://coderwall.com/p/zq8jlq/eslint-pre-commit-hook) * [Git pre-commit hook that only lints staged changes](https://gist.github.com/dahjelle/8ddedf0aebd488208a9a7c829f19b9e8) * [overcommit Git hook manager](https://github.com/brigade/overcommit) +* [Mega-Linter](https://nvuillam.github.io/mega-linter): Linters aggregator for CI, [embedding eslint](https://nvuillam.github.io/mega-linter/descriptors/javascript_eslint/) ## Testing From 97d9bd2a8061e61e98ebabb4c41231af1df7629f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=9B=E5=AE=9A=E8=B0=94=E7=9A=84=E7=8C=AB?= Date: Fri, 18 Jun 2021 13:39:56 +0800 Subject: [PATCH 42/67] Revert "Update: ecmaVersion defaults to 5, and allows "latest" (#14622)" (#14711) This reverts commit 831f6b30270a37800e61e6c668bfa71a39064d2e. --- .../configuring/language-options.md | 4 +- lib/linter/linter.js | 31 ++++----- tests/lib/linter/linter.js | 65 ------------------- 3 files changed, 14 insertions(+), 86 deletions(-) diff --git a/docs/user-guide/configuring/language-options.md b/docs/user-guide/configuring/language-options.md index 08b62aad571..eb3fe8a0afe 100644 --- a/docs/user-guide/configuring/language-options.md +++ b/docs/user-guide/configuring/language-options.md @@ -187,7 +187,7 @@ For ES6 syntax, use `{ "parserOptions": { "ecmaVersion": 6 } }`; for new ES6 glo Parser options are set in your `.eslintrc.*` file by using the `parserOptions` property. The available options are: -* `ecmaVersion` - set to 3, 5 (default), 6, 7, 8, 9, 10, 11, or 12 to specify the version of ECMAScript syntax you want to use. You can also set to 2015 (same as 6), 2016 (same as 7), 2017 (same as 8), 2018 (same as 9), 2019 (same as 10), 2020 (same as 11), or 2021 (same as 12) to use the year-based naming. You can also set "latest" to use the most recently supported version. +* `ecmaVersion` - set to 3, 5 (default), 6, 7, 8, 9, 10, 11, or 12 to specify the version of ECMAScript syntax you want to use. You can also set to 2015 (same as 6), 2016 (same as 7), 2017 (same as 8), 2018 (same as 9), 2019 (same as 10), 2020 (same as 11), or 2021 (same as 12) to use the year-based naming. * `sourceType` - set to `"script"` (default) or `"module"` if your code is in ECMAScript modules. * `ecmaFeatures` - an object indicating which additional language features you'd like to use: * `globalReturn` - allow `return` statements in the global scope @@ -199,7 +199,7 @@ Here's an example `.eslintrc.json` file: ```json { "parserOptions": { - "ecmaVersion": "latest", + "ecmaVersion": 6, "sourceType": "module", "ecmaFeatures": { "jsx": true diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 77682ea9548..e94b507b5dd 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -37,7 +37,6 @@ const const debug = require("debug")("eslint:linter"); const MAX_AUTOFIX_PASSES = 10; const DEFAULT_PARSER_NAME = "espree"; -const DEFAULT_ECMA_VERSION = 5; const commentParser = new ConfigCommentParser(); const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }; @@ -433,20 +432,10 @@ function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) { /** * Normalize ECMAScript version from the initial config - * @param {Parser} parser The parser which uses this options. - * @param {number} ecmaVersion ECMAScript version from the initial config + * @param {number} ecmaVersion ECMAScript version from the initial config * @returns {number} normalized ECMAScript version */ -function normalizeEcmaVersion(parser, ecmaVersion) { - if (parser === espree) { - if (ecmaVersion === void 0) { - return DEFAULT_ECMA_VERSION; - } - - if (ecmaVersion === "latest") { - return espree.latestEcmaVersion; - } - } +function normalizeEcmaVersion(ecmaVersion) { /* * Calculate ECMAScript edition number from official year version starting with @@ -532,13 +521,12 @@ function normalizeVerifyOptions(providedOptions, config) { /** * Combines the provided parserOptions with the options from environments - * @param {Parser} parser The parser which uses this options. + * @param {string} parserName The parser name which uses this options. * @param {ParserOptions} providedOptions The provided 'parserOptions' key in a config * @param {Environment[]} enabledEnvironments The environments enabled in configuration and with inline comments * @returns {ParserOptions} Resulting parser options after merge */ -function resolveParserOptions(parser, providedOptions, enabledEnvironments) { - +function resolveParserOptions(parserName, providedOptions, enabledEnvironments) { const parserOptionsFromEnv = enabledEnvironments .filter(env => env.parserOptions) .reduce((parserOptions, env) => merge(parserOptions, env.parserOptions), {}); @@ -554,7 +542,12 @@ function resolveParserOptions(parser, providedOptions, enabledEnvironments) { mergedParserOptions.ecmaFeatures = Object.assign({}, mergedParserOptions.ecmaFeatures, { globalReturn: false }); } - mergedParserOptions.ecmaVersion = normalizeEcmaVersion(parser, mergedParserOptions.ecmaVersion); + /* + * TODO: @aladdin-add + * 1. for a 3rd-party parser, do not normalize parserOptions + * 2. for espree, no need to do this (espree will do it) + */ + mergedParserOptions.ecmaVersion = normalizeEcmaVersion(mergedParserOptions.ecmaVersion); return mergedParserOptions; } @@ -613,7 +606,7 @@ function getRuleOptions(ruleConfig) { */ function analyzeScope(ast, parserOptions, visitorKeys) { const ecmaFeatures = parserOptions.ecmaFeatures || {}; - const ecmaVersion = parserOptions.ecmaVersion || DEFAULT_ECMA_VERSION; + const ecmaVersion = parserOptions.ecmaVersion || 5; return eslintScope.analyze(ast, { ignoreEval: true, @@ -1130,7 +1123,7 @@ class Linter { .map(envName => getEnv(slots, envName)) .filter(env => env); - const parserOptions = resolveParserOptions(parser, config.parserOptions || {}, enabledEnvs); + const parserOptions = resolveParserOptions(parserName, config.parserOptions || {}, enabledEnvs); const configuredGlobals = resolveGlobals(config.globals || {}, enabledEnvs); const settings = config.settings || {}; diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 15d310c9ab9..e61804d3ae7 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -11,7 +11,6 @@ const assert = require("chai").assert, sinon = require("sinon"), - espree = require("espree"), esprima = require("esprima"), testParsers = require("../../fixtures/parsers/linter-test-parsers"); @@ -3494,70 +3493,6 @@ var a = "test2"; }); describe("ecmaVersion", () => { - - it("should not support ES6 when no ecmaVersion provided", () => { - const messages = linter.verify("let x = 0;"); - - assert.strictEqual(messages.length, 1); - }); - - it("the default ECMAScript version is 5", () => { - let ecmaVersion = null; - const config = { rules: { "ecma-version": 2 } }; - - linter.defineRule("ecma-version", context => ({ - Program() { - ecmaVersion = context.parserOptions.ecmaVersion; - } - })); - linter.verify("", config); - assert.strictEqual(ecmaVersion, 5); - }); - - it("supports ECMAScript version 'latest'", () => { - const messages = linter.verify("let x = 5 ** 7;", { - parserOptions: { ecmaVersion: "latest" } - }); - - assert.strictEqual(messages.length, 0); - }); - - it("the 'latest' is equal to espree.lastEcmaVersion", () => { - let ecmaVersion = null; - const config = { rules: { "ecma-version": 2 }, parserOptions: { ecmaVersion: "latest" } }; - - linter.defineRule("ecma-version", context => ({ - Program() { - ecmaVersion = context.parserOptions.ecmaVersion; - } - })); - linter.verify("", config); - assert.strictEqual(ecmaVersion, espree.latestEcmaVersion); - }); - - it("should pass normalized ecmaVersion to eslint-scope", () => { - let blockScope = null; - - linter.defineRule("block-scope", context => ({ - BlockStatement() { - blockScope = context.getScope(); - } - })); - - linter.verify("{}", { - rules: { "block-scope": 2 }, - parserOptions: { ecmaVersion: "latest" } - }); - - assert.strictEqual(blockScope.type, "block"); - - linter.verify("{}", { - rules: { "block-scope": 2 }, - parserOptions: {} // ecmaVersion defaults to 5 - }); - assert.strictEqual(blockScope.type, "global"); - }); - describe("it should properly parse let declaration when", () => { it("the ECMAScript version number is 6", () => { const messages = linter.verify("let x = 5;", { From 6a1c7a0dac050ea5876972c50563a7eb867b38d3 Mon Sep 17 00:00:00 2001 From: Kevin Gibbons Date: Fri, 18 Jun 2021 12:42:22 -0700 Subject: [PATCH 43/67] Fix: allow fallthrough comment inside block (fixes #14701) (#14702) * Fix: allow fallthrough comment inside block (fixes #14701) * address comments --- docs/rules/no-fallthrough.md | 22 +++++++++++++++ lib/rules/no-fallthrough.js | 23 ++++++++++++---- tests/lib/rules/no-fallthrough.js | 46 +++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/docs/rules/no-fallthrough.md b/docs/rules/no-fallthrough.md index 15ca77f661b..81b428cf070 100644 --- a/docs/rules/no-fallthrough.md +++ b/docs/rules/no-fallthrough.md @@ -54,6 +54,17 @@ switch(foo) { case 2: doSomethingElse(); } + +switch(foo) { + case 1: { + doSomething(); + // falls through + } + + case 2: { + doSomethingElse(); + } +} ``` In this example, there is no confusion as to the expected behavior. It is clear that the first case is meant to fall through to the second case. @@ -124,6 +135,17 @@ switch(foo) { case 2: doSomething(); } + +switch(foo) { + case 1: { + doSomething(); + // falls through + } + + case 2: { + doSomethingElse(); + } +} ``` Note that the last `case` statement in these examples does not cause a warning because there is nothing to fall through into. diff --git a/lib/rules/no-fallthrough.js b/lib/rules/no-fallthrough.js index e8016e93e59..3b949acd1da 100644 --- a/lib/rules/no-fallthrough.js +++ b/lib/rules/no-fallthrough.js @@ -11,15 +11,26 @@ const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/iu; /** - * Checks whether or not a given node has a fallthrough comment. - * @param {ASTNode} node A SwitchCase node to get comments. + * Checks whether or not a given case has a fallthrough comment. + * @param {ASTNode} caseWhichFallsThrough SwitchCase node which falls through. + * @param {ASTNode} subsequentCase The case after caseWhichFallsThrough. * @param {RuleContext} context A rule context which stores comments. * @param {RegExp} fallthroughCommentPattern A pattern to match comment to. - * @returns {boolean} `true` if the node has a valid fallthrough comment. + * @returns {boolean} `true` if the case has a valid fallthrough comment. */ -function hasFallthroughComment(node, context, fallthroughCommentPattern) { +function hasFallthroughComment(caseWhichFallsThrough, subsequentCase, context, fallthroughCommentPattern) { const sourceCode = context.getSourceCode(); - const comment = sourceCode.getCommentsBefore(node).pop(); + + if (caseWhichFallsThrough.consequent.length === 1 && caseWhichFallsThrough.consequent[0].type === "BlockStatement") { + const trailingCloseBrace = sourceCode.getLastToken(caseWhichFallsThrough.consequent[0]); + const commentInBlock = sourceCode.getCommentsBefore(trailingCloseBrace).pop(); + + if (commentInBlock && fallthroughCommentPattern.test(commentInBlock.value)) { + return true; + } + } + + const comment = sourceCode.getCommentsBefore(subsequentCase).pop(); return Boolean(comment && fallthroughCommentPattern.test(comment.value)); } @@ -108,7 +119,7 @@ module.exports = { * Checks whether or not there is a fallthrough comment. * And reports the previous fallthrough node if that does not exist. */ - if (fallthroughCase && !hasFallthroughComment(node, context, fallthroughCommentPattern)) { + if (fallthroughCase && !hasFallthroughComment(fallthroughCase, node, context, fallthroughCommentPattern)) { context.report({ messageId: node.test ? "case" : "default", node diff --git a/tests/lib/rules/no-fallthrough.js b/tests/lib/rules/no-fallthrough.js index e154c31d5f7..0b98d3293e9 100644 --- a/tests/lib/rules/no-fallthrough.js +++ b/tests/lib/rules/no-fallthrough.js @@ -30,6 +30,14 @@ ruleTester.run("no-fallthrough", rule, { "switch(foo) { case 0: a(); /* fall through */ case 1: b(); }", "switch(foo) { case 0: a(); /* fallthrough */ case 1: b(); }", "switch(foo) { case 0: a(); /* FALLS THROUGH */ case 1: b(); }", + "switch(foo) { case 0: { a(); /* falls through */ } case 1: b(); }", + "switch(foo) { case 0: { a()\n /* falls through */ } case 1: b(); }", + "switch(foo) { case 0: { a(); /* fall through */ } case 1: b(); }", + "switch(foo) { case 0: { a(); /* fallthrough */ } case 1: b(); }", + "switch(foo) { case 0: { a(); /* FALLS THROUGH */ } case 1: b(); }", + "switch(foo) { case 0: { a(); } /* falls through */ case 1: b(); }", + "switch(foo) { case 0: { a(); /* falls through */ } /* comment */ case 1: b(); }", + "switch(foo) { case 0: { /* falls through */ } case 1: b(); }", "function foo() { switch(foo) { case 0: a(); return; case 1: b(); }; }", "switch(foo) { case 0: a(); throw 'foo'; case 1: b(); }", "while (a) { switch(foo) { case 0: a(); continue; case 1: b(); } }", @@ -133,6 +141,30 @@ ruleTester.run("no-fallthrough", rule, { code: "switch(foo) { case 0:\n\n default: b() }", errors: errorsDefault }, + { + code: "switch(foo) { case 0: {} default: b() }", + errors: errorsDefault + }, + { + code: "switch(foo) { case 0: a(); { /* falls through */ } default: b() }", + errors: errorsDefault + }, + { + code: "switch(foo) { case 0: { /* falls through */ } a(); default: b() }", + errors: errorsDefault + }, + { + code: "switch(foo) { case 0: if (a) { /* falls through */ } default: b() }", + errors: errorsDefault + }, + { + code: "switch(foo) { case 0: { { /* falls through */ } } default: b() }", + errors: errorsDefault + }, + { + code: "switch(foo) { case 0: { /* comment */ } default: b() }", + errors: errorsDefault + }, { code: "switch(foo) { case 0:\n // comment\n default: b() }", errors: errorsDefault @@ -168,6 +200,20 @@ ruleTester.run("no-fallthrough", rule, { column: 1 } ] + }, + { + code: "switch(foo) { case 0: { a();\n/* no break */\n/* todo: fix readability */ }\ndefault: b() }", + options: [{ + commentPattern: "no break" + }], + errors: [ + { + messageId: "default", + type: "SwitchCase", + line: 4, + column: 1 + } + ] } ] }); From eea7e0d09d6ef43d6663cbe424e7974764a5f7fe Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Sat, 19 Jun 2021 01:20:34 +0530 Subject: [PATCH 44/67] Chore: remove duplicate code (#14719) --- tests/lib/linter/linter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index e61804d3ae7..adce5a39d86 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -3401,7 +3401,6 @@ var a = "test2"; return {}; }); - linter.defineRule("checker", filenameChecker); linter.defineRule("checker", filenameChecker); linter.verify("foo;", { rules: { checker: "error" } }, { filename: "foo.js" }); assert(filenameChecker.calledOnce); From c2cd7b4a18057ca6067bdfc16de771dc5d90c0ea Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 18 Jun 2021 12:57:51 -0700 Subject: [PATCH 45/67] New: Add ESLint#getRulesMetaForResults() (refs #13654) (#14716) * New: Add ESLint#getRulesMetaForResults() (refs #13654) * Update docs * Update docs/developer-guide/nodejs-api.md Co-authored-by: Brandon Mills Co-authored-by: Brandon Mills --- docs/developer-guide/nodejs-api.md | 21 +++++++++ lib/eslint/eslint.js | 33 +++++++++++++ tests/lib/eslint/eslint.js | 75 ++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index f1e21584e32..28b0fff9070 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -10,6 +10,7 @@ While ESLint is designed to be run on the command line, it's possible to use ESL * [constructor()][eslint-constructor] * [lintFiles()][eslint-lintfiles] * [lintText()][eslint-linttext] + * [getRulesMetaForResults()](eslint-getrulesmetaforresults) * [calculateConfigForFile()][eslint-calculateconfigforfile] * [isPathIgnored()][eslint-ispathignored] * [loadFormatter()][eslint-loadformatter] @@ -205,6 +206,25 @@ The second parameter `options` is omittable. * (`Promise`)
The promise that will be fulfilled with an array of [LintResult] objects. This is an array (despite there being only one lint result) in order to keep the interfaces between this and the [`eslint.lintFiles()`][eslint-lintfiles] method similar. +### ◆ eslint.getRulesMetaForResults(results) + +```js +const results = await eslint.lintFiles(patterns); +const rulesMeta = eslint.getRulesMetaForResults(results); +``` + +This method returns an object containing meta information for each rule that triggered a lint error in the given `results`. + +#### Parameters + +* `results` (`LintResult[]`)
+ An array of [LintResult] objects returned from a call to `ESLint#lintFiles()` or `ESLint#lintText()`. + +#### Return Value + +* (`Object`)
+ An object whose property names are the rule IDs from the `results` and whose property values are the rule's meta information (if available). + ### ◆ eslint.calculateConfigForFile(filePath) ```js @@ -1389,6 +1409,7 @@ ruleTester.run("my-rule", myRule, { [eslint-constructor]: #-new-eslintoptions [eslint-lintfiles]: #-eslintlintfilespatterns [eslint-linttext]: #-eslintlinttextcode-options +[eslint-getrulesmetaforresults]: #-eslintgetrulesmetaforresultsresults [eslint-calculateconfigforfile]: #-eslintcalculateconfigforfilefilepath [eslint-ispathignored]: #-eslintispathignoredfilepath [eslint-loadformatter]: #-eslintloadformatternameorpath diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index c387ca72c3a..056e04b5945 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -514,6 +514,39 @@ class ESLint { return CLIEngine.getErrorResults(results); } + /** + * Returns meta objects for each rule represented in the lint results. + * @param {LintResult[]} results The results to fetch rules meta for. + * @returns {Object} A mapping of ruleIds to rule meta objects. + */ + getRulesMetaForResults(results) { + + const resultRuleIds = new Set(); + + // first gather all ruleIds from all results + + for (const result of results) { + for (const { ruleId } of result.messages) { + resultRuleIds.add(ruleId); + } + } + + // create a map of all rules in the results + + const { cliEngine } = privateMembersMap.get(this); + const rules = cliEngine.getRules(); + const resultRules = new Map(); + + for (const [ruleId, rule] of rules) { + if (resultRuleIds.has(ruleId)) { + resultRules.set(ruleId, rule); + } + } + + return createRulesMeta(resultRules); + + } + /** * Executes the current configuration on an array of file and directory names. * @param {string[]} patterns An array of file and directory names. diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 0845d697083..c5a1b7360c0 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -22,6 +22,7 @@ const shell = require("shelljs"); const { CascadingConfigArrayFactory } = require("@eslint/eslintrc/lib/cascading-config-array-factory"); const hash = require("../../../lib/cli-engine/hash"); const { unIndent, createCustomTeardown } = require("../../_utils"); +const coreRules = require("../../../lib/rules"); //------------------------------------------------------------------------------ // Tests @@ -4790,6 +4791,80 @@ describe("ESLint", () => { }); }); + describe("getRulesMetaForResults()", () => { + it("should return empty object when there are no linting errors", async () => { + const engine = new ESLint({ + useEslintrc: false + }); + + const rulesMeta = engine.getRulesMetaForResults([]); + + assert.strictEqual(Object.keys(rulesMeta).length, 0); + }); + + it("should return one rule meta when there is a linting error", async () => { + const engine = new ESLint({ + useEslintrc: false, + overrideConfig: { + rules: { + semi: 2 + } + } + }); + + const results = await engine.lintText("a"); + const rulesMeta = engine.getRulesMetaForResults(results); + + assert.strictEqual(rulesMeta.semi, coreRules.get("semi").meta); + }); + + it("should return multiple rule meta when there are multiple linting errors", async () => { + const engine = new ESLint({ + useEslintrc: false, + overrideConfig: { + rules: { + semi: 2, + quotes: [2, "double"] + } + } + }); + + const results = await engine.lintText("'a'"); + const rulesMeta = engine.getRulesMetaForResults(results); + + assert.strictEqual(rulesMeta.semi, coreRules.get("semi").meta); + assert.strictEqual(rulesMeta.quotes, coreRules.get("quotes").meta); + }); + + it("should return multiple rule meta when there are multiple linting errors from a plugin", async () => { + const nodePlugin = require("eslint-plugin-node"); + const engine = new ESLint({ + useEslintrc: false, + plugins: { + node: nodePlugin + }, + overrideConfig: { + plugins: ["node"], + rules: { + "node/no-new-require": 2, + semi: 2, + quotes: [2, "double"] + } + } + }); + + const results = await engine.lintText("new require('hi')"); + const rulesMeta = engine.getRulesMetaForResults(results); + + assert.strictEqual(rulesMeta.semi, coreRules.get("semi").meta); + assert.strictEqual(rulesMeta.quotes, coreRules.get("quotes").meta); + assert.strictEqual( + rulesMeta["node/no-new-require"], + nodePlugin.rules["no-new-require"].meta + ); + }); + }); + describe("outputFixes()", () => { afterEach(() => { sinon.verifyAndRestore(); From bfbfe5c1fd4c39a06d5e159dbe48479ca4305fc0 Mon Sep 17 00:00:00 2001 From: Brandon Mills Date: Fri, 18 Jun 2021 15:58:27 -0400 Subject: [PATCH 46/67] New: Add only to RuleTester (refs eslint/rfcs#73) (#14677) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * New: Add only to RuleTester (refs eslint/rfcs#73) * Fix variable name typo Co-authored-by: Milos Djermanovic * Clarify executable name Co-authored-by: 薛定谔的猫 * Use this in static accessor for consistency Co-authored-by: Milos Djermanovic * Remove unnecessary spy Co-authored-by: Milos Djermanovic Co-authored-by: 薛定谔的猫 --- Makefile.js | 2 +- docs/developer-guide/nodejs-api.md | 10 +- docs/developer-guide/unit-tests.md | 22 ++- lib/rule-tester/rule-tester.js | 66 ++++++- tests/lib/rule-tester/rule-tester.js | 260 +++++++++++++++++++++++++++ 5 files changed, 346 insertions(+), 14 deletions(-) diff --git a/Makefile.js b/Makefile.js index 6cffb770c64..62b1d76669f 100644 --- a/Makefile.js +++ b/Makefile.js @@ -544,7 +544,7 @@ target.mocha = () => { echo("Running unit tests"); - lastReturn = exec(`${getBinFile("nyc")} -- ${MOCHA} -R progress -t ${MOCHA_TIMEOUT} -c ${TEST_FILES}`); + lastReturn = exec(`${getBinFile("nyc")} -- ${MOCHA} --forbid-only -R progress -t ${MOCHA_TIMEOUT} -c ${TEST_FILES}`); if (lastReturn.code !== 0) { errors++; } diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 28b0fff9070..8d25c6e986d 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -1256,6 +1256,7 @@ A test case is an object with the following properties: * `code` (string, required): The source code that the rule should be run on * `options` (array, optional): The options passed to the rule. The rule severity should not be included in this list. * `filename` (string, optional): The filename for the given case (useful for rules that make assertions about filenames). +* `only` (boolean, optional): Run this case exclusively for debugging in supported test frameworks. In addition to the properties above, invalid test cases can also have the following properties: @@ -1355,10 +1356,13 @@ ruleTester.run("my-rule-for-no-foo", rule, { `RuleTester` depends on two functions to run tests: `describe` and `it`. These functions can come from various places: 1. If `RuleTester.describe` and `RuleTester.it` have been set to function values, `RuleTester` will use `RuleTester.describe` and `RuleTester.it` to run tests. You can use this to customize the behavior of `RuleTester` to match a test framework that you're using. -1. Otherwise, if `describe` and `it` are present as globals, `RuleTester` will use `global.describe` and `global.it` to run tests. This allows `RuleTester` to work when using frameworks like [Mocha](https://mochajs.org/) without any additional configuration. -1. Otherwise, `RuleTester#run` will simply execute all of the tests in sequence, and will throw an error if one of them fails. This means you can simply execute a test file that calls `RuleTester.run` using `node`, without needing a testing framework. -`RuleTester#run` calls the `describe` function with two arguments: a string describing the rule, and a callback function. The callback calls the `it` function with a string describing the test case, and a test function. The test function will return successfully if the test passes, and throw an error if the test fails. (Note that this is the standard behavior for test suites when using frameworks like [Mocha](https://mochajs.org/); this information is only relevant if you plan to customize `RuleTester.it` and `RuleTester.describe`.) + If `RuleTester.itOnly` has been set to a function value, `RuleTester` will call `RuleTester.itOnly` instead of `RuleTester.it` to run cases with `only: true`. If `RuleTester.itOnly` is not set but `RuleTester.it` has an `only` function property, `RuleTester` will fall back to `RuleTester.it.only`. + +2. Otherwise, if `describe` and `it` are present as globals, `RuleTester` will use `global.describe` and `global.it` to run tests and `global.it.only` to run cases with `only: true`. This allows `RuleTester` to work when using frameworks like [Mocha](https://mochajs.org/) without any additional configuration. +3. Otherwise, `RuleTester#run` will simply execute all of the tests in sequence, and will throw an error if one of them fails. This means you can simply execute a test file that calls `RuleTester.run` using `Node.js`, without needing a testing framework. + +`RuleTester#run` calls the `describe` function with two arguments: a string describing the rule, and a callback function. The callback calls the `it` function with a string describing the test case, and a test function. The test function will return successfully if the test passes, and throw an error if the test fails. The signature for `only` is the same as `it`. `RuleTester` calls either `it` or `only` for every case even when some cases have `only: true`, and the test framework is responsible for implementing test case exclusivity. (Note that this is the standard behavior for test suites when using frameworks like [Mocha](https://mochajs.org/); this information is only relevant if you plan to customize `RuleTester.describe`, `RuleTester.it`, or `RuleTester.itOnly`.) Example of customizing `RuleTester`: diff --git a/docs/developer-guide/unit-tests.md b/docs/developer-guide/unit-tests.md index 281c3967454..46d77b59076 100644 --- a/docs/developer-guide/unit-tests.md +++ b/docs/developer-guide/unit-tests.md @@ -10,11 +10,29 @@ This automatically starts Mocha and runs all tests in the `tests` directory. You ## Running Individual Tests -If you want to quickly run just one test, you can do so by running Mocha directly and passing in the filename. For example: +If you want to quickly run just one test file, you can do so by running Mocha directly and passing in the filename. For example: npm run test:cli tests/lib/rules/no-wrap-func.js -Running individual tests is useful when you're working on a specific bug and iterating on the solution. You should be sure to run `npm test` before submitting a pull request. +If you want to run just one or a subset of `RuleTester` test cases, add `only: true` to each test case or wrap the test case in `RuleTester.only(...)` to add it automatically: + +```js +ruleTester.run("my-rule", myRule, { + valid: [ + RuleTester.only("const valid = 42;"), + // Other valid cases + ], + invalid: [ + { + code: "const invalid = 42;", + only: true, + }, + // Other invalid cases + ] +}) +``` + +Running individual tests is useful when you're working on a specific bug and iterating on the solution. You should be sure to run `npm test` before submitting a pull request. `npm test` uses Mocha's `--forbid-only` option to prevent `only` tests from passing full test runs. ## More Control on Unit Testing diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index f177ae3cc17..cac81bc71d1 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -71,6 +71,7 @@ const espreePath = require.resolve("espree"); * @property {{ [name: string]: any }} [parserOptions] Options for the parser. * @property {{ [name: string]: "readonly" | "writable" | "off" }} [globals] The additional global variables. * @property {{ [name: string]: boolean }} [env] Environments for the test case. + * @property {boolean} [only] Run only this test case or the subset of test cases with this property. */ /** @@ -86,6 +87,7 @@ const espreePath = require.resolve("espree"); * @property {{ [name: string]: any }} [parserOptions] Options for the parser. * @property {{ [name: string]: "readonly" | "writable" | "off" }} [globals] The additional global variables. * @property {{ [name: string]: boolean }} [env] Environments for the test case. + * @property {boolean} [only] Run only this test case or the subset of test cases with this property. */ /** @@ -121,7 +123,8 @@ const RuleTesterParameters = [ "filename", "options", "errors", - "output" + "output", + "only" ]; /* @@ -282,6 +285,7 @@ function wrapParser(parser) { // default separators for testing const DESCRIBE = Symbol("describe"); const IT = Symbol("it"); +const IT_ONLY = Symbol("itOnly"); /** * This is `it` default handler if `it` don't exist. @@ -400,6 +404,46 @@ class RuleTester { this[IT] = value; } + /** + * Adds the `only` property to a test to run it in isolation. + * @param {string | ValidTestCase | InvalidTestCase} item A single test to run by itself. + * @returns {ValidTestCase | InvalidTestCase} The test with `only` set. + */ + static only(item) { + if (typeof item === "string") { + return { code: item, only: true }; + } + + return { ...item, only: true }; + } + + static get itOnly() { + if (typeof this[IT_ONLY] === "function") { + return this[IT_ONLY]; + } + if (typeof this[IT] === "function" && typeof this[IT].only === "function") { + return Function.bind.call(this[IT].only, this[IT]); + } + if (typeof it === "function" && typeof it.only === "function") { + return Function.bind.call(it.only, it); + } + + if (typeof this[DESCRIBE] === "function" || typeof this[IT] === "function") { + throw new Error( + "Set `RuleTester.itOnly` to use `only` with a custom test framework.\n" + + "See https://eslint.org/docs/developer-guide/nodejs-api#customizing-ruletester for more." + ); + } + if (typeof it === "function") { + throw new Error("The current test framework does not support exclusive tests with `only`."); + } + throw new Error("To use `only`, use RuleTester with a test framework that provides `it.only()` like Mocha."); + } + + static set itOnly(value) { + this[IT_ONLY] = value; + } + /** * Define a rule for one particular run of tests. * @param {string} name The name of the rule to define. @@ -891,23 +935,29 @@ class RuleTester { RuleTester.describe(ruleName, () => { RuleTester.describe("valid", () => { test.valid.forEach(valid => { - RuleTester.it(sanitize(typeof valid === "object" ? valid.code : valid), () => { - testValidTemplate(valid); - }); + RuleTester[valid.only ? "itOnly" : "it"]( + sanitize(typeof valid === "object" ? valid.code : valid), + () => { + testValidTemplate(valid); + } + ); }); }); RuleTester.describe("invalid", () => { test.invalid.forEach(invalid => { - RuleTester.it(sanitize(invalid.code), () => { - testInvalidTemplate(invalid); - }); + RuleTester[invalid.only ? "itOnly" : "it"]( + sanitize(invalid.code), + () => { + testInvalidTemplate(invalid); + } + ); }); }); }); } } -RuleTester[DESCRIBE] = RuleTester[IT] = null; +RuleTester[DESCRIBE] = RuleTester[IT] = RuleTester[IT_ONLY] = null; module.exports = RuleTester; diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 3aaf6f91f3f..70647b18670 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -97,6 +97,266 @@ describe("RuleTester", () => { ruleTester = new RuleTester(); }); + describe("only", () => { + describe("`itOnly` accessor", () => { + describe("when `itOnly` is set", () => { + before(() => { + RuleTester.itOnly = sinon.spy(); + }); + after(() => { + RuleTester.itOnly = void 0; + }); + beforeEach(() => { + RuleTester.itOnly.resetHistory(); + ruleTester = new RuleTester(); + }); + + it("is called by exclusive tests", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [{ + code: "const notVar = 42;", + only: true + }], + invalid: [] + }); + + sinon.assert.calledWith(RuleTester.itOnly, "const notVar = 42;"); + }); + }); + + describe("when `it` is set and has an `only()` method", () => { + before(() => { + RuleTester.it.only = () => {}; + sinon.spy(RuleTester.it, "only"); + }); + after(() => { + RuleTester.it.only = void 0; + }); + beforeEach(() => { + RuleTester.it.only.resetHistory(); + ruleTester = new RuleTester(); + }); + + it("is called by tests with `only` set", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [{ + code: "const notVar = 42;", + only: true + }], + invalid: [] + }); + + sinon.assert.calledWith(RuleTester.it.only, "const notVar = 42;"); + }); + }); + + describe("when global `it` is a function that has an `only()` method", () => { + let originalGlobalItOnly; + + before(() => { + + /* + * We run tests with `--forbid-only`, so we have to override + * `it.only` to prevent the real one from being called. + */ + originalGlobalItOnly = it.only; + it.only = () => {}; + sinon.spy(it, "only"); + }); + after(() => { + it.only = originalGlobalItOnly; + }); + beforeEach(() => { + it.only.resetHistory(); + ruleTester = new RuleTester(); + }); + + it("is called by tests with `only` set", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [{ + code: "const notVar = 42;", + only: true + }], + invalid: [] + }); + + sinon.assert.calledWith(it.only, "const notVar = 42;"); + }); + }); + + describe("when `describe` and `it` are overridden without `itOnly`", () => { + let originalGlobalItOnly; + + before(() => { + + /* + * These tests override `describe` and `it` already, so we + * don't need to override them here. We do, however, need to + * remove `only` from the global `it` to prevent it from + * being used instead. + */ + originalGlobalItOnly = it.only; + it.only = void 0; + }); + after(() => { + it.only = originalGlobalItOnly; + }); + beforeEach(() => { + ruleTester = new RuleTester(); + }); + + it("throws an error recommending overriding `itOnly`", () => { + assert.throws(() => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [{ + code: "const notVar = 42;", + only: true + }], + invalid: [] + }); + }, "Set `RuleTester.itOnly` to use `only` with a custom test framework."); + }); + }); + + describe("when global `it` is a function that does not have an `only()` method", () => { + let originalGlobalIt; + let originalRuleTesterDescribe; + let originalRuleTesterIt; + + before(() => { + originalGlobalIt = global.it; + + // eslint-disable-next-line no-global-assign + it = () => {}; + + /* + * These tests override `describe` and `it`, so we need to + * un-override them here so they won't interfere. + */ + originalRuleTesterDescribe = RuleTester.describe; + RuleTester.describe = void 0; + originalRuleTesterIt = RuleTester.it; + RuleTester.it = void 0; + }); + after(() => { + + // eslint-disable-next-line no-global-assign + it = originalGlobalIt; + RuleTester.describe = originalRuleTesterDescribe; + RuleTester.it = originalRuleTesterIt; + }); + beforeEach(() => { + ruleTester = new RuleTester(); + }); + + it("throws an error explaining that the current test framework does not support `only`", () => { + assert.throws(() => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [{ + code: "const notVar = 42;", + only: true + }], + invalid: [] + }); + }, "The current test framework does not support exclusive tests with `only`."); + }); + }); + }); + + describe("test cases", () => { + const ruleName = "no-var"; + const rule = require("../../fixtures/testers/rule-tester/no-var"); + + let originalRuleTesterIt; + let spyRuleTesterIt; + let originalRuleTesterItOnly; + let spyRuleTesterItOnly; + + before(() => { + originalRuleTesterIt = RuleTester.it; + spyRuleTesterIt = sinon.spy(); + RuleTester.it = spyRuleTesterIt; + originalRuleTesterItOnly = RuleTester.itOnly; + spyRuleTesterItOnly = sinon.spy(); + RuleTester.itOnly = spyRuleTesterItOnly; + }); + after(() => { + RuleTester.it = originalRuleTesterIt; + RuleTester.itOnly = originalRuleTesterItOnly; + }); + beforeEach(() => { + spyRuleTesterIt.resetHistory(); + spyRuleTesterItOnly.resetHistory(); + ruleTester = new RuleTester(); + }); + + it("isn't called for normal tests", () => { + ruleTester.run(ruleName, rule, { + valid: ["const notVar = 42;"], + invalid: [] + }); + sinon.assert.calledWith(spyRuleTesterIt, "const notVar = 42;"); + sinon.assert.notCalled(spyRuleTesterItOnly); + }); + + it("calls it or itOnly for every test case", () => { + + /* + * `RuleTester` doesn't implement test case exclusivity itself. + * Setting `only: true` just causes `RuleTester` to call + * whatever `only()` function is provided by the test framework + * instead of the regular `it()` function. + */ + + ruleTester.run(ruleName, rule, { + valid: [ + "const valid = 42;", + { + code: "const onlyValid = 42;", + only: true + } + ], + invalid: [ + { + code: "var invalid = 42;", + errors: [/^Bad var/u] + }, + { + code: "var onlyInvalid = 42;", + errors: [/^Bad var/u], + only: true + } + ] + }); + + sinon.assert.calledWith(spyRuleTesterIt, "const valid = 42;"); + sinon.assert.calledWith(spyRuleTesterItOnly, "const onlyValid = 42;"); + sinon.assert.calledWith(spyRuleTesterIt, "var invalid = 42;"); + sinon.assert.calledWith(spyRuleTesterItOnly, "var onlyInvalid = 42;"); + }); + }); + + describe("static helper wrapper", () => { + it("adds `only` to string test cases", () => { + const test = RuleTester.only("const valid = 42;"); + + assert.deepStrictEqual(test, { + code: "const valid = 42;", + only: true + }); + }); + + it("adds `only` to object test cases", () => { + const test = RuleTester.only({ code: "const valid = 42;" }); + + assert.deepStrictEqual(test, { + code: "const valid = 42;", + only: true + }); + }); + }); + }); + it("should not throw an error when everything passes", () => { ruleTester.run("no-eval", require("../../fixtures/testers/rule-tester/no-eval"), { valid: [ From a02dbd2822d00321a16a6b68791178e7d8f21369 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 18 Jun 2021 16:17:15 -0400 Subject: [PATCH 47/67] Build: changelog update for 7.29.0 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eaeb9a9a56..dc8336f27ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +v7.29.0 - June 18, 2021 + +* [`bfbfe5c`](https://github.com/eslint/eslint/commit/bfbfe5c1fd4c39a06d5e159dbe48479ca4305fc0) New: Add only to RuleTester (refs eslint/rfcs#73) (#14677) (Brandon Mills) +* [`c2cd7b4`](https://github.com/eslint/eslint/commit/c2cd7b4a18057ca6067bdfc16de771dc5d90c0ea) New: Add ESLint#getRulesMetaForResults() (refs #13654) (#14716) (Nicholas C. Zakas) +* [`eea7e0d`](https://github.com/eslint/eslint/commit/eea7e0d09d6ef43d6663cbe424e7974764a5f7fe) Chore: remove duplicate code (#14719) (Nitin Kumar) +* [`6a1c7a0`](https://github.com/eslint/eslint/commit/6a1c7a0dac050ea5876972c50563a7eb867b38d3) Fix: allow fallthrough comment inside block (fixes #14701) (#14702) (Kevin Gibbons) +* [`a47e5e3`](https://github.com/eslint/eslint/commit/a47e5e30b0da364593b6881f6826c595da8696f5) Docs: Add Mega-Linter to the list of integrations (#14707) (Nicolas Vuillamy) +* [`353ddf9`](https://github.com/eslint/eslint/commit/353ddf965078030794419b089994373e27ffc86e) Chore: enable reportUnusedDisableDirectives in eslint-config-eslint (#14699) (薛定谔的猫) +* [`757c495`](https://github.com/eslint/eslint/commit/757c49584a5852c468c1b4a0b74ad3aa39d954e5) Chore: add some rules to eslint-config-eslint (#14692) (薛定谔的猫) +* [`c93a222`](https://github.com/eslint/eslint/commit/c93a222563177a9b5bc7a59aa106bc0a6d31e063) Docs: fix a broken link (#14697) (Sam Chen) +* [`655c118`](https://github.com/eslint/eslint/commit/655c1187fc845bac61ae8d06c556f1a59ee2071b) Sponsors: Sync README with website (ESLint Jenkins) +* [`e2bed2e`](https://github.com/eslint/eslint/commit/e2bed2ead22b575d55ccaeed94eecd3a979dd871) Sponsors: Sync README with website (ESLint Jenkins) +* [`8490fb4`](https://github.com/eslint/eslint/commit/8490fb42e559ef0b3c34ac60be4e05e0d879a9cb) Sponsors: Sync README with website (ESLint Jenkins) +* [`ddbe877`](https://github.com/eslint/eslint/commit/ddbe877c95224e127215d35562a175c6f2b7ba22) Sponsors: Sync README with website (ESLint Jenkins) + v7.28.0 - June 4, 2021 * [`1237705`](https://github.com/eslint/eslint/commit/1237705dd08c209c5e3136045ec51a4ba87a3abe) Upgrade: @eslint/eslintrc to 0.4.2 (#14672) (Milos Djermanovic) From b402f7892f500ef7657e87c3f30c4c5a5c8f5d66 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 18 Jun 2021 16:17:16 -0400 Subject: [PATCH 48/67] 7.29.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 601033eb44e..f556a550356 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "7.28.0", + "version": "7.29.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From 8972529f82d13bd04059ee8852b4ebb9b5350962 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 18 Jun 2021 16:26:05 -0400 Subject: [PATCH 49/67] Docs: Update README team and sponsors --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 2202900d194..c87d07e0dcd 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,11 @@ Anix
YeonJuan + + +
+Nitin Kumar +
From aada733d2aee830aa32cccb9828cd72db4ccd6bd Mon Sep 17 00:00:00 2001 From: Sam Chen Date: Sun, 20 Jun 2021 09:49:45 +0800 Subject: [PATCH 50/67] Docs: fix two broken links (#14726) --- docs/developer-guide/working-with-rules.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md index 6a019aff199..a269e1c5fa4 100644 --- a/docs/developer-guide/working-with-rules.md +++ b/docs/developer-guide/working-with-rules.md @@ -127,7 +127,7 @@ The `context` object contains additional functionality that is helpful for rules Additionally, the `context` object has the following methods: * `getAncestors()` - returns an array of the ancestors of the currently-traversed node, starting at the root of the AST and continuing through the direct parent of the current node. This array does not include the currently-traversed node itself. -* `getCwd()` - returns the `cwd` passed to [Linter](./nodejs-api.md#Linter). It is a path to a directory that should be considered as the current working directory. +* `getCwd()` - returns the `cwd` passed to [Linter](./nodejs-api.md#linter). It is a path to a directory that should be considered as the current working directory. * `getDeclaredVariables(node)` - returns a list of [variables](./scope-manager-interface.md#variable-interface) declared by the given node. This information can be used to track references to variables. * If the node is a `VariableDeclaration`, all variables declared in the declaration are returned. * If the node is a `VariableDeclarator`, all variables declared in the declarator are returned. @@ -387,7 +387,7 @@ Suggestions are intended to provide fixes. ESLint will automatically remove the #### Suggestion `messageId`s -Instead of using a `desc` key for suggestions a `messageId` can be used instead. This works the same way as `messageId`s for the overall error (see [messageIds](#messageIds)). Here is an example of how to use it in a rule: +Instead of using a `desc` key for suggestions a `messageId` can be used instead. This works the same way as `messageId`s for the overall error (see [messageIds](#messageids)). Here is an example of how to use it in a rule: ```js {% raw %} From 1b8997ab63781f4ebf87e3269400b2ef4c7d2973 Mon Sep 17 00:00:00 2001 From: Brandon Mills Date: Sat, 19 Jun 2021 21:51:05 -0400 Subject: [PATCH 51/67] Docs: Fix getRulesMetaForResults link syntax (#14723) I caught the correct name of the anchor tag but missed the use of parens instead of square braces. --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 8d25c6e986d..daa56e76479 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -10,7 +10,7 @@ While ESLint is designed to be run on the command line, it's possible to use ESL * [constructor()][eslint-constructor] * [lintFiles()][eslint-lintfiles] * [lintText()][eslint-linttext] - * [getRulesMetaForResults()](eslint-getrulesmetaforresults) + * [getRulesMetaForResults()][eslint-getrulesmetaforresults] * [calculateConfigForFile()][eslint-calculateconfigforfile] * [isPathIgnored()][eslint-ispathignored] * [loadFormatter()][eslint-loadformatter] From f113cdd872257d72bbd66d95e4eaf13623323b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=9B=E5=AE=9A=E8=B0=94=E7=9A=84=E7=8C=AB?= Date: Wed, 23 Jun 2021 18:11:07 +0800 Subject: [PATCH 52/67] Chore: upgrade eslint-plugin-eslint-plugin (#14738) --- lib/rules/dot-notation.js | 6 +++--- lib/rules/prefer-arrow-callback.js | 8 ++++---- package.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/rules/dot-notation.js b/lib/rules/dot-notation.js index 751b4628edc..3aa9f3110f5 100644 --- a/lib/rules/dot-notation.js +++ b/lib/rules/dot-notation.js @@ -94,7 +94,7 @@ module.exports = { // Don't perform any fixes if there are comments inside the brackets. if (sourceCode.commentsExistBetween(leftBracket, rightBracket)) { - return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + return; } // Replace the brackets by an identifier. @@ -154,12 +154,12 @@ module.exports = { // A statement that starts with `let[` is parsed as a destructuring variable declaration, not a MemberExpression. if (node.object.type === "Identifier" && node.object.name === "let" && !node.optional) { - return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + return; } // Don't perform any fixes if there are comments between the dot and the property name. if (sourceCode.commentsExistBetween(dotToken, node.property)) { - return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + return; } // Replace the identifier to brackets. diff --git a/lib/rules/prefer-arrow-callback.js b/lib/rules/prefer-arrow-callback.js index ee5cfe3c8c7..a01c0340821 100644 --- a/lib/rules/prefer-arrow-callback.js +++ b/lib/rules/prefer-arrow-callback.js @@ -295,7 +295,7 @@ module.exports = { * If the callback function has duplicates in its list of parameters (possible in sloppy mode), * don't replace it with an arrow function, because this is a SyntaxError with arrow functions. */ - return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + return; } // Remove `.bind(this)` if exists. @@ -307,7 +307,7 @@ module.exports = { * E.g. `(foo || function(){}).bind(this)` */ if (memberNode.type !== "MemberExpression") { - return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + return; } const callNode = memberNode.parent; @@ -320,12 +320,12 @@ module.exports = { * ^^^^^^^^^^^^ */ if (astUtils.isParenthesised(sourceCode, memberNode)) { - return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + return; } // If comments exist in the `.bind(this)`, don't remove those. if (sourceCode.commentsExistBetween(firstTokenToRemove, lastTokenToRemove)) { - return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + return; } yield fixer.removeRange([firstTokenToRemove.range[0], lastTokenToRemove.range[1]]); diff --git a/package.json b/package.json index f556a550356..5cb9c6a0d2c 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "ejs": "^3.0.2", "eslint": "file:.", "eslint-config-eslint": "file:packages/eslint-config-eslint", - "eslint-plugin-eslint-plugin": "^3.0.3", + "eslint-plugin-eslint-plugin": "^3.2.0", "eslint-plugin-internal-rules": "file:tools/internal-rules", "eslint-plugin-jsdoc": "^25.4.3", "eslint-plugin-node": "^11.1.0", From b08170b92beb22db6ec612ebdfff930f9e0582ab Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Sat, 26 Jun 2021 04:33:02 -0700 Subject: [PATCH 53/67] Update: Implement FlatConfigArray (refs #13481) (#14321) * Update: Implement FlatConfigArray (refs #13481) * Upgrade config-array package * Add schemas for linterOptions, processor, plugins * Continue implementing config schemas * RulesSchema start * Add initial finalization step * Default config * Strict mode * Start rule validation * Finish FlatConfigArray implementation * Remove too-new syntax * Fix default config * fix test * Update tests/lib/config/flat-config-array.js Co-authored-by: Brandon Mills * Update tests/lib/config/flat-config-array.js Co-authored-by: Brandon Mills * Update tests/lib/config/flat-config-array.js Co-authored-by: Brandon Mills * Update tests/lib/config/flat-config-array.js Co-authored-by: Brandon Mills * Update tests * fix test * Allow old-style plugin names * Fix reportUnusedDisableDirectives and add JSDoc * Add more tests * address review comments * Ignore only .git directory * Allow null for global settings * writeable -> writable * Remove incorrect comment * Validate severity-only rule options * Add key to global error message * deeply merge parserOptions and settings * Rename defaultResultConfig * Normalize and fix rule validations * Fix rule options merging * Fix various errors * Rebase onto master Co-authored-by: Brandon Mills --- lib/config/default-config.js | 52 + lib/config/flat-config-array.js | 125 +++ lib/config/flat-config-schema.js | 452 ++++++++ lib/config/rule-validator.js | 169 +++ package.json | 1 + tests/lib/config/flat-config-array.js | 1450 +++++++++++++++++++++++++ 6 files changed, 2249 insertions(+) create mode 100644 lib/config/default-config.js create mode 100644 lib/config/flat-config-array.js create mode 100644 lib/config/flat-config-schema.js create mode 100644 lib/config/rule-validator.js create mode 100644 tests/lib/config/flat-config-array.js diff --git a/lib/config/default-config.js b/lib/config/default-config.js new file mode 100644 index 00000000000..cb6f403380d --- /dev/null +++ b/lib/config/default-config.js @@ -0,0 +1,52 @@ +/** + * @fileoverview Default configuration + * @author Nicholas C. Zakas + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const Rules = require("../rules"); + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + + +exports.defaultConfig = [ + { + plugins: { + "@": { + parsers: { + espree: require("espree") + }, + + /* + * Because we try to delay loading rules until absolutely + * necessary, a proxy allows us to hook into the lazy-loading + * aspect of the rules map while still keeping all of the + * relevant configuration inside of the config array. + */ + rules: new Proxy({}, { + get(target, property) { + return Rules.get(property); + }, + + has(target, property) { + return Rules.has(property); + } + }) + } + }, + ignores: [ + "**/node_modules/**", + ".git/**" + ], + languageOptions: { + parser: "@/espree" + } + } +]; diff --git a/lib/config/flat-config-array.js b/lib/config/flat-config-array.js new file mode 100644 index 00000000000..ecf396a3314 --- /dev/null +++ b/lib/config/flat-config-array.js @@ -0,0 +1,125 @@ +/** + * @fileoverview Flat Config Array + * @author Nicholas C. Zakas + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const { ConfigArray, ConfigArraySymbol } = require("@humanwhocodes/config-array"); +const { flatConfigSchema } = require("./flat-config-schema"); +const { RuleValidator } = require("./rule-validator"); +const { defaultConfig } = require("./default-config"); +const recommendedConfig = require("../../conf/eslint-recommended"); +const allConfig = require("../../conf/eslint-all"); + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +const ruleValidator = new RuleValidator(); + +/** + * Splits a plugin identifier in the form a/b/c into two parts: a/b and c. + * @param {string} identifier The identifier to parse. + * @returns {{objectName: string, pluginName: string}} The parts of the plugin + * name. + */ +function splitPluginIdentifier(identifier) { + const parts = identifier.split("/"); + + return { + objectName: parts.pop(), + pluginName: parts.join("/") + }; +} + +//----------------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------------- + +/** + * Represents an array containing configuration information for ESLint. + */ +class FlatConfigArray extends ConfigArray { + + /** + * Creates a new instance. + * @param {*[]} configs An array of configuration information. + * @param {{basePath: string, baseConfig: FlatConfig}} options The options + * to use for the config array instance. + */ + constructor(configs, { basePath, baseConfig = defaultConfig }) { + super(configs, { + basePath, + schema: flatConfigSchema + }); + + this.unshift(baseConfig); + } + + /* eslint-disable class-methods-use-this */ + /** + * Replaces a config with another config to allow us to put strings + * in the config array that will be replaced by objects before + * normalization. + * @param {Object} config The config to preprocess. + * @returns {Object} The preprocessed config. + */ + [ConfigArraySymbol.preprocessConfig](config) { + if (config === "eslint:recommended") { + return recommendedConfig; + } + + if (config === "eslint:all") { + return allConfig; + } + + return config; + } + + /** + * Finalizes the config by replacing plugin references with their objects + * and validating rule option schemas. + * @param {Object} config The config to finalize. + * @returns {Object} The finalized config. + * @throws {TypeError} If the config is invalid. + */ + [ConfigArraySymbol.finalizeConfig](config) { + + const { plugins, languageOptions, processor } = config; + + // Check parser value + if (languageOptions && languageOptions.parser && typeof languageOptions.parser === "string") { + const { pluginName, objectName: parserName } = splitPluginIdentifier(languageOptions.parser); + + if (!plugins || !plugins[pluginName] || !plugins[pluginName].parsers || !plugins[pluginName].parsers[parserName]) { + throw new TypeError(`Key "parser": Could not find "${parserName}" in plugin "${pluginName}".`); + } + + languageOptions.parser = plugins[pluginName].parsers[parserName]; + } + + // Check processor value + if (processor && typeof processor === "string") { + const { pluginName, objectName: processorName } = splitPluginIdentifier(processor); + + if (!plugins || !plugins[pluginName] || !plugins[pluginName].processors || !plugins[pluginName].processors[processorName]) { + throw new TypeError(`Key "processor": Could not find "${processorName}" in plugin "${pluginName}".`); + } + + config.processor = plugins[pluginName].processors[processorName]; + } + + ruleValidator.validate(config); + + return config; + } + /* eslint-enable class-methods-use-this */ + +} + +exports.FlatConfigArray = FlatConfigArray; diff --git a/lib/config/flat-config-schema.js b/lib/config/flat-config-schema.js new file mode 100644 index 00000000000..80785476133 --- /dev/null +++ b/lib/config/flat-config-schema.js @@ -0,0 +1,452 @@ +/** + * @fileoverview Flat config schema + * @author Nicholas C. Zakas + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Type Definitions +//----------------------------------------------------------------------------- + +/** + * @typedef ObjectPropertySchema + * @property {Function|string} merge The function or name of the function to call + * to merge multiple objects with this property. + * @property {Function|string} validate The function or name of the function to call + * to validate the value of this property. + */ + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +const ruleSeverities = new Map([ + [0, 0], ["off", 0], + [1, 1], ["warn", 1], + [2, 2], ["error", 2] +]); + +const globalVariablesValues = new Set([ + true, "true", "writable", "writeable", + false, "false", "readonly", "readable", null, + "off" +]); + +/** + * Check if a value is a non-null object. + * @param {any} value The value to check. + * @returns {boolean} `true` if the value is a non-null object. + */ +function isNonNullObject(value) { + return typeof value === "object" && value !== null; +} + +/** + * Check if a value is undefined. + * @param {any} value The value to check. + * @returns {boolean} `true` if the value is undefined. + */ +function isUndefined(value) { + return typeof value === "undefined"; +} + +/** + * Deeply merges two objects. + * @param {Object} first The base object. + * @param {Object} second The overrides object. + * @returns {Object} An object with properties from both first and second. + */ +function deepMerge(first = {}, second = {}) { + + /* + * If the second value is an array, just return it. We don't merge + * arrays because order matters and we can't know the correct order. + */ + if (Array.isArray(second)) { + return second; + } + + /* + * First create a result object where properties from the second object + * overwrite properties from the first. This sets up a baseline to use + * later rather than needing to inspect and change every property + * individually. + */ + const result = { + ...first, + ...second + }; + + for (const key of Object.keys(second)) { + + // avoid hairy edge case + if (key === "__proto__") { + continue; + } + + const firstValue = first[key]; + const secondValue = second[key]; + + if (isNonNullObject(firstValue)) { + result[key] = deepMerge(firstValue, secondValue); + } else if (isUndefined(firstValue)) { + if (isNonNullObject(secondValue)) { + result[key] = deepMerge( + Array.isArray(secondValue) ? [] : {}, + secondValue + ); + } else if (!isUndefined(secondValue)) { + result[key] = secondValue; + } + } + } + + return result; + +} + +/** + * Normalizes the rule options config for a given rule by ensuring that + * it is an array and that the first item is 0, 1, or 2. + * @param {Array|string|number} ruleOptions The rule options config. + * @returns {Array} An array of rule options. + */ +function normalizeRuleOptions(ruleOptions) { + + const finalOptions = Array.isArray(ruleOptions) + ? ruleOptions.slice(0) + : [ruleOptions]; + + finalOptions[0] = ruleSeverities.get(finalOptions[0]); + return finalOptions; +} + +//----------------------------------------------------------------------------- +// Assertions +//----------------------------------------------------------------------------- + +/** + * Validates that a value is a valid rule options entry. + * @param {any} value The value to check. + * @returns {void} + * @throws {TypeError} If the value isn't a valid rule options. + */ +function assertIsRuleOptions(value) { + + if (typeof value !== "string" && typeof value !== "number" && !Array.isArray(value)) { + throw new TypeError("Expected a string, number, or array."); + } +} + +/** + * Validates that a value is valid rule severity. + * @param {any} value The value to check. + * @returns {void} + * @throws {TypeError} If the value isn't a valid rule severity. + */ +function assertIsRuleSeverity(value) { + const severity = typeof value === "string" + ? ruleSeverities.get(value.toLowerCase()) + : ruleSeverities.get(value); + + if (typeof severity === "undefined") { + throw new TypeError("Expected severity of \"off\", 0, \"warn\", 1, \"error\", or 2."); + } +} + +/** + * Validates that a given string is the form pluginName/objectName. + * @param {string} value The string to check. + * @returns {void} + * @throws {TypeError} If the string isn't in the correct format. + */ +function assertIsPluginMemberName(value) { + if (!/[@a-z0-9-_$]+(?:\/(?:[a-z0-9-_$]+))+$/iu.test(value)) { + throw new TypeError(`Expected string in the form "pluginName/objectName" but found "${value}".`); + } +} + +/** + * Validates that a value is an object. + * @param {any} value The value to check. + * @returns {void} + * @throws {TypeError} If the value isn't an object. + */ +function assertIsObject(value) { + if (!isNonNullObject(value)) { + throw new TypeError("Expected an object."); + } +} + +/** + * Validates that a value is an object or a string. + * @param {any} value The value to check. + * @returns {void} + * @throws {TypeError} If the value isn't an object or a string. + */ +function assertIsObjectOrString(value) { + if ((!value || typeof value !== "object") && typeof value !== "string") { + throw new TypeError("Expected an object or string."); + } +} + +//----------------------------------------------------------------------------- +// Low-Level Schemas +//----------------------------------------------------------------------------- + + +/** @type {ObjectPropertySchema} */ +const numberSchema = { + merge: "replace", + validate: "number" +}; + +/** @type {ObjectPropertySchema} */ +const booleanSchema = { + merge: "replace", + validate: "boolean" +}; + +/** @type {ObjectPropertySchema} */ +const deepObjectAssignSchema = { + merge(first = {}, second = {}) { + return deepMerge(first, second); + }, + validate: "object" +}; + +//----------------------------------------------------------------------------- +// High-Level Schemas +//----------------------------------------------------------------------------- + +/** @type {ObjectPropertySchema} */ +const globalsSchema = { + merge: "assign", + validate(value) { + + assertIsObject(value); + + for (const key of Object.keys(value)) { + + // avoid hairy edge case + if (key === "__proto__") { + continue; + } + + if (key !== key.trim()) { + throw new TypeError(`Global "${key}" has leading or trailing whitespace.`); + } + + if (!globalVariablesValues.has(value[key])) { + throw new TypeError(`Key "${key}": Expected "readonly", "writable", or "off".`); + } + } + } +}; + +/** @type {ObjectPropertySchema} */ +const parserSchema = { + merge: "replace", + validate(value) { + assertIsObjectOrString(value); + + if (typeof value === "object" && typeof value.parse !== "function" && typeof value.parseForESLint !== "function") { + throw new TypeError("Expected object to have a parse() or parseForESLint() method."); + } + + if (typeof value === "string") { + assertIsPluginMemberName(value); + } + } +}; + +/** @type {ObjectPropertySchema} */ +const pluginsSchema = { + merge(first = {}, second = {}) { + const keys = new Set([...Object.keys(first), ...Object.keys(second)]); + const result = {}; + + // manually validate that plugins are not redefined + for (const key of keys) { + + // avoid hairy edge case + if (key === "__proto__") { + continue; + } + + if (key in first && key in second && first[key] !== second[key]) { + throw new TypeError(`Cannot redefine plugin "${key}".`); + } + + result[key] = second[key] || first[key]; + } + + return result; + }, + validate(value) { + + // first check the value to be sure it's an object + if (value === null || typeof value !== "object") { + throw new TypeError("Expected an object."); + } + + // second check the keys to make sure they are objects + for (const key of Object.keys(value)) { + + // avoid hairy edge case + if (key === "__proto__") { + continue; + } + + if (value[key] === null || typeof value[key] !== "object") { + throw new TypeError(`Key "${key}": Expected an object.`); + } + } + } +}; + +/** @type {ObjectPropertySchema} */ +const processorSchema = { + merge: "replace", + validate(value) { + if (typeof value === "string") { + assertIsPluginMemberName(value); + } else if (value && typeof value === "object") { + if (typeof value.preprocess !== "function" || typeof value.postprocess !== "function") { + throw new TypeError("Object must have a preprocess() and a postprocess() method."); + } + } else { + throw new TypeError("Expected an object or a string."); + } + } +}; + +/** @type {ObjectPropertySchema} */ +const rulesSchema = { + merge(first = {}, second = {}) { + + const result = { + ...first, + ...second + }; + + for (const ruleId of Object.keys(result)) { + + // avoid hairy edge case + if (ruleId === "__proto__") { + + /* eslint-disable-next-line no-proto */ + delete result.__proto__; + continue; + } + + result[ruleId] = normalizeRuleOptions(result[ruleId]); + + /* + * If either rule config is missing, then the correct + * config is already present and we just need to normalize + * the severity. + */ + if (!(ruleId in first) || !(ruleId in second)) { + continue; + } + + const firstRuleOptions = normalizeRuleOptions(first[ruleId]); + const secondRuleOptions = normalizeRuleOptions(second[ruleId]); + + /* + * If the second rule config only has a severity (length of 1), + * then use that severity and keep the rest of the options from + * the first rule config. + */ + if (secondRuleOptions.length === 1) { + result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)]; + continue; + } + + /* + * In any other situation, then the second rule config takes + * precedence. That means the value at `result[ruleId]` is + * already correct and no further work is necessary. + */ + } + + return result; + }, + + validate(value) { + assertIsObject(value); + + let lastRuleId; + + // Performance: One try-catch has less overhead than one per loop iteration + try { + + /* + * We are not checking the rule schema here because there is no + * guarantee that the rule definition is present at this point. Instead + * we wait and check the rule schema during the finalization step + * of calculating a config. + */ + for (const ruleId of Object.keys(value)) { + + // avoid hairy edge case + if (ruleId === "__proto__") { + continue; + } + + lastRuleId = ruleId; + + const ruleOptions = value[ruleId]; + + assertIsRuleOptions(ruleOptions); + + if (Array.isArray(ruleOptions)) { + assertIsRuleSeverity(ruleOptions[0]); + } else { + assertIsRuleSeverity(ruleOptions); + } + } + } catch (error) { + error.message = `Key "${lastRuleId}": ${error.message}`; + throw error; + } + } +}; + +/** @type {ObjectPropertySchema} */ +const sourceTypeSchema = { + merge: "replace", + validate(value) { + if (typeof value !== "string" || !/^(?:script|module|commonjs)$/u.test(value)) { + throw new TypeError("Expected \"script\", \"module\", or \"commonjs\"."); + } + } +}; + +//----------------------------------------------------------------------------- +// Full schema +//----------------------------------------------------------------------------- + +exports.flatConfigSchema = { + settings: deepObjectAssignSchema, + linterOptions: { + schema: { + noInlineConfig: booleanSchema, + reportUnusedDisableDirectives: booleanSchema + } + }, + languageOptions: { + schema: { + ecmaVersion: numberSchema, + sourceType: sourceTypeSchema, + globals: globalsSchema, + parser: parserSchema, + parserOptions: deepObjectAssignSchema + } + }, + processor: processorSchema, + plugins: pluginsSchema, + rules: rulesSchema +}; diff --git a/lib/config/rule-validator.js b/lib/config/rule-validator.js new file mode 100644 index 00000000000..f162dd81a05 --- /dev/null +++ b/lib/config/rule-validator.js @@ -0,0 +1,169 @@ +/** + * @fileoverview Rule Validator + * @author Nicholas C. Zakas + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const ajv = require("../shared/ajv")(); + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Finds a rule with the given ID in the given config. + * @param {string} ruleId The ID of the rule to find. + * @param {Object} config The config to search in. + * @returns {{create: Function, schema: (Array|null)}} THe rule object. + */ +function findRuleDefinition(ruleId, config) { + const ruleIdParts = ruleId.split("/"); + let pluginName, ruleName; + + // built-in rule + if (ruleIdParts.length === 1) { + pluginName = "@"; + ruleName = ruleIdParts[0]; + } else { + ruleName = ruleIdParts.pop(); + pluginName = ruleIdParts.join("/"); + } + + if (!config.plugins || !config.plugins[pluginName]) { + throw new TypeError(`Key "rules": Key "${ruleId}": Could not find plugin "${pluginName}".`); + } + + if (!config.plugins[pluginName].rules || !config.plugins[pluginName].rules[ruleName]) { + throw new TypeError(`Key "rules": Key "${ruleId}": Could not find "${ruleName}" in plugin "${pluginName}".`); + } + + return config.plugins[pluginName].rules[ruleName]; + +} + +/** + * Gets a complete options schema for a rule. + * @param {{create: Function, schema: (Array|null)}} rule A new-style rule object + * @returns {Object} JSON Schema for the rule's options. + */ +function getRuleOptionsSchema(rule) { + + if (!rule) { + return null; + } + + const schema = rule.schema || rule.meta && rule.meta.schema; + + if (Array.isArray(schema)) { + if (schema.length) { + return { + type: "array", + items: schema, + minItems: 0, + maxItems: schema.length + }; + } + return { + type: "array", + minItems: 0, + maxItems: 0 + }; + + } + + // Given a full schema, leave it alone + return schema || null; +} + +//----------------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------------- + +/** + * Implements validation functionality for the rules portion of a config. + */ +class RuleValidator { + + /** + * Creates a new instance. + */ + constructor() { + + /** + * A collection of compiled validators for rules that have already + * been validated. + * @type {WeakMap} + * @property validators + */ + this.validators = new WeakMap(); + } + + /** + * Validates all of the rule configurations in a config against each + * rule's schema. + * @param {Object} config The full config to validate. This object must + * contain both the rules section and the plugins section. + * @returns {void} + * @throws {Error} If a rule's configuration does not match its schema. + */ + validate(config) { + + if (!config.rules) { + return; + } + + for (const [ruleId, ruleOptions] of Object.entries(config.rules)) { + + // check for edge case + if (ruleId === "__proto__") { + continue; + } + + /* + * If a rule is disabled, we don't do any validation. This allows + * users to safely set any value to 0 or "off" without worrying + * that it will cause a validation error. + * + * Note: ruleOptions is always an array at this point because + * this validation occurs after FlatConfigArray has merged and + * normalized values. + */ + if (ruleOptions[0] === 0) { + continue; + } + + const rule = findRuleDefinition(ruleId, config); + + // Precompile and cache validator the first time + if (!this.validators.has(rule)) { + const schema = getRuleOptionsSchema(rule); + + if (schema) { + this.validators.set(rule, ajv.compile(schema)); + } + } + + const validateRule = this.validators.get(rule); + + if (validateRule) { + + validateRule(ruleOptions.slice(1)); + + if (validateRule.errors) { + throw new Error(`Key "rules": Key "${ruleId}": ${ + validateRule.errors.map( + error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n` + ).join("") + }`); + } + } + } + } +} + +exports.RuleValidator = RuleValidator; diff --git a/package.json b/package.json index 5cb9c6a0d2c..5d53e1e9aa6 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.2", + "@humanwhocodes/config-array": "^0.5.0", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js new file mode 100644 index 00000000000..fd89f8d972c --- /dev/null +++ b/tests/lib/config/flat-config-array.js @@ -0,0 +1,1450 @@ +/** + * @fileoverview Tests for FlatConfigArray + * @author Nicholas C. Zakas + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const { FlatConfigArray } = require("../../../lib/config/flat-config-array"); +const assert = require("chai").assert; +const allConfig = require("../../../conf/eslint-all"); +const recommendedConfig = require("../../../conf/eslint-recommended"); + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +const baseConfig = { + plugins: { + "@": { + rules: { + foo: { + schema: { + type: "array", + items: [ + { + enum: ["always", "never"] + } + ], + minItems: 0, + maxItems: 1 + } + + }, + bar: { + + }, + baz: { + + }, + + // old-style + boom() {}, + + foo2: { + schema: { + type: "array", + items: { + type: "string" + }, + uniqueItems: true, + minItems: 1 + } + } + } + } + } +}; + +/** + * Creates a config array with the correct default options. + * @param {*[]} configs An array of configs to use in the config array. + * @returns {FlatConfigArray} The config array; + */ +function createFlatConfigArray(configs) { + return new FlatConfigArray(configs, { + basePath: __dirname, + baseConfig + }); +} + +/** + * Asserts that a given set of configs will be merged into the given + * result config. + * @param {*[]} values An array of configs to use in the config array. + * @param {Object} result The expected merged result of the configs. + * @returns {void} + * @throws {AssertionError} If the actual result doesn't match the + * expected result. + */ +async function assertMergedResult(values, result) { + const configs = createFlatConfigArray(values); + + await configs.normalize(); + + const config = configs.getConfig("foo.js"); + + assert.deepStrictEqual(config, result); +} + +/** + * Asserts that a given set of configs results in an invalid config. + * @param {*[]} values An array of configs to use in the config array. + * @param {string|RegExp} message The expected error message. + * @returns {void} + * @throws {AssertionError} If the config is valid or if the error + * has an unexpected message. + */ +async function assertInvalidConfig(values, message) { + const configs = createFlatConfigArray(values); + + await configs.normalize(); + + assert.throws(() => { + configs.getConfig("foo.js"); + }, message); +} + +/** + * Normalizes the rule configs to an array with severity to match + * how Flat Config merges rule options. + * @param {Object} rulesConfig The rules config portion of a config. + * @returns {Array} The rules config object. + */ +function normalizeRuleConfig(rulesConfig) { + const rulesConfigCopy = { + ...rulesConfig + }; + + for (const ruleId of Object.keys(rulesConfigCopy)) { + rulesConfigCopy[ruleId] = [2]; + } + + return rulesConfigCopy; +} + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("FlatConfigArray", () => { + + describe("Special configs", () => { + it("eslint:recommended is replaced with an actual config", async () => { + const configs = new FlatConfigArray(["eslint:recommended"], { basePath: __dirname }); + + await configs.normalize(); + const config = configs.getConfig("foo.js"); + + assert.deepStrictEqual(config.rules, normalizeRuleConfig(recommendedConfig.rules)); + }); + + it("eslint:all is replaced with an actual config", async () => { + const configs = new FlatConfigArray(["eslint:all"], { basePath: __dirname }); + + await configs.normalize(); + const config = configs.getConfig("foo.js"); + + assert.deepStrictEqual(config.rules, normalizeRuleConfig(allConfig.rules)); + }); + }); + + describe("Config Properties", () => { + + describe("settings", () => { + + it("should merge two objects", () => assertMergedResult([ + { + settings: { + a: true, + b: false + } + }, + { + settings: { + c: true, + d: false + } + } + ], { + plugins: baseConfig.plugins, + + settings: { + a: true, + b: false, + c: true, + d: false + } + })); + + it("should merge two objects when second object has overrides", () => assertMergedResult([ + { + settings: { + a: true, + b: false, + d: [1, 2], + e: [5, 6] + } + }, + { + settings: { + c: true, + a: false, + d: [3, 4] + } + } + ], { + plugins: baseConfig.plugins, + + settings: { + a: false, + b: false, + c: true, + d: [3, 4], + e: [5, 6] + } + })); + + it("should deeply merge two objects when second object has overrides", () => assertMergedResult([ + { + settings: { + object: { + a: true, + b: false + } + } + }, + { + settings: { + object: { + c: true, + a: false + } + } + } + ], { + plugins: baseConfig.plugins, + + settings: { + object: { + a: false, + b: false, + c: true + } + } + })); + + it("should merge an object and undefined into one object", () => assertMergedResult([ + { + settings: { + a: true, + b: false + } + }, + { + } + ], { + plugins: baseConfig.plugins, + + settings: { + a: true, + b: false + } + })); + + it("should merge undefined and an object into one object", () => assertMergedResult([ + { + }, + { + settings: { + a: true, + b: false + } + } + ], { + plugins: baseConfig.plugins, + + settings: { + a: true, + b: false + } + })); + + }); + + describe("plugins", () => { + + const pluginA = {}; + const pluginB = {}; + const pluginC = {}; + + it("should merge two objects", () => assertMergedResult([ + { + plugins: { + a: pluginA, + b: pluginB + } + }, + { + plugins: { + c: pluginC + } + } + ], { + plugins: { + a: pluginA, + b: pluginB, + c: pluginC, + ...baseConfig.plugins + } + })); + + it("should merge an object and undefined into one object", () => assertMergedResult([ + { + plugins: { + a: pluginA, + b: pluginB + } + }, + { + } + ], { + plugins: { + a: pluginA, + b: pluginB, + ...baseConfig.plugins + } + })); + + it("should error when attempting to redefine a plugin", async () => { + + await assertInvalidConfig([ + { + plugins: { + a: pluginA, + b: pluginB + } + }, + { + plugins: { + a: pluginC + } + } + ], "Cannot redefine plugin \"a\"."); + }); + + it("should error when plugin is not an object", async () => { + + await assertInvalidConfig([ + { + plugins: { + a: true + } + } + ], "Key \"a\": Expected an object."); + }); + + + }); + + describe("processor", () => { + + it("should merge two values when second is a string", () => { + + const stubProcessor = { + preprocess() {}, + postprocess() {} + }; + + return assertMergedResult([ + { + processor: { + preprocess() {}, + postprocess() {} + } + }, + { + plugins: { + markdown: { + processors: { + markdown: stubProcessor + } + } + }, + processor: "markdown/markdown" + } + ], { + plugins: { + markdown: { + processors: { + markdown: stubProcessor + } + }, + ...baseConfig.plugins + }, + processor: stubProcessor + }); + }); + + it("should merge two values when second is an object", () => { + + const processor = { + preprocess() { }, + postprocess() { } + }; + + return assertMergedResult([ + { + processor: "markdown/markdown" + }, + { + processor + } + ], { + plugins: baseConfig.plugins, + + processor + }); + }); + + it("should error when an invalid string is used", async () => { + + await assertInvalidConfig([ + { + processor: "foo" + } + ], "pluginName/objectName"); + }); + + it("should error when an empty string is used", async () => { + + await assertInvalidConfig([ + { + processor: "" + } + ], "pluginName/objectName"); + }); + + it("should error when an invalid processor is used", async () => { + await assertInvalidConfig([ + { + processor: {} + } + ], "Object must have a preprocess() and a postprocess() method."); + + }); + + it("should error when a processor cannot be found in a plugin", async () => { + await assertInvalidConfig([ + { + plugins: { + foo: {} + }, + processor: "foo/bar" + } + ], /Could not find "bar" in plugin "foo"/u); + + }); + + }); + + describe("linterOptions", () => { + + it("should error when an unexpected key is found", async () => { + + await assertInvalidConfig([ + { + linterOptions: { + foo: true + } + } + ], "Unexpected key \"foo\" found."); + + }); + + describe("noInlineConfig", () => { + + it("should error when an unexpected value is found", async () => { + + await assertInvalidConfig([ + { + linterOptions: { + noInlineConfig: "true" + } + } + ], "Expected a Boolean."); + }); + + it("should merge two objects when second object has overrides", () => assertMergedResult([ + { + linterOptions: { + noInlineConfig: true + } + }, + { + linterOptions: { + noInlineConfig: false + } + } + ], { + plugins: baseConfig.plugins, + + linterOptions: { + noInlineConfig: false + } + })); + + it("should merge an object and undefined into one object", () => assertMergedResult([ + { + linterOptions: { + noInlineConfig: false + } + }, + { + } + ], { + plugins: baseConfig.plugins, + + linterOptions: { + noInlineConfig: false + } + })); + + it("should merge undefined and an object into one object", () => assertMergedResult([ + { + }, + { + linterOptions: { + noInlineConfig: false + } + } + ], { + plugins: baseConfig.plugins, + + linterOptions: { + noInlineConfig: false + } + })); + + + }); + describe("reportUnusedDisableDirectives", () => { + + it("should error when an unexpected value is found", async () => { + + await assertInvalidConfig([ + { + linterOptions: { + reportUnusedDisableDirectives: "true" + } + } + ], /Expected a Boolean/u); + }); + + it("should merge two objects when second object has overrides", () => assertMergedResult([ + { + linterOptions: { + reportUnusedDisableDirectives: false + } + }, + { + linterOptions: { + reportUnusedDisableDirectives: true + } + } + ], { + plugins: baseConfig.plugins, + + linterOptions: { + reportUnusedDisableDirectives: true + } + })); + + it("should merge an object and undefined into one object", () => assertMergedResult([ + {}, + { + linterOptions: { + reportUnusedDisableDirectives: true + } + } + ], { + plugins: baseConfig.plugins, + + linterOptions: { + reportUnusedDisableDirectives: true + } + })); + + + }); + + }); + + describe("languageOptions", () => { + + it("should error when an unexpected key is found", async () => { + + await assertInvalidConfig([ + { + languageOptions: { + foo: true + } + } + ], "Unexpected key \"foo\" found."); + + }); + + it("should merge two languageOptions objects with different properties", () => assertMergedResult([ + { + languageOptions: { + ecmaVersion: 2019 + } + }, + { + languageOptions: { + sourceType: "commonjs" + } + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + ecmaVersion: 2019, + sourceType: "commonjs" + } + })); + + describe("ecmaVersion", () => { + + it("should error when an unexpected value is found", async () => { + + await assertInvalidConfig([ + { + languageOptions: { + ecmaVersion: "true" + } + } + ], "Expected a number."); + }); + + it("should merge two objects when second object has overrides", () => assertMergedResult([ + { + languageOptions: { + ecmaVersion: 2019 + } + }, + { + languageOptions: { + ecmaVersion: 2021 + } + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + ecmaVersion: 2021 + } + })); + + it("should merge an object and undefined into one object", () => assertMergedResult([ + { + languageOptions: { + ecmaVersion: 2021 + } + }, + { + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + ecmaVersion: 2021 + } + })); + + + it("should merge undefined and an object into one object", () => assertMergedResult([ + { + }, + { + languageOptions: { + ecmaVersion: 2021 + } + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + ecmaVersion: 2021 + } + })); + + + }); + + describe("sourceType", () => { + + it("should error when an unexpected value is found", async () => { + + await assertInvalidConfig([ + { + languageOptions: { + sourceType: "true" + } + } + ], "Expected \"script\", \"module\", or \"commonjs\"."); + }); + + it("should merge two objects when second object has overrides", () => assertMergedResult([ + { + languageOptions: { + sourceType: "module" + } + }, + { + languageOptions: { + sourceType: "script" + } + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + sourceType: "script" + } + })); + + it("should merge an object and undefined into one object", () => assertMergedResult([ + { + languageOptions: { + sourceType: "script" + } + }, + { + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + sourceType: "script" + } + })); + + + it("should merge undefined and an object into one object", () => assertMergedResult([ + { + }, + { + languageOptions: { + sourceType: "module" + } + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + sourceType: "module" + } + })); + + + }); + + describe("globals", () => { + + it("should error when an unexpected value is found", async () => { + + await assertInvalidConfig([ + { + languageOptions: { + globals: "true" + } + } + ], "Expected an object."); + }); + + it("should error when an unexpected key value is found", async () => { + + await assertInvalidConfig([ + { + languageOptions: { + globals: { + foo: "truex" + } + } + } + ], "Key \"foo\": Expected \"readonly\", \"writable\", or \"off\"."); + }); + + it("should error when a global has leading whitespace", async () => { + + await assertInvalidConfig([ + { + languageOptions: { + globals: { + " foo": "readonly" + } + } + } + ], /Global " foo" has leading or trailing whitespace/u); + }); + + it("should error when a global has trailing whitespace", async () => { + + await assertInvalidConfig([ + { + languageOptions: { + globals: { + "foo ": "readonly" + } + } + } + ], /Global "foo " has leading or trailing whitespace/u); + }); + + it("should merge two objects when second object has different keys", () => assertMergedResult([ + { + languageOptions: { + globals: { + foo: "readonly" + } + } + }, + { + languageOptions: { + globals: { + bar: "writable" + } + } + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + globals: { + foo: "readonly", + bar: "writable" + } + } + })); + + it("should merge two objects when second object has overrides", () => assertMergedResult([ + { + languageOptions: { + globals: { + foo: null + } + } + }, + { + languageOptions: { + globals: { + foo: "writeable" + } + } + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + globals: { + foo: "writeable" + } + } + })); + + it("should merge an object and undefined into one object", () => assertMergedResult([ + { + languageOptions: { + globals: { + foo: "readable" + } + } + }, + { + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + globals: { + foo: "readable" + } + } + })); + + + it("should merge undefined and an object into one object", () => assertMergedResult([ + { + }, + { + languageOptions: { + globals: { + foo: "false" + } + } + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + globals: { + foo: "false" + } + } + })); + + + }); + + describe("parser", () => { + + it("should error when an unexpected value is found", async () => { + + await assertInvalidConfig([ + { + languageOptions: { + parser: true + } + } + ], "Expected an object or string."); + }); + + it("should error when an unexpected value is found", async () => { + + await assertInvalidConfig([ + { + languageOptions: { + parser: "true" + } + } + ], /Expected string in the form "pluginName\/objectName"/u); + }); + + it("should error when a plugin parser can't be found", async () => { + + await assertInvalidConfig([ + { + languageOptions: { + parser: "foo/bar" + } + } + ], "Key \"parser\": Could not find \"bar\" in plugin \"foo\"."); + }); + + it("should error when a value doesn't have a parse() method", async () => { + + await assertInvalidConfig([ + { + languageOptions: { + parser: {} + } + } + ], "Expected object to have a parse() or parseForESLint() method."); + }); + + it("should merge two objects when second object has overrides", () => { + + const parser = { parse() {} }; + const stubParser = { parse() { } }; + + return assertMergedResult([ + { + languageOptions: { + parser + } + }, + { + plugins: { + "@foo/baz": { + parsers: { + bar: stubParser + } + } + }, + languageOptions: { + parser: "@foo/baz/bar" + } + } + ], { + plugins: { + "@foo/baz": { + parsers: { + bar: stubParser + } + }, + ...baseConfig.plugins + }, + languageOptions: { + parser: stubParser + } + }); + }); + + it("should merge an object and undefined into one object", () => { + + const stubParser = { parse() { } }; + + return assertMergedResult([ + { + plugins: { + foo: { + parsers: { + bar: stubParser + } + } + }, + + languageOptions: { + parser: "foo/bar" + } + }, + { + } + ], { + plugins: { + foo: { + parsers: { + bar: stubParser + } + }, + ...baseConfig.plugins + }, + + languageOptions: { + parser: stubParser + } + }); + + }); + + + it("should merge undefined and an object into one object", () => { + + const stubParser = { parse() {} }; + + return assertMergedResult([ + { + }, + { + plugins: { + foo: { + parsers: { + bar: stubParser + } + } + }, + + languageOptions: { + parser: "foo/bar" + } + } + ], { + plugins: { + foo: { + parsers: { + bar: stubParser + } + }, + ...baseConfig.plugins + }, + + languageOptions: { + parser: stubParser + } + }); + + }); + + }); + + + describe("parserOptions", () => { + + it("should error when an unexpected value is found", async () => { + + await assertInvalidConfig([ + { + languageOptions: { + parserOptions: "true" + } + } + ], "Expected an object."); + }); + + it("should merge two objects when second object has different keys", () => assertMergedResult([ + { + languageOptions: { + parserOptions: { + foo: "whatever" + } + } + }, + { + languageOptions: { + parserOptions: { + bar: "baz" + } + } + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + parserOptions: { + foo: "whatever", + bar: "baz" + } + } + })); + + it("should deeply merge two objects when second object has different keys", () => assertMergedResult([ + { + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + } + }, + { + languageOptions: { + parserOptions: { + ecmaFeatures: { + globalReturn: true + } + } + } + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + globalReturn: true + } + } + } + })); + + it("should deeply merge two objects when second object has missing key", () => assertMergedResult([ + { + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + } + }, + { + languageOptions: { + ecmaVersion: 2021 + } + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + ecmaVersion: 2021, + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + } + })); + + it("should merge two objects when second object has overrides", () => assertMergedResult([ + { + languageOptions: { + parserOptions: { + foo: "whatever" + } + } + }, + { + languageOptions: { + parserOptions: { + foo: "bar" + } + } + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + parserOptions: { + foo: "bar" + } + } + })); + + it("should merge an object and undefined into one object", () => assertMergedResult([ + { + languageOptions: { + parserOptions: { + foo: "whatever" + } + } + }, + { + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + parserOptions: { + foo: "whatever" + } + } + })); + + + it("should merge undefined and an object into one object", () => assertMergedResult([ + { + }, + { + languageOptions: { + parserOptions: { + foo: "bar" + } + } + } + ], { + plugins: baseConfig.plugins, + + languageOptions: { + parserOptions: { + foo: "bar" + } + } + })); + + + }); + + + }); + + describe("rules", () => { + + it("should error when an unexpected value is found", async () => { + + await assertInvalidConfig([ + { + rules: true + } + ], "Expected an object."); + }); + + it("should error when an invalid rule severity is set", async () => { + + await assertInvalidConfig([ + { + rules: { + foo: true + } + } + ], "Key \"rules\": Key \"foo\": Expected a string, number, or array."); + }); + + it("should error when an invalid rule severity of the right type is set", async () => { + + await assertInvalidConfig([ + { + rules: { + foo: 3 + } + } + ], "Key \"rules\": Key \"foo\": Expected severity of \"off\", 0, \"warn\", 1, \"error\", or 2."); + }); + + it("should error when an invalid rule severity is set in an array", async () => { + + await assertInvalidConfig([ + { + rules: { + foo: [true] + } + } + ], "Key \"rules\": Key \"foo\": Expected severity of \"off\", 0, \"warn\", 1, \"error\", or 2."); + }); + + it("should error when rule options don't match schema", async () => { + + await assertInvalidConfig([ + { + rules: { + foo: [1, "bar"] + } + } + ], /Value "bar" should be equal to one of the allowed values/u); + }); + + it("should error when rule options don't match schema requiring at least one item", async () => { + + await assertInvalidConfig([ + { + rules: { + foo2: 1 + } + } + ], /Value \[\] should NOT have fewer than 1 items/u); + }); + + it("should merge two objects", () => assertMergedResult([ + { + rules: { + foo: 1, + bar: "error" + } + }, + { + rules: { + baz: "warn", + boom: 0 + } + } + ], { + plugins: baseConfig.plugins, + + rules: { + foo: [1], + bar: [2], + baz: [1], + boom: [0] + } + })); + + it("should merge two objects when second object has simple overrides", () => assertMergedResult([ + { + rules: { + foo: [1, "always"], + bar: "error" + } + }, + { + rules: { + foo: "error", + bar: 0 + } + } + ], { + plugins: baseConfig.plugins, + + rules: { + foo: [2, "always"], + bar: [0] + } + })); + + it("should merge two objects when second object has array overrides", () => assertMergedResult([ + { + rules: { + foo: 1, + bar: "error" + } + }, + { + rules: { + foo: ["error", "never"], + bar: ["warn", "foo"] + } + } + ], { + plugins: baseConfig.plugins, + rules: { + foo: [2, "never"], + bar: [1, "foo"] + } + })); + + it("should merge two objects and options when second object overrides without options", () => assertMergedResult([ + { + rules: { + foo: [1, "always"], + bar: "error" + } + }, + { + plugins: { + "foo/baz/boom": { + rules: { + bang: {} + } + } + }, + rules: { + foo: ["error"], + bar: 0, + "foo/baz/boom/bang": "error" + } + } + ], { + plugins: { + ...baseConfig.plugins, + "foo/baz/boom": { + rules: { + bang: {} + } + } + }, + rules: { + foo: [2, "always"], + bar: [0], + "foo/baz/boom/bang": [2] + } + })); + + it("should merge an object and undefined into one object", () => assertMergedResult([ + { + rules: { + foo: 0, + bar: 1 + } + }, + { + } + ], { + plugins: baseConfig.plugins, + rules: { + foo: [0], + bar: [1] + } + })); + + it("should merge a rule that doesn't exist without error when the rule is off", () => assertMergedResult([ + { + rules: { + foo: 0, + bar: 1 + } + }, + { + rules: { + nonExistentRule: 0, + nonExistentRule2: ["off", "bar"] + } + } + ], { + plugins: baseConfig.plugins, + rules: { + foo: [0], + bar: [1], + nonExistentRule: [0], + nonExistentRule2: [0, "bar"] + } + })); + + }); + + }); +}); From 104c0b592f203d315a108d311c58375357e40b24 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Sat, 26 Jun 2021 17:15:16 +0530 Subject: [PATCH 54/67] Update: improve use-isnan rule to detect `Number.NaN` (fixes #14715) (#14718) * Update: improve `isNaNIdentifier` to detect `Number.isNaN` (fixes #14715) * Chore: add test cases for `Number.NaN` * Docs: add more examples for `use-isnan` * Chore: improve logic and add more test cases * Docs: Update docs/rules/use-isnan.md Co-authored-by: Milos Djermanovic Co-authored-by: Milos Djermanovic --- docs/rules/use-isnan.md | 48 +++++ lib/rules/use-isnan.js | 5 +- tests/lib/rules/use-isnan.js | 344 +++++++++++++++++++++++++++++++++++ 3 files changed, 396 insertions(+), 1 deletion(-) diff --git a/docs/rules/use-isnan.md b/docs/rules/use-isnan.md index ae9fefe36af..67c83c96a90 100644 --- a/docs/rules/use-isnan.md +++ b/docs/rules/use-isnan.md @@ -25,6 +25,14 @@ if (foo == NaN) { if (foo != NaN) { // ... } + +if (foo == Number.NaN) { + // ... +} + +if (foo != Number.NaN) { + // ... +} ``` Examples of **correct** code for this rule: @@ -77,6 +85,26 @@ switch (NaN) { break; // ... } + +switch (foo) { + case Number.NaN: + bar(); + break; + case 1: + baz(); + break; + // ... +} + +switch (Number.NaN) { + case a: + bar(); + break; + case b: + baz(); + break; + // ... +} ``` Examples of **correct** code for this rule with `"enforceForSwitchCase"` option set to `true` (default): @@ -126,6 +154,26 @@ switch (NaN) { break; // ... } + +switch (foo) { + case Number.NaN: + bar(); + break; + case 1: + baz(); + break; + // ... +} + +switch (Number.NaN) { + case a: + bar(); + break; + case b: + baz(); + break; + // ... +} ``` ### enforceForIndexOf diff --git a/lib/rules/use-isnan.js b/lib/rules/use-isnan.js index 0c7e888c976..ef95b21314a 100644 --- a/lib/rules/use-isnan.js +++ b/lib/rules/use-isnan.js @@ -21,7 +21,10 @@ const astUtils = require("./utils/ast-utils"); * @returns {boolean} `true` if the node is 'NaN' identifier. */ function isNaNIdentifier(node) { - return Boolean(node) && node.type === "Identifier" && node.name === "NaN"; + return Boolean(node) && ( + astUtils.isSpecificId(node, "NaN") || + astUtils.isSpecificMemberAccess(node, "Number", "NaN") + ); } //------------------------------------------------------------------------------ diff --git a/tests/lib/rules/use-isnan.js b/tests/lib/rules/use-isnan.js index c5a4b6bd686..a9bfe3a67d7 100644 --- a/tests/lib/rules/use-isnan.js +++ b/tests/lib/rules/use-isnan.js @@ -36,6 +36,19 @@ ruleTester.run("use-isnan", rule, { "foo(NaN / 2)", "foo(2 / NaN)", "var x; if (x = NaN) { }", + "var x = Number.NaN;", + "isNaN(Number.NaN) === true;", + "Number.isNaN(Number.NaN) === true;", + "foo(Number.NaN + 1);", + "foo(1 + Number.NaN);", + "foo(Number.NaN - 1)", + "foo(1 - Number.NaN)", + "foo(Number.NaN * 2)", + "foo(2 * Number.NaN)", + "foo(Number.NaN / 2)", + "foo(2 / Number.NaN)", + "var x; if (x = Number.NaN) { }", + "x === Number[NaN];", //------------------------------------------------------------------------------ // enforceForSwitchCase @@ -105,6 +118,62 @@ ruleTester.run("use-isnan", rule, { code: "switch(foo) { case bar: break; case 1: break; default: break; }", options: [{ enforceForSwitchCase: true }] }, + { + code: "switch(Number.NaN) { case foo: break; }", + options: [{ enforceForSwitchCase: false }] + }, + { + code: "switch(foo) { case Number.NaN: break; }", + options: [{ enforceForSwitchCase: false }] + }, + { + code: "switch(NaN) { case Number.NaN: break; }", + options: [{ enforceForSwitchCase: false }] + }, + { + code: "switch(foo) { case bar: break; case Number.NaN: break; default: break; }", + options: [{ enforceForSwitchCase: false }] + }, + { + code: "switch(foo) { case bar: Number.NaN; }", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo) { default: Number.NaN; }", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(Number.Nan) {}", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch('Number.NaN') { default: break; }", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo(Number.NaN)) {}", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo.Number.NaN) {}", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo) { case Number.Nan: break }", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo) { case 'Number.NaN': break }", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo) { case foo(Number.NaN): break }", + options: [{ enforceForSwitchCase: true }] + }, + { + code: "switch(foo) { case foo.Number.NaN: break }", + options: [{ enforceForSwitchCase: true }] + }, //------------------------------------------------------------------------------ // enforceForIndexOf @@ -112,6 +181,8 @@ ruleTester.run("use-isnan", rule, { "foo.indexOf(NaN)", "foo.lastIndexOf(NaN)", + "foo.indexOf(Number.NaN)", + "foo.lastIndexOf(Number.NaN)", { code: "foo.indexOf(NaN)", options: [{}] @@ -200,6 +271,79 @@ ruleTester.run("use-isnan", rule, { { code: "foo.lastIndexOf(NaN())", options: [{ enforceForIndexOf: true }] + }, + { + code: "foo.indexOf(Number.NaN)", + options: [{}] + }, + { + code: "foo.lastIndexOf(Number.NaN)", + options: [{}] + }, + { + code: "foo.indexOf(Number.NaN)", + options: [{ enforceForIndexOf: false }] + }, + { + code: "foo.lastIndexOf(Number.NaN)", + options: [{ enforceForIndexOf: false }] + }, + { + code: "indexOf(Number.NaN)", + options: [{ enforceForIndexOf: true }] + }, + { + code: "lastIndexOf(Number.NaN)", + options: [{ enforceForIndexOf: true }] + }, + { + code: "new foo.indexOf(Number.NaN)", + options: [{ enforceForIndexOf: true }] + }, + { + code: "foo.bar(Number.NaN)", + options: [{ enforceForIndexOf: true }] + }, + { + code: "foo.IndexOf(Number.NaN)", + options: [{ enforceForIndexOf: true }] + }, + { + code: "foo[indexOf](Number.NaN)", + options: [{ enforceForIndexOf: true }] + }, + { + code: "foo[lastIndexOf](Number.NaN)", + options: [{ enforceForIndexOf: true }] + }, + { + code: "indexOf.foo(Number.NaN)", + options: [{ enforceForIndexOf: true }] + }, + { + code: "foo.lastIndexOf(Number.Nan)", + options: [{ enforceForIndexOf: true }] + }, + { + code: "foo.indexOf(a, Number.NaN)", + options: [{ enforceForIndexOf: true }] + }, + { + code: "foo.lastIndexOf(Number.NaN, b)", + options: [{ enforceForIndexOf: true }] + }, + { + code: "foo.lastIndexOf(Number.NaN, NaN)", + options: [{ enforceForIndexOf: true }] + }, + { + code: "foo.indexOf(...Number.NaN)", + options: [{ enforceForIndexOf: true }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "foo.lastIndexOf(Number.NaN())", + options: [{ enforceForIndexOf: true }] } ], invalid: [ @@ -267,6 +411,79 @@ ruleTester.run("use-isnan", rule, { code: "\"abc\" >= NaN;", errors: [comparisonError] }, + { + code: "123 == Number.NaN;", + errors: [comparisonError] + }, + { + code: "123 === Number.NaN;", + errors: [comparisonError] + }, + { + code: "Number.NaN === \"abc\";", + errors: [comparisonError] + }, + { + code: "Number.NaN == \"abc\";", + errors: [comparisonError] + }, + { + code: "123 != Number.NaN;", + errors: [comparisonError] + }, + { + code: "123 !== Number.NaN;", + errors: [comparisonError] + }, + { + code: "Number.NaN !== \"abc\";", + errors: [comparisonError] + }, + { + code: "Number.NaN != \"abc\";", + errors: [comparisonError] + }, + { + code: "Number.NaN < \"abc\";", + errors: [comparisonError] + }, + { + code: "\"abc\" < Number.NaN;", + errors: [comparisonError] + }, + { + code: "Number.NaN > \"abc\";", + errors: [comparisonError] + }, + { + code: "\"abc\" > Number.NaN;", + errors: [comparisonError] + }, + { + code: "Number.NaN <= \"abc\";", + errors: [comparisonError] + }, + { + code: "\"abc\" <= Number.NaN;", + errors: [comparisonError] + }, + { + code: "Number.NaN >= \"abc\";", + errors: [comparisonError] + }, + { + code: "\"abc\" >= Number.NaN;", + errors: [comparisonError] + }, + { + code: "x === Number?.NaN;", + parserOptions: { ecmaVersion: 2020 }, + errors: [comparisonError] + }, + { + code: "x === Number['NaN'];", + errors: [comparisonError] + }, //------------------------------------------------------------------------------ // enforceForSwitchCase @@ -351,6 +568,85 @@ ruleTester.run("use-isnan", rule, { { messageId: "caseNaN", type: "SwitchCase", column: 15 } ] }, + { + code: "switch(Number.NaN) { case foo: break; }", + errors: [{ messageId: "switchNaN", type: "SwitchStatement", column: 1 }] + }, + { + code: "switch(foo) { case Number.NaN: break; }", + errors: [{ messageId: "caseNaN", type: "SwitchCase", column: 15 }] + }, + { + code: "switch(Number.NaN) { case foo: break; }", + options: [{}], + errors: [{ messageId: "switchNaN", type: "SwitchStatement", column: 1 }] + }, + { + code: "switch(foo) { case Number.NaN: break; }", + options: [{}], + errors: [{ messageId: "caseNaN", type: "SwitchCase", column: 15 }] + }, + { + code: "switch(Number.NaN) {}", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "switchNaN", type: "SwitchStatement", column: 1 }] + }, + { + code: "switch(Number.NaN) { case foo: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "switchNaN", type: "SwitchStatement", column: 1 }] + }, + { + code: "switch(Number.NaN) { default: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "switchNaN", type: "SwitchStatement", column: 1 }] + }, + { + code: "switch(Number.NaN) { case foo: break; default: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "switchNaN", type: "SwitchStatement", column: 1 }] + }, + { + code: "switch(foo) { case Number.NaN: }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "caseNaN", type: "SwitchCase", column: 15 }] + }, + { + code: "switch(foo) { case Number.NaN: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "caseNaN", type: "SwitchCase", column: 15 }] + }, + { + code: "switch(foo) { case (Number.NaN): break; }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "caseNaN", type: "SwitchCase", column: 15 }] + }, + { + code: "switch(foo) { case bar: break; case Number.NaN: break; default: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "caseNaN", type: "SwitchCase", column: 32 }] + }, + { + code: "switch(foo) { case bar: case Number.NaN: default: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [{ messageId: "caseNaN", type: "SwitchCase", column: 25 }] + }, + { + code: "switch(foo) { case bar: break; case NaN: break; case baz: break; case Number.NaN: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [ + { messageId: "caseNaN", type: "SwitchCase", column: 32 }, + { messageId: "caseNaN", type: "SwitchCase", column: 66 } + ] + }, + { + code: "switch(Number.NaN) { case Number.NaN: break; }", + options: [{ enforceForSwitchCase: true }], + errors: [ + { messageId: "switchNaN", type: "SwitchStatement", column: 1 }, + { messageId: "caseNaN", type: "SwitchCase", column: 22 } + ] + }, //------------------------------------------------------------------------------ // enforceForIndexOf @@ -403,6 +699,54 @@ ruleTester.run("use-isnan", rule, { options: [{ enforceForIndexOf: true }], parserOptions: { ecmaVersion: 2020 }, errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + }, + { + code: "foo.indexOf(Number.NaN)", + options: [{ enforceForIndexOf: true }], + errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }] + }, + { + code: "foo.lastIndexOf(Number.NaN)", + options: [{ enforceForIndexOf: true }], + errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }] + }, + { + code: "foo['indexOf'](Number.NaN)", + options: [{ enforceForIndexOf: true }], + errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }] + }, + { + code: "foo['lastIndexOf'](Number.NaN)", + options: [{ enforceForIndexOf: true }], + errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }] + }, + { + code: "foo().indexOf(Number.NaN)", + options: [{ enforceForIndexOf: true }], + errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "indexOf" } }] + }, + { + code: "foo.bar.lastIndexOf(Number.NaN)", + options: [{ enforceForIndexOf: true }], + errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }] + }, + { + code: "foo.indexOf?.(Number.NaN)", + options: [{ enforceForIndexOf: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + }, + { + code: "foo?.indexOf(Number.NaN)", + options: [{ enforceForIndexOf: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + }, + { + code: "(foo?.indexOf)(Number.NaN)", + options: [{ enforceForIndexOf: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] } ] }); From ed1da5d96af2587b7211854e45cf8657ef808710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=9B=E5=AE=9A=E8=B0=94=E7=9A=84=E7=8C=AB?= Date: Sat, 26 Jun 2021 19:54:10 +0800 Subject: [PATCH 55/67] Update: ecmaVersion allows "latest" (#14720) * Revert "Revert "Update: ecmaVersion defaults to 5, and allows "latest" (#14622)" (#14711)" This reverts commit 97d9bd2a8061e61e98ebabb4c41231af1df7629f. * chore: use parser.$parser to check if it's espree * chore: add some tests * chore: not set default 5 * chore: make the $parser non-enumerable * chore: use symbol * chore: a small refactor --- .../configuring/language-options.md | 4 +- lib/linter/linter.js | 28 +-- lib/rule-tester/rule-tester.js | 6 + .../fixtures/parsers/empty-program-parser.js | 27 +++ tests/lib/linter/linter.js | 52 +++++ tests/lib/rule-tester/rule-tester.js | 200 +++++++++++++++++- 6 files changed, 302 insertions(+), 15 deletions(-) create mode 100644 tests/fixtures/parsers/empty-program-parser.js diff --git a/docs/user-guide/configuring/language-options.md b/docs/user-guide/configuring/language-options.md index eb3fe8a0afe..08b62aad571 100644 --- a/docs/user-guide/configuring/language-options.md +++ b/docs/user-guide/configuring/language-options.md @@ -187,7 +187,7 @@ For ES6 syntax, use `{ "parserOptions": { "ecmaVersion": 6 } }`; for new ES6 glo Parser options are set in your `.eslintrc.*` file by using the `parserOptions` property. The available options are: -* `ecmaVersion` - set to 3, 5 (default), 6, 7, 8, 9, 10, 11, or 12 to specify the version of ECMAScript syntax you want to use. You can also set to 2015 (same as 6), 2016 (same as 7), 2017 (same as 8), 2018 (same as 9), 2019 (same as 10), 2020 (same as 11), or 2021 (same as 12) to use the year-based naming. +* `ecmaVersion` - set to 3, 5 (default), 6, 7, 8, 9, 10, 11, or 12 to specify the version of ECMAScript syntax you want to use. You can also set to 2015 (same as 6), 2016 (same as 7), 2017 (same as 8), 2018 (same as 9), 2019 (same as 10), 2020 (same as 11), or 2021 (same as 12) to use the year-based naming. You can also set "latest" to use the most recently supported version. * `sourceType` - set to `"script"` (default) or `"module"` if your code is in ECMAScript modules. * `ecmaFeatures` - an object indicating which additional language features you'd like to use: * `globalReturn` - allow `return` statements in the global scope @@ -199,7 +199,7 @@ Here's an example `.eslintrc.json` file: ```json { "parserOptions": { - "ecmaVersion": 6, + "ecmaVersion": "latest", "sourceType": "module", "ecmaFeatures": { "jsx": true diff --git a/lib/linter/linter.js b/lib/linter/linter.js index e94b507b5dd..4e80926a895 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -37,8 +37,10 @@ const const debug = require("debug")("eslint:linter"); const MAX_AUTOFIX_PASSES = 10; const DEFAULT_PARSER_NAME = "espree"; +const DEFAULT_ECMA_VERSION = 5; const commentParser = new ConfigCommentParser(); const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }; +const parserSymbol = Symbol.for("eslint.RuleTester.parser"); //------------------------------------------------------------------------------ // Typedefs @@ -432,10 +434,16 @@ function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) { /** * Normalize ECMAScript version from the initial config - * @param {number} ecmaVersion ECMAScript version from the initial config + * @param {Parser} parser The parser which uses this options. + * @param {number} ecmaVersion ECMAScript version from the initial config * @returns {number} normalized ECMAScript version */ -function normalizeEcmaVersion(ecmaVersion) { +function normalizeEcmaVersion(parser, ecmaVersion) { + if ((parser[parserSymbol] || parser) === espree) { + if (ecmaVersion === "latest") { + return espree.latestEcmaVersion; + } + } /* * Calculate ECMAScript edition number from official year version starting with @@ -521,12 +529,13 @@ function normalizeVerifyOptions(providedOptions, config) { /** * Combines the provided parserOptions with the options from environments - * @param {string} parserName The parser name which uses this options. + * @param {Parser} parser The parser which uses this options. * @param {ParserOptions} providedOptions The provided 'parserOptions' key in a config * @param {Environment[]} enabledEnvironments The environments enabled in configuration and with inline comments * @returns {ParserOptions} Resulting parser options after merge */ -function resolveParserOptions(parserName, providedOptions, enabledEnvironments) { +function resolveParserOptions(parser, providedOptions, enabledEnvironments) { + const parserOptionsFromEnv = enabledEnvironments .filter(env => env.parserOptions) .reduce((parserOptions, env) => merge(parserOptions, env.parserOptions), {}); @@ -542,12 +551,7 @@ function resolveParserOptions(parserName, providedOptions, enabledEnvironments) mergedParserOptions.ecmaFeatures = Object.assign({}, mergedParserOptions.ecmaFeatures, { globalReturn: false }); } - /* - * TODO: @aladdin-add - * 1. for a 3rd-party parser, do not normalize parserOptions - * 2. for espree, no need to do this (espree will do it) - */ - mergedParserOptions.ecmaVersion = normalizeEcmaVersion(mergedParserOptions.ecmaVersion); + mergedParserOptions.ecmaVersion = normalizeEcmaVersion(parser, mergedParserOptions.ecmaVersion); return mergedParserOptions; } @@ -606,7 +610,7 @@ function getRuleOptions(ruleConfig) { */ function analyzeScope(ast, parserOptions, visitorKeys) { const ecmaFeatures = parserOptions.ecmaFeatures || {}; - const ecmaVersion = parserOptions.ecmaVersion || 5; + const ecmaVersion = parserOptions.ecmaVersion || DEFAULT_ECMA_VERSION; return eslintScope.analyze(ast, { ignoreEval: true, @@ -1123,7 +1127,7 @@ class Linter { .map(envName => getEnv(slots, envName)) .filter(env => env); - const parserOptions = resolveParserOptions(parserName, config.parserOptions || {}, enabledEnvs); + const parserOptions = resolveParserOptions(parser, config.parserOptions || {}, enabledEnvs); const configuredGlobals = resolveGlobals(config.globals || {}, enabledEnvs); const settings = config.settings || {}; diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index cac81bc71d1..2b5524923be 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -53,6 +53,7 @@ const const ajv = require("../shared/ajv")({ strictDefaults: true }); const espreePath = require.resolve("espree"); +const parserSymbol = Symbol.for("eslint.RuleTester.parser"); //------------------------------------------------------------------------------ // Typedefs @@ -239,6 +240,7 @@ function defineStartEndAsError(objName, node) { }); } + /** * Define `start`/`end` properties of all nodes of the given AST as throwing error. * @param {ASTNode} ast The root node to errorize `start`/`end` properties. @@ -258,8 +260,10 @@ function defineStartEndAsErrorInTree(ast, visitorKeys) { * @returns {Parser} Wrapped parser object. */ function wrapParser(parser) { + if (typeof parser.parseForESLint === "function") { return { + [parserSymbol]: parser, parseForESLint(...args) { const ret = parser.parseForESLint(...args); @@ -268,7 +272,9 @@ function wrapParser(parser) { } }; } + return { + [parserSymbol]: parser, parse(...args) { const ast = parser.parse(...args); diff --git a/tests/fixtures/parsers/empty-program-parser.js b/tests/fixtures/parsers/empty-program-parser.js new file mode 100644 index 00000000000..7f336cdbef1 --- /dev/null +++ b/tests/fixtures/parsers/empty-program-parser.js @@ -0,0 +1,27 @@ +"use strict"; + +exports.parse = function (text, parserOptions) { + return { + "type": "Program", + "start": 0, + "end": 0, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 0 + } + }, + "range": [ + 0, + 0 + ], + "body": [], + "sourceType": "script", + "comments": [], + "tokens": [] + }; +}; diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index adce5a39d86..522f9b19726 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -11,6 +11,7 @@ const assert = require("chai").assert, sinon = require("sinon"), + espree = require("espree"), esprima = require("esprima"), testParsers = require("../../fixtures/parsers/linter-test-parsers"); @@ -3492,6 +3493,57 @@ var a = "test2"; }); describe("ecmaVersion", () => { + + it("should not support ES6 when no ecmaVersion provided", () => { + const messages = linter.verify("let x = 0;"); + + assert.strictEqual(messages.length, 1); + }); + + it("supports ECMAScript version 'latest'", () => { + const messages = linter.verify("let x = 5 ** 7;", { + parserOptions: { ecmaVersion: "latest" } + }); + + assert.strictEqual(messages.length, 0); + }); + + it("the 'latest' is equal to espree.lastEcmaVersion", () => { + let ecmaVersion = null; + const config = { rules: { "ecma-version": 2 }, parserOptions: { ecmaVersion: "latest" } }; + + linter.defineRule("ecma-version", context => ({ + Program() { + ecmaVersion = context.parserOptions.ecmaVersion; + } + })); + linter.verify("", config); + assert.strictEqual(ecmaVersion, espree.latestEcmaVersion); + }); + + it("should pass normalized ecmaVersion to eslint-scope", () => { + let blockScope = null; + + linter.defineRule("block-scope", context => ({ + BlockStatement() { + blockScope = context.getScope(); + } + })); + + linter.verify("{}", { + rules: { "block-scope": 2 }, + parserOptions: { ecmaVersion: "latest" } + }); + + assert.strictEqual(blockScope.type, "block"); + + linter.verify("{}", { + rules: { "block-scope": 2 }, + parserOptions: {} // ecmaVersion defaults to 5 + }); + assert.strictEqual(blockScope.type, "global"); + }); + describe("it should properly parse let declaration when", () => { it("the ECMAScript version number is 6", () => { const messages = linter.verify("let x = 5;", { diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 70647b18670..71225611dfe 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -11,7 +11,8 @@ const sinon = require("sinon"), EventEmitter = require("events"), { RuleTester } = require("../../../lib/rule-tester"), assert = require("chai").assert, - nodeAssert = require("assert"); + nodeAssert = require("assert"), + espree = require("espree"); const NODE_ASSERT_STRICT_EQUAL_OPERATOR = (() => { try { @@ -1041,6 +1042,203 @@ describe("RuleTester", () => { }); assert.strictEqual(spy.args[1][1].parser, require.resolve("esprima")); }); + it("should pass normalized ecmaVersion to the rule", () => { + const reportEcmaVersionRule = { + meta: { + messages: { + ecmaVersionMessage: "context.parserOptions.ecmaVersion is {{type}} {{ecmaVersion}}." + } + }, + create: context => ({ + Program(node) { + const { ecmaVersion } = context.parserOptions; + + context.report({ + node, + messageId: "ecmaVersionMessage", + data: { type: typeof ecmaVersion, ecmaVersion } + }); + } + }) + }; + + const notEspree = require.resolve("../../fixtures/parsers/empty-program-parser"); + + ruleTester.run("report-ecma-version", reportEcmaVersionRule, { + valid: [], + invalid: [ + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }] + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], + parserOptions: {} + }, + { + code: "
", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], + parserOptions: { ecmaFeatures: { jsx: true } } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], + parser: require.resolve("espree") + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], + env: { browser: true } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], + env: { es6: false } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], + env: { es6: true } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "8" } }], + env: { es6: false, es2017: true } + }, + { + code: "let x", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], + env: { es6: "truthy" } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "8" } }], + env: { es2017: true } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "11" } }], + env: { es2020: true } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "12" } }], + env: { es2021: true } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], + parserOptions: { ecmaVersion: "latest" } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], + parser: require.resolve("espree"), + parserOptions: { ecmaVersion: "latest" } + }, + { + code: "
", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], + parserOptions: { ecmaVersion: "latest", ecmaFeatures: { jsx: true } } + }, + { + code: "import 'foo'", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], + parserOptions: { ecmaVersion: "latest", sourceType: "module" } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], + parserOptions: { ecmaVersion: "latest" }, + env: { es6: true } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }], + parserOptions: { ecmaVersion: "latest" }, + env: { es2020: true } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], + parser: notEspree + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }], + parser: notEspree, + parserOptions: {} + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "5" } }], + parser: notEspree, + parserOptions: { ecmaVersion: 5 } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], + parser: notEspree, + parserOptions: { ecmaVersion: 6 } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], + parser: notEspree, + parserOptions: { ecmaVersion: 2015 } + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "string", ecmaVersion: "latest" } }], + parser: notEspree, + parserOptions: { ecmaVersion: "latest" } + } + ] + }); + + [{ parserOptions: { ecmaVersion: 6 } }, { env: { es6: true } }].forEach(options => { + new RuleTester(options).run("report-ecma-version", reportEcmaVersionRule, { + valid: [], + invalid: [ + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }] + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }], + parserOptions: {} + } + ] + }); + }); + + new RuleTester({ parser: notEspree }).run("report-ecma-version", reportEcmaVersionRule, { + valid: [], + invalid: [ + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }] + }, + { + code: "", + errors: [{ messageId: "ecmaVersionMessage", data: { type: "string", ecmaVersion: "latest" } }], + parserOptions: { ecmaVersion: "latest" } + } + ] + }); + }); it("should pass-through services from parseForESLint to the rule", () => { const enhancedParserPath = require.resolve("../../fixtures/parsers/enhanced-parser"); From 278813a6e759f6b5512ac64c7530c9c51732e692 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Sun, 27 Jun 2021 00:31:45 +0530 Subject: [PATCH 56/67] Docs: fix and add more examples for new-cap rule (fixes #12874) (#14725) * Docs: fix and add more examples for `new-cap` rule * Docs: update * Docs: update * Docs: update * Docs: Update docs/rules/new-cap.md Co-authored-by: Milos Djermanovic Co-authored-by: Milos Djermanovic --- docs/rules/new-cap.md | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/docs/rules/new-cap.md b/docs/rules/new-cap.md index 22ac57729dc..ef8c61508c6 100644 --- a/docs/rules/new-cap.md +++ b/docs/rules/new-cap.md @@ -112,15 +112,24 @@ var emitter = new events(); ### newIsCapExceptionPattern -Examples of additional **correct** code for this rule with the `{ "newIsCapExceptionPattern": "^person\.." }` option: +Examples of additional **correct** code for this rule with the `{ "newIsCapExceptionPattern": "^person\\.." }` option: ```js -/*eslint new-cap: ["error", { "newIsCapExceptionPattern": "^person\.." }]*/ +/*eslint new-cap: ["error", { "newIsCapExceptionPattern": "^person\\.." }]*/ var friend = new person.acquaintance(); + var bestFriend = new person.friend(); ``` +Examples of additional **correct** code for this rule with the `{ "newIsCapExceptionPattern": "\\.bar$" }` option: + +```js +/*eslint new-cap: ["error", { "newIsCapExceptionPattern": "\\.bar$" }]*/ + +var friend = new person.bar(); +``` + ### capIsNewExceptions Examples of additional **correct** code for this rule with the `{ "capIsNewExceptions": ["Person"] }` option: @@ -135,15 +144,35 @@ function foo(arg) { ### capIsNewExceptionPattern -Examples of additional **correct** code for this rule with the `{ "capIsNewExceptionPattern": "^Person\.." }` option: +Examples of additional **correct** code for this rule with the `{ "capIsNewExceptionPattern": "^person\\.." }` option: ```js -/*eslint new-cap: ["error", { "capIsNewExceptionPattern": "^Person\.." }]*/ +/*eslint new-cap: ["error", { "capIsNewExceptionPattern": "^person\\.." }]*/ var friend = person.Acquaintance(); var bestFriend = person.Friend(); ``` +Examples of additional **correct** code for this rule with the `{ "capIsNewExceptionPattern": "\\.Bar$" }` option: + +```js +/*eslint new-cap: ["error", { "capIsNewExceptionPattern": "\\.Bar$" }]*/ + +foo.Bar(); +``` + +Examples of additional **correct** code for this rule with the `{ "capIsNewExceptionPattern": "^Foo" }` option: + +```js +/*eslint new-cap: ["error", { "capIsNewExceptionPattern": "^Foo" }]*/ + +var x = Foo(42); + +var y = Foobar(42); + +var z = Foo.Bar(42); +``` + ### properties Examples of **incorrect** code for this rule with the default `{ "properties": true }` option: From aa87329d919f569404ca573b439934552006572f Mon Sep 17 00:00:00 2001 From: Sam Chen Date: Tue, 29 Jun 2021 23:27:14 +0800 Subject: [PATCH 57/67] Docs: fix broken links (#14756) * Docs: remove superfluous whitespace * Docs: update links --- docs/developer-guide/working-with-rules.md | 2 +- docs/rules/no-inner-declarations.md | 2 +- docs/user-guide/configuring/configuration-files.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md index a269e1c5fa4..c15f77c4cd1 100644 --- a/docs/developer-guide/working-with-rules.md +++ b/docs/developer-guide/working-with-rules.md @@ -66,7 +66,7 @@ The source file for a rule exports an object with the following properties. In a custom rule or plugin, you can omit `docs` or include any properties that you need in it. -* `fixable` (string) is either `"code"` or `"whitespace"` if the `--fix` option on the [command line](../user-guide/command-line-interface.md#-fix) automatically fixes problems reported by the rule +* `fixable` (string) is either `"code"` or `"whitespace"` if the `--fix` option on the [command line](../user-guide/command-line-interface.md#--fix) automatically fixes problems reported by the rule **Important:** the `fixable` property is mandatory for fixable rules. If this property isn't specified, ESLint will throw an error whenever the rule attempts to produce a fix. Omit the `fixable` property if the rule is not fixable. diff --git a/docs/rules/no-inner-declarations.md b/docs/rules/no-inner-declarations.md index 707fc6e781f..9bdfaa7c400 100644 --- a/docs/rules/no-inner-declarations.md +++ b/docs/rules/no-inner-declarations.md @@ -1,4 +1,4 @@ -# disallow variable or `function` declarations in nested blocks (no-inner-declarations) +# disallow variable or `function` declarations in nested blocks (no-inner-declarations) In JavaScript, prior to ES6, a function declaration is only allowed in the first level of a program or the body of another function, though parsers sometimes [erroneously accept them elsewhere](https://code.google.com/p/esprima/issues/detail?id=422). This only applies to function declarations; named or anonymous function expressions can occur anywhere an expression is permitted. diff --git a/docs/user-guide/configuring/configuration-files.md b/docs/user-guide/configuring/configuration-files.md index e8aa6bf0e77..bf9b70d12db 100644 --- a/docs/user-guide/configuring/configuration-files.md +++ b/docs/user-guide/configuring/configuration-files.md @@ -37,7 +37,7 @@ The second way to use configuration files is to save the file wherever you would eslint -c myconfig.json myfiletotest.js -If you are using one configuration file and want ESLint to ignore any `.eslintrc.*` files, make sure to use [`--no-eslintrc`](https://eslint.org/docs/user-guide/command-line-interface#-no-eslintrc) along with the [`-c`](https://eslint.org/docs/user-guide/command-line-interface#-c-config) flag. +If you are using one configuration file and want ESLint to ignore any `.eslintrc.*` files, make sure to use [`--no-eslintrc`](https://eslint.org/docs/user-guide/command-line-interface#--no-eslintrc) along with the [`-c`](https://eslint.org/docs/user-guide/command-line-interface#-c---config) flag. ### Comments in configuration files From 19a871a35ae9997ce352624b1081c96c54b73a9f Mon Sep 17 00:00:00 2001 From: Bryan Mishkin <698306+bmish@users.noreply.github.com> Date: Fri, 2 Jul 2021 12:17:42 -0600 Subject: [PATCH 58/67] Docs: Suggest linting plugins for ESLint plugin developers (#14754) --- docs/developer-guide/working-with-plugins.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/developer-guide/working-with-plugins.md b/docs/developer-guide/working-with-plugins.md index 3809e3700d0..bea47021932 100644 --- a/docs/developer-guide/working-with-plugins.md +++ b/docs/developer-guide/working-with-plugins.md @@ -216,6 +216,14 @@ The plugin support was introduced in ESLint version `0.8.0`. Ensure the `peerDep ESLint provides the [`RuleTester`](/docs/developer-guide/nodejs-api.md#ruletester) utility to make it easy to test the rules of your plugin. +### Linting + +ESLint plugins should be linted too! It's suggested to lint your plugin with the `recommended` configurations of: + +* [eslint](https://www.npmjs.com/package/eslint) +* [eslint-plugin-eslint-plugin](https://www.npmjs.com/package/eslint-plugin-eslint-plugin) +* [eslint-plugin-node](https://www.npmjs.com/package/eslint-plugin-node) + ## Share Plugins In order to make your plugin available to the community you have to publish it on npm. From 5f746420700d457b92dd86659de588d272937b79 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 2 Jul 2021 22:35:50 +0200 Subject: [PATCH 59/67] Chore: don't check Program.start in SourceCode#getComments (refs #14744) (#14748) --- lib/source-code/source-code.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/source-code/source-code.js b/lib/source-code/source-code.js index c13ce29b877..cc4524fa74c 100644 --- a/lib/source-code/source-code.js +++ b/lib/source-code/source-code.js @@ -349,7 +349,7 @@ class SourceCode extends TokenStore { let currentToken = this.getTokenBefore(node, { includeComments: true }); while (currentToken && isCommentToken(currentToken)) { - if (node.parent && (currentToken.start < node.parent.start)) { + if (node.parent && node.parent.type !== "Program" && (currentToken.start < node.parent.start)) { break; } comments.leading.push(currentToken); @@ -361,7 +361,7 @@ class SourceCode extends TokenStore { currentToken = this.getTokenAfter(node, { includeComments: true }); while (currentToken && isCommentToken(currentToken)) { - if (node.parent && (currentToken.end > node.parent.end)) { + if (node.parent && node.parent.type !== "Program" && (currentToken.end > node.parent.end)) { break; } comments.trailing.push(currentToken); From 21a85b1b90a8ee16b0999796c1191944c43a47f5 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 2 Jul 2021 18:02:22 -0400 Subject: [PATCH 60/67] Build: changelog update for 7.30.0 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8336f27ab..f4a316be0f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +v7.30.0 - July 2, 2021 + +* [`5f74642`](https://github.com/eslint/eslint/commit/5f746420700d457b92dd86659de588d272937b79) Chore: don't check Program.start in SourceCode#getComments (refs #14744) (#14748) (Milos Djermanovic) +* [`19a871a`](https://github.com/eslint/eslint/commit/19a871a35ae9997ce352624b1081c96c54b73a9f) Docs: Suggest linting plugins for ESLint plugin developers (#14754) (Bryan Mishkin) +* [`aa87329`](https://github.com/eslint/eslint/commit/aa87329d919f569404ca573b439934552006572f) Docs: fix broken links (#14756) (Sam Chen) +* [`278813a`](https://github.com/eslint/eslint/commit/278813a6e759f6b5512ac64c7530c9c51732e692) Docs: fix and add more examples for new-cap rule (fixes #12874) (#14725) (Nitin Kumar) +* [`ed1da5d`](https://github.com/eslint/eslint/commit/ed1da5d96af2587b7211854e45cf8657ef808710) Update: ecmaVersion allows "latest" (#14720) (薛定谔的猫) +* [`104c0b5`](https://github.com/eslint/eslint/commit/104c0b592f203d315a108d311c58375357e40b24) Update: improve use-isnan rule to detect `Number.NaN` (fixes #14715) (#14718) (Nitin Kumar) +* [`b08170b`](https://github.com/eslint/eslint/commit/b08170b92beb22db6ec612ebdfff930f9e0582ab) Update: Implement FlatConfigArray (refs #13481) (#14321) (Nicholas C. Zakas) +* [`f113cdd`](https://github.com/eslint/eslint/commit/f113cdd872257d72bbd66d95e4eaf13623323b24) Chore: upgrade eslint-plugin-eslint-plugin (#14738) (薛定谔的猫) +* [`1b8997a`](https://github.com/eslint/eslint/commit/1b8997ab63781f4ebf87e3269400b2ef4c7d2973) Docs: Fix getRulesMetaForResults link syntax (#14723) (Brandon Mills) +* [`aada733`](https://github.com/eslint/eslint/commit/aada733d2aee830aa32cccb9828cd72db4ccd6bd) Docs: fix two broken links (#14726) (Sam Chen) +* [`8972529`](https://github.com/eslint/eslint/commit/8972529f82d13bd04059ee8852b4ebb9b5350962) Docs: Update README team and sponsors (ESLint Jenkins) + v7.29.0 - June 18, 2021 * [`bfbfe5c`](https://github.com/eslint/eslint/commit/bfbfe5c1fd4c39a06d5e159dbe48479ca4305fc0) New: Add only to RuleTester (refs eslint/rfcs#73) (#14677) (Brandon Mills) From e837d811e9275e43ccc266aba10e9c0833e0376c Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 2 Jul 2021 18:02:23 -0400 Subject: [PATCH 61/67] 7.30.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d53e1e9aa6..ac3a78cc3b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "7.29.0", + "version": "7.30.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From a6a7438502abc6a1e29ec35cfbe2058ffc0803b1 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Tue, 6 Jul 2021 04:53:07 +0200 Subject: [PATCH 62/67] Chore: pin fs-teardown@0.1.1 (#14771) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ac3a78cc3b0..8fabe0a68f5 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "eslint-release": "^2.0.0", "eslump": "^3.0.0", "esprima": "^4.0.1", - "fs-teardown": "^0.1.0", + "fs-teardown": "0.1.1", "glob": "^7.1.6", "jsdoc": "^3.5.5", "karma": "^6.1.1", From 000cc796fd487e7b9ba8bcc5857dd691044479cc Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Wed, 7 Jul 2021 19:11:41 -0400 Subject: [PATCH 63/67] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c87d07e0dcd..68982812e3b 100644 --- a/README.md +++ b/README.md @@ -288,7 +288,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Automattic

Gold Sponsors

Nx (by Nrwl) Chrome's Web Framework & Tools Performance Fund Salesforce Airbnb Coinbase Substack

Silver Sponsors

Retool Liftoff

Bronze Sponsors

-

Anagram Solver Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Fire Stick Tricks Practice Ignition

+

Anagram Solver Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Fire Stick Tricks Practice Ignition

## Technology Sponsors From b0d22e3eff18ea7f08189134c07cddceaec69a09 Mon Sep 17 00:00:00 2001 From: Bryan Mishkin <698306+bmish@users.noreply.github.com> Date: Thu, 8 Jul 2021 02:16:45 -0400 Subject: [PATCH 64/67] Docs: Mention benefit of providing `meta.docs.url` (#14774) Perhaps the top benefit of rules providing the `meta.docs.url` property with a link to their documentation is that IDEs / code editors can then provide a helpful link on rule violations. Developers often find this link useful to better understand a violation and learn how to fix it. However, many rule authors are unaware of this feature and thus exclude the property because they don't see the value of it. Mentioning this feature in ESLint's rule documentation should help encourage greater adoption of this property. Note that there is also a lint rule to require and autofix this property: https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-docs-url.md --- docs/developer-guide/working-with-rules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md index c15f77c4cd1..77e05da1034 100644 --- a/docs/developer-guide/working-with-rules.md +++ b/docs/developer-guide/working-with-rules.md @@ -61,7 +61,7 @@ The source file for a rule exports an object with the following properties. * `description` (string) provides the short description of the rule in the [rules index](../rules/) * `category` (string) specifies the heading under which the rule is listed in the [rules index](../rules/) * `recommended` (boolean) is whether the `"extends": "eslint:recommended"` property in a [configuration file](../user-guide/configuring/configuration-files.md#extending-configuration-files) enables the rule - * `url` (string) specifies the URL at which the full documentation can be accessed + * `url` (string) specifies the URL at which the full documentation can be accessed (enabling code editors to provide a helpful link on highlighted rule violations) * `suggestion` (boolean) specifies whether rules can return suggestions (defaults to false if omitted) In a custom rule or plugin, you can omit `docs` or include any properties that you need in it. From bba714c2ed813821ed288fbc07722cdde6e534fe Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Fri, 9 Jul 2021 15:09:09 -0500 Subject: [PATCH 65/67] Update: Clarifying what changes need to be made in no-mixed-operators (#14765) --- lib/rules/no-mixed-operators.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/no-mixed-operators.js b/lib/rules/no-mixed-operators.js index 15eb20bed2a..5a2e139a620 100644 --- a/lib/rules/no-mixed-operators.js +++ b/lib/rules/no-mixed-operators.js @@ -117,7 +117,7 @@ module.exports = { ], messages: { - unexpectedMixedOperator: "Unexpected mix of '{{leftOperator}}' and '{{rightOperator}}'." + unexpectedMixedOperator: "Unexpected mix of '{{leftOperator}}' and '{{rightOperator}}'. Use parentheses to clarify the intended order of operations." } }, From ddffa8ad58b4b124b08061e9045fdb5370cbdbe3 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Sat, 10 Jul 2021 16:15:42 -0500 Subject: [PATCH 66/67] Update: Indicating the operator in question (#14764) --- lib/rules/operator-assignment.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/rules/operator-assignment.js b/lib/rules/operator-assignment.js index fdb0884922b..a48d2725197 100644 --- a/lib/rules/operator-assignment.js +++ b/lib/rules/operator-assignment.js @@ -76,8 +76,8 @@ module.exports = { fixable: "code", messages: { - replaced: "Assignment can be replaced with operator assignment.", - unexpected: "Unexpected operator assignment shorthand." + replaced: "Assignment (=) can be replaced with operator assignment ({{operator}}=).", + unexpected: "Unexpected operator assignment ({{operator}}=) shorthand." } }, @@ -113,6 +113,7 @@ module.exports = { context.report({ node, messageId: "replaced", + data: { operator }, fix(fixer) { if (canBeFixed(left) && canBeFixed(expr.left)) { const equalsToken = getOperatorToken(node); @@ -139,7 +140,8 @@ module.exports = { */ context.report({ node, - messageId: "replaced" + messageId: "replaced", + data: { operator } }); } } @@ -155,6 +157,7 @@ module.exports = { context.report({ node, messageId: "unexpected", + data: { operator: node.operator }, fix(fixer) { if (canBeFixed(node.left)) { const firstToken = sourceCode.getFirstToken(node); From 9a3c73c130d437a65f4edba0dcb63390e68cac41 Mon Sep 17 00:00:00 2001 From: Sam Chen Date: Tue, 13 Jul 2021 04:42:57 +0800 Subject: [PATCH 67/67] Docs: fix a broken link (#14790) --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index daa56e76479..ec53551c538 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -462,7 +462,7 @@ const codeLines = SourceCode.splitLines(code); The `Linter` object does the actual evaluation of the JavaScript code. It doesn't do any filesystem operations, it simply parses and reports on the code. In particular, the `Linter` object does not process configuration objects or files. The `Linter` is a constructor, and you can create a new instance by passing in the options you want to use. The available options are: -* `cwd` - Path to a directory that should be considered as the current working directory. It is accessible to rules by calling `context.getCwd()` (see [The Context Object](./working-with-rules.md#The-Context-Object)). If `cwd` is `undefined`, it will be normalized to `process.cwd()` if the global `process` object is defined (for example, in the Node.js runtime) , or `undefined` otherwise. +* `cwd` - Path to a directory that should be considered as the current working directory. It is accessible to rules by calling `context.getCwd()` (see [The Context Object](./working-with-rules.md#the-context-object)). If `cwd` is `undefined`, it will be normalized to `process.cwd()` if the global `process` object is defined (for example, in the Node.js runtime) , or `undefined` otherwise. For example: