From de77c11e7515f2097ff355ddc0d7b6db9c83c892 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 22 Jun 2020 19:26:21 -0700 Subject: [PATCH 01/66] Fix: Replace Infinity with Number.MAX_SAFE_INTEGER (fixes #13427) (#13435) --- lib/cli-engine/config-array-factory.js | 7 ++++++- .../config-file/cloned-config/configWithInfinity.js | 6 ++++++ tests/lib/cli.js | 12 ++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/config-file/cloned-config/configWithInfinity.js diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index 6d0992151ad..39f62bb6da1 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -722,7 +722,12 @@ class ConfigArrayFactory { * * Refer https://github.com/eslint/eslint/issues/12592 */ - const clonedRulesConfig = rules && JSON.parse(JSON.stringify((rules))); + const clonedRulesConfig = rules && JSON.parse( + JSON.stringify( + rules, + (key, value) => (value === Infinity ? Number.MAX_SAFE_INTEGER : value) + ) + ); // Flatten `extends`. for (const extendName of extendList.filter(Boolean)) { diff --git a/tests/fixtures/config-file/cloned-config/configWithInfinity.js b/tests/fixtures/config-file/cloned-config/configWithInfinity.js new file mode 100644 index 00000000000..73e3c93c085 --- /dev/null +++ b/tests/fixtures/config-file/cloned-config/configWithInfinity.js @@ -0,0 +1,6 @@ +module.exports = { + + rules: { + "max-len": [ "error", { code: Infinity }] + } +}; diff --git a/tests/lib/cli.js b/tests/lib/cli.js index afb225f1f7c..2f84900eefb 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -1172,9 +1172,7 @@ describe("cli", () => { assert.strictEqual(exit, 0); }); - }); - describe("config file and input file", () => { it("should exit with 1 as camelcase has wrong property type", async () => { const configPath = getFixturePath("config-file", "cloned-config", "eslintConfigFail.js"); const filePath = getFixturePath("config-file", "cloned-config", "index.js"); @@ -1187,6 +1185,16 @@ describe("cli", () => { } }); + + it("should not cause an error when a rule configuration has `Infinity`", async () => { + const configPath = getFixturePath("config-file", "cloned-config", "configWithInfinity.js"); + const filePath = getFixturePath("config-file", "cloned-config", "index.js"); + const args = `--config ${configPath} ${filePath}`; + + const exit = await cli.execute(args); + + assert.strictEqual(exit, 0); + }); }); describe("inline config and input file", () => { From 44ee84277a2faa188dda56fe8f6fc1d80fae3f8f Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Mon, 22 Jun 2020 22:34:45 -0400 Subject: [PATCH 02/66] Build: changelog update for 7.3.1 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa7a033d35a..3474642e4be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +v7.3.1 - June 22, 2020 + +* [`de77c11`](https://github.com/eslint/eslint/commit/de77c11e7515f2097ff355ddc0d7b6db9c83c892) Fix: Replace Infinity with Number.MAX_SAFE_INTEGER (fixes #13427) (#13435) (Nicholas C. Zakas) v7.3.0 - June 19, 2020 * [`638a6d6`](https://github.com/eslint/eslint/commit/638a6d6be18b4a37cfdc7223e1f5acd3718694be) Update: add missing `additionalProperties: false` to some rules' schema (#13198) (Milos Djermanovic) From bcf17771dc681473210c86271cbb408bc2551190 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Mon, 22 Jun 2020 22:34:46 -0400 Subject: [PATCH 03/66] 7.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 46154d1ab3e..78a6ebeed62 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "7.3.0", + "version": "7.3.1", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From 004adae3f959414f56e44e5884f6221e9dcda142 Mon Sep 17 00:00:00 2001 From: Kai Cataldo Date: Thu, 25 Jun 2020 13:17:48 -0400 Subject: [PATCH 04/66] Update: rename id-blacklist to id-denylist (fixes #13407) (#13408) * Update: rename id-blacklist to id-denylist (fixes #13407) * Update index.js --- .../rules/{id-blacklist.md => id-denylist.md} | 16 +- lib/rules/{id-blacklist.js => id-denylist.js} | 24 +- lib/rules/index.js | 5 +- tests/lib/rules/id-blacklist.js | 1359 ----------------- tests/lib/rules/id-denylist.js | 1359 +++++++++++++++++ tools/rule-types.json | 4 +- 6 files changed, 1385 insertions(+), 1382 deletions(-) rename docs/rules/{id-blacklist.md => id-denylist.md} (67%) rename lib/rules/{id-blacklist.js => id-denylist.js} (89%) create mode 100644 tests/lib/rules/id-denylist.js diff --git a/docs/rules/id-blacklist.md b/docs/rules/id-denylist.md similarity index 67% rename from docs/rules/id-blacklist.md rename to docs/rules/id-denylist.md index a7685c4c2f2..040f26e894c 100644 --- a/docs/rules/id-blacklist.md +++ b/docs/rules/id-denylist.md @@ -1,20 +1,20 @@ -# disallow specified identifiers (id-blacklist) +# disallow specified identifiers (id-denylist) > "There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton -Bad names can lead to hard-to-decipher code. Generic names, such as `data`, don't infer much about the code and the values it receives. This rule allows you to configure a blacklist of bad identifier names, that you don't want to see in your code. +Generic names can lead to hard-to-decipher code. This rule allows you to specify a deny list of disallowed identifier names to avoid this practice. ## Rule Details This rule disallows specified identifiers in assignments and `function` definitions. -This rule will catch blacklisted identifiers that are: +This rule will catch disallowed identifiers that are: - variable declarations - function declarations - object properties assigned to during object creation -It will not catch blacklisted identifiers that are: +It will not catch disallowed identifiers that are: - function calls (so you can still use functions you do not have control over) - object properties (so you can still use objects you do not have control over) @@ -27,14 +27,14 @@ For example, to restrict the use of common generic identifiers: ```json { - "id-blacklist": ["error", "data", "err", "e", "cb", "callback"] + "id-denylist": ["error", "data", "err", "e", "cb", "callback"] } ``` Examples of **incorrect** code for this rule with sample `"data", "callback"` restricted identifiers: ```js -/*eslint id-blacklist: ["error", "data", "callback"] */ +/*eslint id-denylist: ["error", "data", "callback"] */ var data = {...}; @@ -54,7 +54,7 @@ var itemSet = { Examples of **correct** code for this rule with sample `"data", "callback"` restricted identifiers: ```js -/*eslint id-blacklist: ["error", "data", "callback"] */ +/*eslint id-denylist: ["error", "data", "callback"] */ var encodingOptions = {...}; @@ -79,4 +79,4 @@ foo.data; // all property names that are not assignments are ignored ## When Not To Use It -You can turn this rule off if you are happy for identifiers to be named freely. +You can turn this rule off if you do not want to restrict the use of certain identifiers. diff --git a/lib/rules/id-blacklist.js b/lib/rules/id-denylist.js similarity index 89% rename from lib/rules/id-blacklist.js rename to lib/rules/id-denylist.js index d77a35d41b6..112fd8a9d55 100644 --- a/lib/rules/id-blacklist.js +++ b/lib/rules/id-denylist.js @@ -1,6 +1,6 @@ /** * @fileoverview Rule that warns when identifier names that are - * blacklisted in the configuration are used. + * specified in the configuration are used. * @author Keith Cirkel (http://keithcirkel.co.uk) */ @@ -117,7 +117,7 @@ module.exports = { description: "disallow specified identifiers", category: "Stylistic Issues", recommended: false, - url: "https://eslint.org/docs/rules/id-blacklist" + url: "https://eslint.org/docs/rules/id-denylist" }, schema: { @@ -128,25 +128,25 @@ module.exports = { uniqueItems: true }, messages: { - blacklisted: "Identifier '{{name}}' is blacklisted." + restricted: "Identifier '{{name}}' is restricted." } }, create(context) { - const blacklist = new Set(context.options); + const denyList = new Set(context.options); const reportedNodes = new Set(); let globalScope; /** - * Checks whether the given name is blacklisted. + * Checks whether the given name is restricted. * @param {string} name The name to check. - * @returns {boolean} `true` if the name is blacklisted. + * @returns {boolean} `true` if the name is restricted. * @private */ - function isBlacklisted(name) { - return blacklist.has(name); + function isRestricted(name) { + return denyList.has(name); } /** @@ -172,8 +172,8 @@ module.exports = { /* * Member access has special rules for checking property names. - * Read access to a property with a blacklisted name is allowed, because it can be on an object that user has no control over. - * Write access isn't allowed, because it potentially creates a new property with a blacklisted name. + * Read access to a property with a restricted name is allowed, because it can be on an object that user has no control over. + * Write access isn't allowed, because it potentially creates a new property with a restricted name. */ if ( parent.type === "MemberExpression" && @@ -205,7 +205,7 @@ module.exports = { if (!reportedNodes.has(node)) { context.report({ node, - messageId: "blacklisted", + messageId: "restricted", data: { name: node.name } @@ -221,7 +221,7 @@ module.exports = { }, Identifier(node) { - if (isBlacklisted(node.name) && shouldCheck(node)) { + if (isRestricted(node.name) && shouldCheck(node)) { report(node); } } diff --git a/lib/rules/index.js b/lib/rules/index.js index 567cd4a0eca..e34d090899a 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -56,7 +56,10 @@ module.exports = new LazyLoadingRuleMap(Object.entries({ "grouped-accessor-pairs": () => require("./grouped-accessor-pairs"), "guard-for-in": () => require("./guard-for-in"), "handle-callback-err": () => require("./handle-callback-err"), - "id-blacklist": () => require("./id-blacklist"), + + // Renamed to id-denylist. + "id-blacklist": () => require("./id-denylist"), + "id-denylist": () => require("./id-denylist"), "id-length": () => require("./id-length"), "id-match": () => require("./id-match"), "implicit-arrow-linebreak": () => require("./implicit-arrow-linebreak"), diff --git a/tests/lib/rules/id-blacklist.js b/tests/lib/rules/id-blacklist.js index 6c3f729e17a..e69de29bb2d 100644 --- a/tests/lib/rules/id-blacklist.js +++ b/tests/lib/rules/id-blacklist.js @@ -1,1359 +0,0 @@ -/** - * @fileoverview Tests for id-blacklist rule. - * @author Keith Cirkel - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const rule = require("../../../lib/rules/id-blacklist"), - { RuleTester } = require("../../../lib/rule-tester"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -const ruleTester = new RuleTester(); -const error = { messageId: "blacklisted", type: "Identifier" }; - -ruleTester.run("id-blacklist", rule, { - valid: [ - { - code: "foo = \"bar\"", - options: ["bar"] - }, - { - code: "bar = \"bar\"", - options: ["foo"] - }, - { - code: "foo = \"bar\"", - options: ["f", "fo", "fooo", "bar"] - }, - { - code: "function foo(){}", - options: ["bar"] - }, - { - code: "foo()", - options: ["f", "fo", "fooo", "bar"] - }, - { - code: "import { foo as bar } from 'mod'", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" } - }, - { - code: "export { foo as bar } from 'mod'", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" } - }, - { - code: "foo.bar()", - options: ["f", "fo", "fooo", "b", "ba", "baz"] - }, - { - code: "var foo = bar.baz;", - options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz"] - }, - { - code: "var foo = bar.baz.bing;", - options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] - }, - { - code: "foo.bar.baz = bing.bong.bash;", - options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] - }, - { - code: "if (foo.bar) {}", - options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] - }, - { - code: "var obj = { key: foo.bar };", - options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] - }, - { - code: "const {foo: bar} = baz", - options: ["foo"], - parserOptions: { ecmaVersion: 6 } - }, - { - code: "const {foo: {bar: baz}} = qux", - options: ["foo", "bar"], - parserOptions: { ecmaVersion: 6 } - }, - { - code: "function foo({ bar: baz }) {}", - options: ["bar"], - parserOptions: { ecmaVersion: 6 } - }, - { - code: "function foo({ bar: {baz: qux} }) {}", - options: ["bar", "baz"], - parserOptions: { ecmaVersion: 6 } - }, - { - code: "function foo({baz} = obj.qux) {}", - options: ["qux"], - parserOptions: { ecmaVersion: 6 } - }, - { - code: "function foo({ foo: {baz} = obj.qux }) {}", - options: ["qux"], - parserOptions: { ecmaVersion: 6 } - }, - { - code: "({a: bar = obj.baz});", - options: ["baz"], - parserOptions: { ecmaVersion: 6 } - }, - { - code: "({foo: {a: bar = obj.baz}} = qux);", - options: ["baz"], - parserOptions: { ecmaVersion: 6 } - }, - { - code: "var arr = [foo.bar];", - options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] - }, - { - code: "[foo.bar]", - options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] - }, - { - code: "[foo.bar.nesting]", - options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] - }, - { - code: "if (foo.bar === bar.baz) { [foo.bar] }", - options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] - }, - { - code: "var myArray = new Array(); var myDate = new Date();", - options: ["array", "date", "mydate", "myarray", "new", "var"] - }, - { - code: "foo()", - options: ["foo"] - }, - { - code: "foo.bar()", - options: ["bar"] - }, - { - code: "foo.bar", - options: ["bar"] - }, - { - code: "({foo: obj.bar.bar.bar.baz} = {});", - options: ["foo", "bar"], - parserOptions: { ecmaVersion: 6 } - }, - { - code: "({[obj.bar]: a = baz} = qux);", - options: ["bar"], - parserOptions: { ecmaVersion: 6 } - }, - - // references to global variables - { - code: "Number.parseInt()", - options: ["Number"] - }, - { - code: "x = Number.NaN;", - options: ["Number"] - }, - { - code: "var foo = undefined;", - options: ["undefined"] - }, - { - code: "if (foo === undefined);", - options: ["undefined"] - }, - { - code: "obj[undefined] = 5;", // creates obj["undefined"]. It should be disallowed, but the rule doesn't know values of globals and can't control computed access. - options: ["undefined"] - }, - { - code: "foo = { [myGlobal]: 1 };", - options: ["myGlobal"], - parserOptions: { ecmaVersion: 6 }, - globals: { myGlobal: "readonly" } - }, - { - code: "({ myGlobal } = foo);", // writability doesn't affect the logic, it's always assumed that user doesn't have control over the names of globals. - options: ["myGlobal"], - parserOptions: { ecmaVersion: 6 }, - globals: { myGlobal: "writable" } - }, - { - code: "/* global myGlobal: readonly */ myGlobal = 5;", - options: ["myGlobal"] - }, - { - code: "var foo = [Map];", - options: ["Map"], - env: { es6: true } - }, - { - code: "var foo = { bar: window.baz };", - options: ["window"], - env: { browser: true } - } - ], - invalid: [ - { - code: "foo = \"bar\"", - options: ["foo"], - errors: [ - error - ] - }, - { - code: "bar = \"bar\"", - options: ["bar"], - errors: [ - error - ] - }, - { - code: "foo = \"bar\"", - options: ["f", "fo", "foo", "bar"], - errors: [ - error - ] - }, - { - code: "function foo(){}", - options: ["f", "fo", "foo", "bar"], - errors: [ - error - ] - }, - { - code: "import foo from 'mod'", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [ - error - ] - }, - { - code: "import * as foo from 'mod'", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [ - error - ] - }, - { - code: "export * as foo from 'mod'", - options: ["foo"], - parserOptions: { ecmaVersion: 2020, sourceType: "module" }, - errors: [ - error - ] - }, - { - code: "import { foo } from 'mod'", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [ - error - ] - }, - { - code: "import { foo as bar } from 'mod'", - options: ["bar"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [{ - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 17 - }] - }, - { - code: "import { foo as bar } from 'mod'", - options: ["foo", "bar"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [{ - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 17 - }] - }, - { - code: "import { foo as foo } from 'mod'", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [{ - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 17 - }] - }, - { - code: "import { foo, foo as bar } from 'mod'", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [{ - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 10 - }] - }, - { - code: "import { foo as bar, foo } from 'mod'", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [{ - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 22 - }] - }, - { - code: "import foo, { foo as bar } from 'mod'", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [{ - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 8 - }] - }, - { - code: "var foo; export { foo as bar };", - options: ["bar"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [{ - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 26 - }] - }, - { - code: "var foo; export { foo };", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [ - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 5 - }, - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 19 - } - ] - }, - { - code: "var foo; export { foo as bar };", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [ - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 5 - }, - - // reports each occurrence of local identifier, although it's renamed in this export specifier - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 19 - } - ] - }, - { - code: "var foo; export { foo as foo };", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [ - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 5 - }, - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 19 - }, - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 26 - } - ] - }, - { - code: "var foo; export { foo as bar };", - options: ["foo", "bar"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [ - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 5 - }, - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 19 - }, - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 26 - } - ] - }, - { - code: "export { foo } from 'mod'", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [ - error - ] - }, - { - code: "export { foo as bar } from 'mod'", - options: ["bar"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [{ - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 17 - }] - }, - { - code: "export { foo as bar } from 'mod'", - options: ["foo", "bar"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [{ - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 17 - }] - }, - { - code: "export { foo as foo } from 'mod'", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [{ - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 17 - }] - }, - { - code: "export { foo, foo as bar } from 'mod'", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [{ - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 10 - }] - }, - { - code: "export { foo as bar, foo } from 'mod'", - options: ["foo"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [{ - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 22 - }] - }, - { - code: "foo.bar()", - options: ["f", "fo", "foo", "b", "ba", "baz"], - errors: [ - error - ] - }, - { - code: "foo[bar] = baz;", - options: ["bar"], - errors: [{ - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier" - }] - }, - { - code: "baz = foo[bar];", - options: ["bar"], - errors: [{ - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier" - }] - }, - { - code: "var foo = bar.baz;", - options: ["f", "fo", "foo", "b", "ba", "barr", "bazz"], - errors: [ - error - ] - }, - { - code: "var foo = bar.baz;", - options: ["f", "fo", "fooo", "b", "ba", "bar", "bazz"], - errors: [ - error - ] - }, - { - code: "if (foo.bar) {}", - options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], - errors: [ - error - ] - }, - { - code: "var obj = { key: foo.bar };", - options: ["obj"], - errors: [ - error - ] - }, - { - code: "var obj = { key: foo.bar };", - options: ["key"], - errors: [ - error - ] - }, - { - code: "var obj = { key: foo.bar };", - options: ["foo"], - errors: [ - error - ] - }, - { - code: "var arr = [foo.bar];", - options: ["arr"], - errors: [ - error - ] - }, - { - code: "var arr = [foo.bar];", - options: ["foo"], - errors: [ - error - ] - }, - { - code: "[foo.bar]", - options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], - errors: [ - error - ] - }, - { - code: "if (foo.bar === bar.baz) { [bing.baz] }", - options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], - errors: [ - error - ] - }, - { - code: "if (foo.bar === bar.baz) { [foo.bar] }", - options: ["f", "fo", "fooo", "b", "ba", "bar", "bazz", "bingg"], - errors: [ - error - ] - }, - { - code: "var myArray = new Array(); var myDate = new Date();", - options: ["array", "date", "myDate", "myarray", "new", "var"], - errors: [ - error - ] - }, - { - code: "var myArray = new Array(); var myDate = new Date();", - options: ["array", "date", "mydate", "myArray", "new", "var"], - errors: [ - error - ] - }, - { - code: "foo.bar = 1", - options: ["bar"], - errors: [ - error - ] - }, - { - code: "foo.bar.baz = 1", - options: ["bar", "baz"], - errors: [ - error - ] - }, - { - code: "const {foo} = baz", - options: ["foo"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 8 - } - ] - }, - { - code: "const {foo: bar} = baz", - options: ["foo", "bar"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 13 - } - ] - }, - { - code: "const {[foo]: bar} = baz", - options: ["foo", "bar"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 9 - }, - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 15 - } - ] - }, - { - code: "const {foo: {bar: baz}} = qux", - options: ["foo", "bar", "baz"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "baz" }, - type: "Identifier", - column: 19 - } - ] - }, - { - code: "const {foo: {[bar]: baz}} = qux", - options: ["foo", "bar", "baz"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 15 - }, - { - messageId: "blacklisted", - data: { name: "baz" }, - type: "Identifier", - column: 21 - } - ] - }, - { - code: "const {[foo]: {[bar]: baz}} = qux", - options: ["foo", "bar", "baz"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 9 - }, - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 17 - }, - { - messageId: "blacklisted", - data: { name: "baz" }, - type: "Identifier", - column: 23 - } - ] - }, - { - code: "function foo({ bar: baz }) {}", - options: ["bar", "baz"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "baz" }, - type: "Identifier", - column: 21 - } - ] - }, - { - code: "function foo({ bar: {baz: qux} }) {}", - options: ["bar", "baz", "qux"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "qux" }, - type: "Identifier", - column: 27 - } - ] - }, - { - code: "({foo: obj.bar} = baz);", - options: ["foo", "bar"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 12 - } - ] - }, - { - code: "({foo: obj.bar.bar.bar.baz} = {});", - options: ["foo", "bar", "baz"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "baz" }, - type: "Identifier", - column: 24 - } - ] - }, - { - code: "({[foo]: obj.bar} = baz);", - options: ["foo", "bar"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 4 - }, - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 14 - } - ] - }, - { - code: "({foo: { a: obj.bar }} = baz);", - options: ["bar"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 17 - } - ] - }, - { - code: "({a: obj.bar = baz} = qux);", - options: ["bar"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 10 - } - ] - }, - { - code: "({a: obj.bar.bar.baz = obj.qux} = obj.qux);", - options: ["a", "bar", "baz", "qux"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "baz" }, - type: "Identifier", - column: 18 - } - ] - }, - { - code: "({a: obj[bar] = obj.qux} = obj.qux);", - options: ["a", "bar", "baz", "qux"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 10 - } - ] - }, - { - code: "({a: [obj.bar] = baz} = qux);", - options: ["bar"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 11 - } - ] - }, - { - code: "({foo: { a: obj.bar = baz}} = qux);", - options: ["bar"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 17 - } - ] - }, - { - code: "({foo: { [a]: obj.bar }} = baz);", - options: ["bar"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 19 - } - ] - }, - { - code: "({...obj.bar} = baz);", - options: ["bar"], - parserOptions: { ecmaVersion: 9 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 10 - } - ] - }, - { - code: "([obj.bar] = baz);", - options: ["bar"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 7 - } - ] - }, - { - code: "const [bar] = baz;", - options: ["bar"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "bar" }, - type: "Identifier", - column: 8 - } - ] - }, - - // not a reference to a global variable, because it isn't a reference to a variable - { - code: "foo.undefined = 1;", - options: ["undefined"], - errors: [ - { - messageId: "blacklisted", - data: { name: "undefined" }, - type: "Identifier" - } - ] - }, - { - code: "var foo = { undefined: 1 };", - options: ["undefined"], - errors: [ - { - messageId: "blacklisted", - data: { name: "undefined" }, - type: "Identifier" - } - ] - }, - { - code: "var foo = { undefined: undefined };", - options: ["undefined"], - errors: [ - { - messageId: "blacklisted", - data: { name: "undefined" }, - type: "Identifier", - column: 13 - } - ] - }, - { - code: "var foo = { Number() {} };", - options: ["Number"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "Number" }, - type: "Identifier" - } - ] - }, - { - code: "class Foo { Number() {} }", - options: ["Number"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "Number" }, - type: "Identifier" - } - ] - }, - { - code: "myGlobal: while(foo) { break myGlobal; } ", - options: ["myGlobal"], - globals: { myGlobal: "readonly" }, - errors: [ - { - messageId: "blacklisted", - data: { name: "myGlobal" }, - type: "Identifier", - column: 1 - }, - { - messageId: "blacklisted", - data: { name: "myGlobal" }, - type: "Identifier", - column: 30 - } - ] - }, - - // globals declared in the given source code are not excluded from consideration - { - code: "const foo = 1; bar = foo;", - options: ["foo"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 7 - }, - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 22 - } - ] - }, - { - code: "let foo; foo = bar;", - options: ["foo"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 5 - }, - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 10 - } - ] - }, - { - code: "bar = foo; var foo;", - options: ["foo"], - errors: [ - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 7 - }, - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 16 - } - ] - }, - { - code: "function foo() {} var bar = foo;", - options: ["foo"], - errors: [ - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 10 - }, - { - messageId: "blacklisted", - data: { name: "foo" }, - type: "Identifier", - column: 29 - } - ] - }, - { - code: "class Foo {} var bar = Foo;", - options: ["Foo"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "Foo" }, - type: "Identifier", - column: 7 - }, - { - messageId: "blacklisted", - data: { name: "Foo" }, - type: "Identifier", - column: 24 - } - ] - }, - - // redeclared globals are not excluded from consideration - { - code: "let undefined; undefined = 1;", - options: ["undefined"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "undefined" }, - type: "Identifier", - column: 5 - }, - { - messageId: "blacklisted", - data: { name: "undefined" }, - type: "Identifier", - column: 16 - } - ] - }, - { - code: "foo = undefined; var undefined;", - options: ["undefined"], - errors: [ - { - messageId: "blacklisted", - data: { name: "undefined" }, - type: "Identifier", - column: 7 - }, - { - messageId: "blacklisted", - data: { name: "undefined" }, - type: "Identifier", - column: 22 - } - ] - }, - { - code: "function undefined(){} x = undefined;", - options: ["undefined"], - errors: [ - { - messageId: "blacklisted", - data: { name: "undefined" }, - type: "Identifier", - column: 10 - }, - { - messageId: "blacklisted", - data: { name: "undefined" }, - type: "Identifier", - column: 28 - } - ] - }, - { - code: "class Number {} x = Number.NaN;", - options: ["Number"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "Number" }, - type: "Identifier", - column: 7 - }, - { - messageId: "blacklisted", - data: { name: "Number" }, - type: "Identifier", - column: 21 - } - ] - }, - - /* - * Assignment to a property with a blacklisted name isn't allowed, in general. - * In this case, that restriction prevents creating a global variable with a blacklisted name. - */ - { - code: "/* globals myGlobal */ window.myGlobal = 5; foo = myGlobal;", - options: ["myGlobal"], - env: { browser: true }, - errors: [ - { - messageId: "blacklisted", - data: { name: "myGlobal" }, - type: "Identifier", - column: 31 - } - ] - }, - - // disabled global variables - { - code: "var foo = undefined;", - options: ["undefined"], - globals: { undefined: "off" }, - errors: [ - { - messageId: "blacklisted", - data: { name: "undefined" }, - type: "Identifier" - } - ] - }, - { - code: "/* globals Number: off */ Number.parseInt()", - options: ["Number"], - errors: [ - { - messageId: "blacklisted", - data: { name: "Number" }, - type: "Identifier" - } - ] - }, - { - code: "var foo = [Map];", // this actually isn't a disabled global: it was never enabled because es6 environment isn't enabled - options: ["Map"], - errors: [ - { - messageId: "blacklisted", - data: { name: "Map" }, - type: "Identifier" - } - ] - }, - - // shadowed global variables - { - code: "if (foo) { let undefined; bar = undefined; }", - options: ["undefined"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "undefined" }, - type: "Identifier", - column: 16 - }, - { - messageId: "blacklisted", - data: { name: "undefined" }, - type: "Identifier", - column: 33 - } - ] - }, - { - code: "function foo(Number) { var x = Number.NaN; }", - options: ["Number"], - errors: [ - { - messageId: "blacklisted", - data: { name: "Number" }, - type: "Identifier", - column: 14 - }, - { - messageId: "blacklisted", - data: { name: "Number" }, - type: "Identifier", - column: 32 - } - ] - }, - { - code: "function foo() { var myGlobal; x = myGlobal; }", - options: ["myGlobal"], - globals: { myGlobal: "readonly" }, - errors: [ - { - messageId: "blacklisted", - data: { name: "myGlobal" }, - type: "Identifier", - column: 22 - }, - { - messageId: "blacklisted", - data: { name: "myGlobal" }, - type: "Identifier", - column: 36 - } - ] - }, - { - code: "function foo(bar) { return Number.parseInt(bar); } const Number = 1;", - options: ["Number"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [ - { - messageId: "blacklisted", - data: { name: "Number" }, - type: "Identifier", - column: 28 - }, - { - messageId: "blacklisted", - data: { name: "Number" }, - type: "Identifier", - column: 58 - } - ] - }, - { - code: "import Number from 'myNumber'; const foo = Number.parseInt(bar);", - options: ["Number"], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [ - { - messageId: "blacklisted", - data: { name: "Number" }, - type: "Identifier", - column: 8 - }, - { - messageId: "blacklisted", - data: { name: "Number" }, - type: "Identifier", - column: 44 - } - ] - }, - { - code: "var foo = function undefined() {};", - options: ["undefined"], - errors: [ - { - messageId: "blacklisted", - data: { name: "undefined" }, - type: "Identifier" - } - ] - }, - - // this is a reference to a global variable, but at the same time creates a property with a blacklisted name - { - code: "var foo = { undefined }", - options: ["undefined"], - parserOptions: { ecmaVersion: 6 }, - errors: [ - { - messageId: "blacklisted", - data: { name: "undefined" }, - type: "Identifier" - } - ] - } - ] -}); diff --git a/tests/lib/rules/id-denylist.js b/tests/lib/rules/id-denylist.js new file mode 100644 index 00000000000..6da179d99ff --- /dev/null +++ b/tests/lib/rules/id-denylist.js @@ -0,0 +1,1359 @@ +/** + * @fileoverview Tests for id-denylist rule. + * @author Keith Cirkel + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/id-denylist"), + { RuleTester } = require("../../../lib/rule-tester"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); +const error = { messageId: "restricted", type: "Identifier" }; + +ruleTester.run("id-denylist", rule, { + valid: [ + { + code: "foo = \"bar\"", + options: ["bar"] + }, + { + code: "bar = \"bar\"", + options: ["foo"] + }, + { + code: "foo = \"bar\"", + options: ["f", "fo", "fooo", "bar"] + }, + { + code: "function foo(){}", + options: ["bar"] + }, + { + code: "foo()", + options: ["f", "fo", "fooo", "bar"] + }, + { + code: "import { foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" } + }, + { + code: "export { foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" } + }, + { + code: "foo.bar()", + options: ["f", "fo", "fooo", "b", "ba", "baz"] + }, + { + code: "var foo = bar.baz;", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz"] + }, + { + code: "var foo = bar.baz.bing;", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "foo.bar.baz = bing.bong.bash;", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "if (foo.bar) {}", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "var obj = { key: foo.bar };", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "const {foo: bar} = baz", + options: ["foo"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "const {foo: {bar: baz}} = qux", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({ bar: baz }) {}", + options: ["bar"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({ bar: {baz: qux} }) {}", + options: ["bar", "baz"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({baz} = obj.qux) {}", + options: ["qux"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({ foo: {baz} = obj.qux }) {}", + options: ["qux"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "({a: bar = obj.baz});", + options: ["baz"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "({foo: {a: bar = obj.baz}} = qux);", + options: ["baz"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "var arr = [foo.bar];", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "[foo.bar]", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "[foo.bar.nesting]", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "if (foo.bar === bar.baz) { [foo.bar] }", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "var myArray = new Array(); var myDate = new Date();", + options: ["array", "date", "mydate", "myarray", "new", "var"] + }, + { + code: "foo()", + options: ["foo"] + }, + { + code: "foo.bar()", + options: ["bar"] + }, + { + code: "foo.bar", + options: ["bar"] + }, + { + code: "({foo: obj.bar.bar.bar.baz} = {});", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "({[obj.bar]: a = baz} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 } + }, + + // references to global variables + { + code: "Number.parseInt()", + options: ["Number"] + }, + { + code: "x = Number.NaN;", + options: ["Number"] + }, + { + code: "var foo = undefined;", + options: ["undefined"] + }, + { + code: "if (foo === undefined);", + options: ["undefined"] + }, + { + code: "obj[undefined] = 5;", // creates obj["undefined"]. It should be disallowed, but the rule doesn't know values of globals and can't control computed access. + options: ["undefined"] + }, + { + code: "foo = { [myGlobal]: 1 };", + options: ["myGlobal"], + parserOptions: { ecmaVersion: 6 }, + globals: { myGlobal: "readonly" } + }, + { + code: "({ myGlobal } = foo);", // writability doesn't affect the logic, it's always assumed that user doesn't have control over the names of globals. + options: ["myGlobal"], + parserOptions: { ecmaVersion: 6 }, + globals: { myGlobal: "writable" } + }, + { + code: "/* global myGlobal: readonly */ myGlobal = 5;", + options: ["myGlobal"] + }, + { + code: "var foo = [Map];", + options: ["Map"], + env: { es6: true } + }, + { + code: "var foo = { bar: window.baz };", + options: ["window"], + env: { browser: true } + } + ], + invalid: [ + { + code: "foo = \"bar\"", + options: ["foo"], + errors: [ + error + ] + }, + { + code: "bar = \"bar\"", + options: ["bar"], + errors: [ + error + ] + }, + { + code: "foo = \"bar\"", + options: ["f", "fo", "foo", "bar"], + errors: [ + error + ] + }, + { + code: "function foo(){}", + options: ["f", "fo", "foo", "bar"], + errors: [ + error + ] + }, + { + code: "import foo from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "import * as foo from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "export * as foo from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "import { foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "import { foo as bar } from 'mod'", + options: ["bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "import { foo as bar } from 'mod'", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "import { foo as foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "import { foo, foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + }] + }, + { + code: "import { foo as bar, foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 22 + }] + }, + { + code: "import foo, { foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 8 + }] + }, + { + code: "var foo; export { foo as bar };", + options: ["bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 26 + }] + }, + { + code: "var foo; export { foo };", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "var foo; export { foo as bar };", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + + // reports each occurrence of local identifier, although it's renamed in this export specifier + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "var foo; export { foo as foo };", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 26 + } + ] + }, + { + code: "var foo; export { foo as bar };", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 26 + } + ] + }, + { + code: "export { foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "export { foo as bar } from 'mod'", + options: ["bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "export { foo as bar } from 'mod'", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "export { foo as foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "export { foo, foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + }] + }, + { + code: "export { foo as bar, foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 22 + }] + }, + { + code: "foo.bar()", + options: ["f", "fo", "foo", "b", "ba", "baz"], + errors: [ + error + ] + }, + { + code: "foo[bar] = baz;", + options: ["bar"], + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier" + }] + }, + { + code: "baz = foo[bar];", + options: ["bar"], + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier" + }] + }, + { + code: "var foo = bar.baz;", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz"], + errors: [ + error + ] + }, + { + code: "var foo = bar.baz;", + options: ["f", "fo", "fooo", "b", "ba", "bar", "bazz"], + errors: [ + error + ] + }, + { + code: "if (foo.bar) {}", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "var obj = { key: foo.bar };", + options: ["obj"], + errors: [ + error + ] + }, + { + code: "var obj = { key: foo.bar };", + options: ["key"], + errors: [ + error + ] + }, + { + code: "var obj = { key: foo.bar };", + options: ["foo"], + errors: [ + error + ] + }, + { + code: "var arr = [foo.bar];", + options: ["arr"], + errors: [ + error + ] + }, + { + code: "var arr = [foo.bar];", + options: ["foo"], + errors: [ + error + ] + }, + { + code: "[foo.bar]", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "if (foo.bar === bar.baz) { [bing.baz] }", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "if (foo.bar === bar.baz) { [foo.bar] }", + options: ["f", "fo", "fooo", "b", "ba", "bar", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "var myArray = new Array(); var myDate = new Date();", + options: ["array", "date", "myDate", "myarray", "new", "var"], + errors: [ + error + ] + }, + { + code: "var myArray = new Array(); var myDate = new Date();", + options: ["array", "date", "mydate", "myArray", "new", "var"], + errors: [ + error + ] + }, + { + code: "foo.bar = 1", + options: ["bar"], + errors: [ + error + ] + }, + { + code: "foo.bar.baz = 1", + options: ["bar", "baz"], + errors: [ + error + ] + }, + { + code: "const {foo} = baz", + options: ["foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 8 + } + ] + }, + { + code: "const {foo: bar} = baz", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 13 + } + ] + }, + { + code: "const {[foo]: bar} = baz", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 9 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 15 + } + ] + }, + { + code: "const {foo: {bar: baz}} = qux", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "const {foo: {[bar]: baz}} = qux", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 15 + }, + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 21 + } + ] + }, + { + code: "const {[foo]: {[bar]: baz}} = qux", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 9 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }, + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 23 + } + ] + }, + { + code: "function foo({ bar: baz }) {}", + options: ["bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 21 + } + ] + }, + { + code: "function foo({ bar: {baz: qux} }) {}", + options: ["bar", "baz", "qux"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "qux" }, + type: "Identifier", + column: 27 + } + ] + }, + { + code: "({foo: obj.bar} = baz);", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 12 + } + ] + }, + { + code: "({foo: obj.bar.bar.bar.baz} = {});", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 24 + } + ] + }, + { + code: "({[foo]: obj.bar} = baz);", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 4 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 14 + } + ] + }, + { + code: "({foo: { a: obj.bar }} = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + } + ] + }, + { + code: "({a: obj.bar = baz} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "({a: obj.bar.bar.baz = obj.qux} = obj.qux);", + options: ["a", "bar", "baz", "qux"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 18 + } + ] + }, + { + code: "({a: obj[bar] = obj.qux} = obj.qux);", + options: ["a", "bar", "baz", "qux"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "({a: [obj.bar] = baz} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 11 + } + ] + }, + { + code: "({foo: { a: obj.bar = baz}} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + } + ] + }, + { + code: "({foo: { [a]: obj.bar }} = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "({...obj.bar} = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 9 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "([obj.bar] = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 7 + } + ] + }, + { + code: "const [bar] = baz;", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 8 + } + ] + }, + + // not a reference to a global variable, because it isn't a reference to a variable + { + code: "foo.undefined = 1;", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = { undefined: 1 };", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = { undefined: undefined };", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 13 + } + ] + }, + { + code: "var foo = { Number() {} };", + options: ["Number"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier" + } + ] + }, + { + code: "class Foo { Number() {} }", + options: ["Number"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier" + } + ] + }, + { + code: "myGlobal: while(foo) { break myGlobal; } ", + options: ["myGlobal"], + globals: { myGlobal: "readonly" }, + errors: [ + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 1 + }, + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 30 + } + ] + }, + + // globals declared in the given source code are not excluded from consideration + { + code: "const foo = 1; bar = foo;", + options: ["foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 22 + } + ] + }, + { + code: "let foo; foo = bar;", + options: ["foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "bar = foo; var foo;", + options: ["foo"], + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 16 + } + ] + }, + { + code: "function foo() {} var bar = foo;", + options: ["foo"], + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 29 + } + ] + }, + { + code: "class Foo {} var bar = Foo;", + options: ["Foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Foo" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "Foo" }, + type: "Identifier", + column: 24 + } + ] + }, + + // redeclared globals are not excluded from consideration + { + code: "let undefined; undefined = 1;", + options: ["undefined"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 16 + } + ] + }, + { + code: "foo = undefined; var undefined;", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 22 + } + ] + }, + { + code: "function undefined(){} x = undefined;", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 10 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 28 + } + ] + }, + { + code: "class Number {} x = Number.NaN;", + options: ["Number"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 21 + } + ] + }, + + /* + * Assignment to a property with a restricted name isn't allowed, in general. + * In this case, that restriction prevents creating a global variable with a restricted name. + */ + { + code: "/* globals myGlobal */ window.myGlobal = 5; foo = myGlobal;", + options: ["myGlobal"], + env: { browser: true }, + errors: [ + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 31 + } + ] + }, + + // disabled global variables + { + code: "var foo = undefined;", + options: ["undefined"], + globals: { undefined: "off" }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + { + code: "/* globals Number: off */ Number.parseInt()", + options: ["Number"], + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = [Map];", // this actually isn't a disabled global: it was never enabled because es6 environment isn't enabled + options: ["Map"], + errors: [ + { + messageId: "restricted", + data: { name: "Map" }, + type: "Identifier" + } + ] + }, + + // shadowed global variables + { + code: "if (foo) { let undefined; bar = undefined; }", + options: ["undefined"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 16 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 33 + } + ] + }, + { + code: "function foo(Number) { var x = Number.NaN; }", + options: ["Number"], + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 14 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 32 + } + ] + }, + { + code: "function foo() { var myGlobal; x = myGlobal; }", + options: ["myGlobal"], + globals: { myGlobal: "readonly" }, + errors: [ + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 22 + }, + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 36 + } + ] + }, + { + code: "function foo(bar) { return Number.parseInt(bar); } const Number = 1;", + options: ["Number"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 28 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 58 + } + ] + }, + { + code: "import Number from 'myNumber'; const foo = Number.parseInt(bar);", + options: ["Number"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 8 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 44 + } + ] + }, + { + code: "var foo = function undefined() {};", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + + // this is a reference to a global variable, but at the same time creates a property with a restricted name + { + code: "var foo = { undefined }", + options: ["undefined"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + } + ] +}); diff --git a/tools/rule-types.json b/tools/rule-types.json index 6f2e6a834b3..2421b9fdaad 100644 --- a/tools/rule-types.json +++ b/tools/rule-types.json @@ -43,7 +43,7 @@ "grouped-accessor-pairs": "suggestion", "guard-for-in": "suggestion", "handle-callback-err": "suggestion", - "id-blacklist": "suggestion", + "id-denylist": "suggestion", "id-length": "suggestion", "id-match": "suggestion", "implicit-arrow-linebreak": "layout", @@ -280,4 +280,4 @@ "wrap-regex": "layout", "yield-star-spacing": "layout", "yoda": "suggestion" -} \ No newline at end of file +} From d53d69af08cfe55f42e0a0ca725b1014dabccc21 Mon Sep 17 00:00:00 2001 From: Mathias Schreck Date: Thu, 25 Jun 2020 20:19:15 +0200 Subject: [PATCH 05/66] Update: prefer-regex-literal detect regex literals (fixes #12840) (#12842) The rule `prefer-regex-literal` now detects when regex literals are unnecessarily passed to the `RegExp` constructor. --- docs/rules/prefer-regex-literals.md | 32 ++++++++++ lib/rules/prefer-regex-literals.js | 74 +++++++++++++++++++++--- tests/lib/rules/prefer-regex-literals.js | 45 ++++++++++++++ 3 files changed, 143 insertions(+), 8 deletions(-) diff --git a/docs/rules/prefer-regex-literals.md b/docs/rules/prefer-regex-literals.md index fea589d3412..2ba8cacc08d 100644 --- a/docs/rules/prefer-regex-literals.md +++ b/docs/rules/prefer-regex-literals.md @@ -88,6 +88,38 @@ RegExp(`${prefix}abc`); new RegExp(String.raw`^\d\. ${suffix}`); ``` +## Options + +This rule has an object option: + +* `disallowRedundantWrapping` set to `true` additionally checks for unnecessarily wrapped regex literals (Default `false`). + +### `disallowRedundantWrapping` + +By default, this rule doesn’t check when a regex literal is unnecessarily wrapped in a `RegExp` constructor call. When the option `disallowRedundantWrapping` is set to `true`, the rule will also disallow such unnecessary patterns. + +Examples of `incorrect` code for `{ "disallowRedundantWrapping": true }` + +```js +/*eslint prefer-regex-literals: ["error", {"disallowRedundantWrapping": true}]*/ + +new RegExp(/abc/); + +new RegExp(/abc/, 'u'); +``` + +Examples of `correct` code for `{ "disallowRedundantWrapping": true }` + +```js +/*eslint prefer-regex-literals: ["error", {"disallowRedundantWrapping": true}]*/ + +/abc/; + +/abc/u; + +new RegExp(/abc/, flags); +``` + ## Further Reading * [MDN: Regular Expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) diff --git a/lib/rules/prefer-regex-literals.js b/lib/rules/prefer-regex-literals.js index 47b2b090f82..8a5d209c1e2 100644 --- a/lib/rules/prefer-regex-literals.js +++ b/lib/rules/prefer-regex-literals.js @@ -25,6 +25,15 @@ function isStringLiteral(node) { return node.type === "Literal" && typeof node.value === "string"; } +/** + * Determines whether the given node is a regex literal. + * @param {ASTNode} node Node to check. + * @returns {boolean} True if the node is a regex literal. + */ +function isRegexLiteral(node) { + return node.type === "Literal" && Object.prototype.hasOwnProperty.call(node, "regex"); +} + /** * Determines whether the given node is a template literal without expressions. * @param {ASTNode} node Node to check. @@ -50,14 +59,28 @@ module.exports = { url: "https://eslint.org/docs/rules/prefer-regex-literals" }, - schema: [], + schema: [ + { + type: "object", + properties: { + disallowRedundantWrapping: { + type: "boolean", + default: false + } + }, + additionalProperties: false + } + ], messages: { - unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor." + unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor.", + unexpectedRedundantRegExp: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.", + unexpectedRedundantRegExpWithFlags: "Use regular expression literal with flags instead of the 'RegExp' constructor." } }, create(context) { + const [{ disallowRedundantWrapping = false } = {}] = context.options; /** * Determines whether the given identifier node is a reference to a global variable. @@ -98,6 +121,40 @@ module.exports = { isStringRawTaggedStaticTemplateLiteral(node); } + /** + * Determines whether the relevant arguments of the given are all static string literals. + * @param {ASTNode} node Node to check. + * @returns {boolean} True if all arguments are static strings. + */ + function hasOnlyStaticStringArguments(node) { + const args = node.arguments; + + if ((args.length === 1 || args.length === 2) && args.every(isStaticString)) { + return true; + } + + return false; + } + + /** + * Determines whether the arguments of the given node indicate that a regex literal is unnecessarily wrapped. + * @param {ASTNode} node Node to check. + * @returns {boolean} True if the node already contains a regex literal argument. + */ + function isUnnecessarilyWrappedRegexLiteral(node) { + const args = node.arguments; + + if (args.length === 1 && isRegexLiteral(args[0])) { + return true; + } + + if (args.length === 2 && isRegexLiteral(args[0]) && isStaticString(args[1])) { + return true; + } + + return false; + } + return { Program() { const scope = context.getScope(); @@ -110,12 +167,13 @@ module.exports = { }; for (const { node } of tracker.iterateGlobalReferences(traceMap)) { - const args = node.arguments; - - if ( - (args.length === 1 || args.length === 2) && - args.every(isStaticString) - ) { + if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(node)) { + if (node.arguments.length === 2) { + context.report({ node, messageId: "unexpectedRedundantRegExpWithFlags" }); + } else { + context.report({ node, messageId: "unexpectedRedundantRegExp" }); + } + } else if (hasOnlyStaticStringArguments(node)) { context.report({ node, messageId: "unexpectedRegExp" }); } } diff --git a/tests/lib/rules/prefer-regex-literals.js b/tests/lib/rules/prefer-regex-literals.js index 65979de83f9..9f6a2e6fbae 100644 --- a/tests/lib/rules/prefer-regex-literals.js +++ b/tests/lib/rules/prefer-regex-literals.js @@ -23,6 +23,7 @@ ruleTester.run("prefer-regex-literals", rule, { "/abc/", "/abc/g", + // considered as dynamic "new RegExp(pattern)", "RegExp(pattern, 'g')", @@ -41,6 +42,26 @@ ruleTester.run("prefer-regex-literals", rule, { "new RegExp(String.raw`a${''}c`);", "new RegExp('a' + 'b')", "RegExp(1)", + "new RegExp(/a/, 'u');", + "new RegExp(/a/);", + { + code: "new RegExp(/a/, flags);", + options: [{ disallowRedundantWrapping: true }] + }, + { + code: "new RegExp(/a/, `u${flags}`);", + options: [{ disallowRedundantWrapping: true }] + }, + + // redundant wrapping is allowed + { + code: "new RegExp(/a/);", + options: [{}] + }, + { + code: "new RegExp(/a/);", + options: [{ disallowRedundantWrapping: false }] + }, // invalid number of arguments "new RegExp;", @@ -52,6 +73,10 @@ ruleTester.run("prefer-regex-literals", rule, { "RegExp(`a`, `g`, `b`);", "new RegExp(String.raw`a`, String.raw`g`, String.raw`b`);", "RegExp(String.raw`a`, String.raw`g`, String.raw`b`);", + { + code: "new RegExp(/a/, 'u', 'foo');", + options: [{ disallowRedundantWrapping: true }] + }, // not String.raw`` "new RegExp(String`a`);", @@ -196,6 +221,26 @@ ruleTester.run("prefer-regex-literals", rule, { code: "globalThis.RegExp('a');", env: { es2020: true }, errors: [{ messageId: "unexpectedRegExp", type: "CallExpression" }] + }, + { + code: "new RegExp(/a/);", + options: [{ disallowRedundantWrapping: true }], + errors: [{ messageId: "unexpectedRedundantRegExp", type: "NewExpression", line: 1, column: 1 }] + }, + { + code: "new RegExp(/a/, 'u');", + options: [{ disallowRedundantWrapping: true }], + errors: [{ messageId: "unexpectedRedundantRegExpWithFlags", type: "NewExpression", line: 1, column: 1 }] + }, + { + code: "new RegExp(/a/, `u`);", + options: [{ disallowRedundantWrapping: true }], + errors: [{ messageId: "unexpectedRedundantRegExpWithFlags", type: "NewExpression", line: 1, column: 1 }] + }, + { + code: "new RegExp('a');", + options: [{ disallowRedundantWrapping: true }], + errors: [{ messageId: "unexpectedRegExp", type: "NewExpression", line: 1, column: 1 }] } ] }); From 0655f66525d167ca1288167b79a77087cfc8fcf6 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Fri, 26 Jun 2020 03:26:29 +0900 Subject: [PATCH 06/66] Update: improve report location in arrow-body-style (refs #12334) (#13424) --- lib/rules/arrow-body-style.js | 4 ++-- tests/lib/rules/arrow-body-style.js | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/rules/arrow-body-style.js b/lib/rules/arrow-body-style.js index 9d5c77d8573..5954cf4a18d 100644 --- a/lib/rules/arrow-body-style.js +++ b/lib/rules/arrow-body-style.js @@ -136,7 +136,7 @@ module.exports = { context.report({ node, - loc: arrowBody.loc.start, + loc: arrowBody.loc, messageId, fix(fixer) { const fixes = []; @@ -201,7 +201,7 @@ module.exports = { if (always || (asNeeded && requireReturnForObjectLiteral && arrowBody.type === "ObjectExpression")) { context.report({ node, - loc: arrowBody.loc.start, + loc: arrowBody.loc, messageId: "expectedBlock", fix(fixer) { const fixes = []; diff --git a/tests/lib/rules/arrow-body-style.js b/tests/lib/rules/arrow-body-style.js index 7c1bea64ef8..c624a847c6a 100644 --- a/tests/lib/rules/arrow-body-style.js +++ b/tests/lib/rules/arrow-body-style.js @@ -53,6 +53,8 @@ ruleTester.run("arrow-body-style", rule, { { line: 1, column: 17, + endLine: 1, + endColumn: 18, type: "ArrowFunctionExpression", messageId: "expectedBlock" } @@ -440,6 +442,8 @@ ruleTester.run("arrow-body-style", rule, { { line: 1, column: 17, + endLine: 3, + endColumn: 2, type: "ArrowFunctionExpression", messageId: "unexpectedSingleBlock" } @@ -452,6 +456,8 @@ ruleTester.run("arrow-body-style", rule, { { line: 1, column: 17, + endLine: 2, + endColumn: 13, type: "ArrowFunctionExpression", messageId: "unexpectedSingleBlock" } @@ -464,6 +470,8 @@ ruleTester.run("arrow-body-style", rule, { { line: 1, column: 17, + endLine: 2, + endColumn: 2, type: "ArrowFunctionExpression", messageId: "unexpectedSingleBlock" } @@ -508,6 +516,8 @@ ruleTester.run("arrow-body-style", rule, { { line: 2, column: 31, + endLine: 7, + endColumn: 16, type: "ArrowFunctionExpression", messageId: "unexpectedObjectBlock" } From 51e42eca3e87d8259815d736ffe81e604f184057 Mon Sep 17 00:00:00 2001 From: David Gasperoni Date: Fri, 26 Jun 2020 21:56:44 +0200 Subject: [PATCH 07/66] Update: Add option "ignoreGlobals" to camelcase rule (fixes #11716) (#12782) * Update: Add option "ignoreGlobals" to camelcase rule (fixes 11716) * Change reference check to look for global variable * Fix behavior for global references and add tests * Add more valid tests to camelcase rule * Add more invalid tests for camelcase rule * Don't ignore non-camelcase global variable used as object key * Corrections to camelcase documentation --- docs/rules/camelcase.md | 24 +++ lib/rules/camelcase.js | 47 +++++ tests/lib/rules/camelcase.js | 349 +++++++++++++++++++++++++++++++++++ 3 files changed, 420 insertions(+) diff --git a/docs/rules/camelcase.md b/docs/rules/camelcase.md index 96135f8c932..164dfab7c46 100644 --- a/docs/rules/camelcase.md +++ b/docs/rules/camelcase.md @@ -16,6 +16,8 @@ This rule has an object option: * `"ignoreDestructuring": true` does not check destructured identifiers (but still checks any use of those identifiers later in the code) * `"ignoreImports": false` (default) enforces camelcase style for ES2015 imports * `"ignoreImports": true` does not check ES2015 imports (but still checks any use of the imports later in the code except function arguments) +* `"ignoreGlobals": false` (default) enforces camelcase style for global variables +* `"ignoreGlobals": true` does not enforce camelcase style for global variables * `allow` (`string[]`) list of properties to accept. Accept regex. ### properties: "always" @@ -217,6 +219,28 @@ Examples of **correct** code for this rule with the `{ "ignoreImports": true }` import { snake_cased } from 'mod'; ``` +### ignoreGlobals: false + +Examples of **incorrect** code for this rule with the default `{ "ignoreGlobals": false }` option: + +```js +/*eslint camelcase: ["error", {ignoreGlobals: false}]*/ +/* global no_camelcased */ + +const foo = no_camelcased; +``` + +### ignoreGlobals: true + +Examples of **correct** code for this rule with the `{ "ignoreGlobals": true }` option: + +```js +/*eslint camelcase: ["error", {ignoreGlobals: true}]*/ +/* global no_camelcased */ + +const foo = no_camelcased; +``` + ## allow Examples of **correct** code for this rule with the `allow` option: diff --git a/lib/rules/camelcase.js b/lib/rules/camelcase.js index 04360837294..d34656cfabe 100644 --- a/lib/rules/camelcase.js +++ b/lib/rules/camelcase.js @@ -32,6 +32,10 @@ module.exports = { type: "boolean", default: false }, + ignoreGlobals: { + type: "boolean", + default: false + }, properties: { enum: ["always", "never"] }, @@ -61,8 +65,11 @@ module.exports = { let properties = options.properties || ""; const ignoreDestructuring = options.ignoreDestructuring; const ignoreImports = options.ignoreImports; + const ignoreGlobals = options.ignoreGlobals; const allow = options.allow || []; + let globalScope; + if (properties !== "always" && properties !== "never") { properties = "always"; } @@ -159,6 +166,37 @@ module.exports = { return false; } + /** + * Checks whether the given node represents a reference to a global variable that is not declared in the source code. + * These identifiers will be allowed, as it is assumed that user has no control over the names of external global variables. + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a reference to a global variable. + */ + function isReferenceToGlobalVariable(node) { + const variable = globalScope.set.get(node.name); + + return variable && variable.defs.length === 0 && + variable.references.some(ref => ref.identifier === node); + } + + /** + * Checks whether the given node represents a reference to a property of an object in an object literal expression. + * This allows to differentiate between a global variable that is allowed to be used as a reference, and the key + * of the expressed object (which shouldn't be allowed). + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a property name of an object literal expression + */ + function isPropertyNameInObjectLiteral(node) { + const parent = node.parent; + + return ( + parent.type === "Property" && + parent.parent.type === "ObjectExpression" && + !parent.computed && + parent.key === node + ); + } + /** * Reports an AST node as a rule violation. * @param {ASTNode} node The node to report. @@ -174,6 +212,10 @@ module.exports = { return { + Program() { + globalScope = context.getScope(); + }, + Identifier(node) { /* @@ -189,6 +231,11 @@ module.exports = { return; } + // Check if it's a global variable + if (ignoreGlobals && isReferenceToGlobalVariable(node) && !isPropertyNameInObjectLiteral(node)) { + return; + } + // MemberExpressions get special rules if (node.parent.type === "MemberExpression") { diff --git a/tests/lib/rules/camelcase.js b/tests/lib/rules/camelcase.js index 7c7a4576c77..eab0f354aec 100644 --- a/tests/lib/rules/camelcase.js +++ b/tests/lib/rules/camelcase.js @@ -169,6 +169,104 @@ ruleTester.run("camelcase", rule, { options: [{ ignoreImports: false }], parserOptions: { ecmaVersion: 6, sourceType: "module" } }, + { + code: "var _camelCased = aGlobalVariable", + options: [{ ignoreGlobals: false }], + globals: { aGlobalVariable: "readonly" } + }, + { + code: "var camelCased = _aGlobalVariable", + options: [{ ignoreGlobals: false }], + globals: { _aGlobalVariable: "readonly" } + }, + { + code: "var camelCased = a_global_variable", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable.foo()", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable[undefined]", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "var foo = a_global_variable.bar", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable.foo = bar", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "( { foo: a_global_variable.bar } = baz )", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable = foo", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "a_global_variable = foo", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "({ a_global_variable } = foo)", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "({ snake_cased: a_global_variable } = foo)", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "({ snake_cased: a_global_variable = foo } = bar)", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "[a_global_variable] = bar", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "[a_global_variable = foo] = bar", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" } // eslint-disable-line camelcase + }, + { + code: "foo[a_global_variable] = bar", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "var foo = { [a_global_variable]: bar }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, + { + code: "var { [a_global_variable]: foo } = bar", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "readonly" } // eslint-disable-line camelcase + }, { code: "function foo({ no_camelcased: camelCased }) {};", parserOptions: { ecmaVersion: 6 } @@ -652,6 +750,257 @@ ruleTester.run("camelcase", rule, { } ] }, + { + code: "var camelCased = snake_cased", + options: [{ ignoreGlobals: false }], + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "snake_cased" }, + type: "Identifier" + } + ] + }, + { + code: "a_global_variable.foo()", + options: [{ ignoreGlobals: false }], + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "a_global_variable[undefined]", + options: [{ ignoreGlobals: false }], + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "var camelCased = snake_cased", + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "snake_cased" }, + type: "Identifier" + } + ] + }, + { + code: "var camelCased = snake_cased", + options: [{}], + globals: { snake_cased: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "snake_cased" }, + type: "Identifier" + } + ] + }, + { + code: "foo.a_global_variable = bar", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = { a_global_variable: bar }", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = { a_global_variable: a_global_variable }", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 13 + } + ] + }, + { + code: "var foo = { a_global_variable() {} }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "class Foo { a_global_variable() {} }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "a_global_variable: for (;;);", + options: [{ ignoreGlobals: true }], + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "if (foo) { let a_global_variable; a_global_variable = bar; }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 16 + }, + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 35 + } + ] + }, + { + code: "function foo(a_global_variable) { foo = a_global_variable; }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 14 + }, + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 41 + } + ] + }, + { + code: "var a_global_variable", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "function a_global_variable () {}", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, + { + code: "const a_global_variable = foo; bar = a_global_variable", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 7 + }, + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 38 + } + ] + }, + { + code: "bar = a_global_variable; var a_global_variable;", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "writable" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 7 + }, + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier", + column: 30 + } + ] + }, + { + code: "var foo = { a_global_variable }", + options: [{ ignoreGlobals: true }], + parserOptions: { ecmaVersion: 6 }, + globals: { a_global_variable: "readonly" }, // eslint-disable-line camelcase + errors: [ + { + messageId: "notCamelCase", + data: { name: "a_global_variable" }, + type: "Identifier" + } + ] + }, { code: "export * as snake_cased from 'mod'", parserOptions: { ecmaVersion: 2020, sourceType: "module" }, From 53912aab1856327b399cca26cbb2ba81fd01bfa2 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Mon, 29 Jun 2020 12:11:46 -0400 Subject: [PATCH 08/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a6aeb4d2fe..cbd74745e2b 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Shopify Salesforce Airbnb

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver vpn netflix Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Free Icons by Icons8 Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

+

Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver vpn netflix Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Free Icons by Icons8 Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

## Technology Sponsors From 5c4c3fdfbda18a13223ad36f44283adbfee8c496 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Mon, 29 Jun 2020 13:11:45 -0400 Subject: [PATCH 09/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbd74745e2b..424ad6ee166 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Shopify Salesforce Airbnb

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver vpn netflix Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Free Icons by Icons8 Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

+

Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver vpn netflix Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

## Technology Sponsors From 7baf02e983af909800261263f125cca901a5bd0f Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Tue, 30 Jun 2020 00:11:47 -0400 Subject: [PATCH 10/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 424ad6ee166..2adb1b4c344 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Shopify Salesforce Airbnb

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver vpn netflix Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

+

Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Anagram Solver vpn netflix Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

## Technology Sponsors From bf3939bbd9a33d0eb96cebe6a53bf61c855f9ba6 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Tue, 30 Jun 2020 01:11:40 -0400 Subject: [PATCH 11/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2adb1b4c344..424ad6ee166 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Shopify Salesforce Airbnb

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Anagram Solver vpn netflix Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

+

Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver vpn netflix Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

## Technology Sponsors From c680387ba61f6dccf0390d24a85d871fa83e9fea Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Tue, 30 Jun 2020 20:11:50 -0400 Subject: [PATCH 12/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 424ad6ee166..10a2915d8c4 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Shopify Salesforce Airbnb

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver vpn netflix Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

+

My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver vpn netflix Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

## Technology Sponsors From 0c17e9d2ac307cc288eea6ed7971bd5a7d33321a Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Tue, 30 Jun 2020 22:11:48 -0400 Subject: [PATCH 13/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 10a2915d8c4..58ffc1f6a5c 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Shopify Salesforce Airbnb

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver vpn netflix Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

+

My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver vpn netflix Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

## Technology Sponsors From c1391566a5f765f25716527de7b5cdee16c0ce36 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 3 Jul 2020 02:11:41 -0400 Subject: [PATCH 14/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 58ffc1f6a5c..4b5a911b2c1 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Shopify Salesforce Airbnb

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver vpn netflix Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

+

My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

## Technology Sponsors From 825a5b98d3d84f6eb72b75f7d8519de763cc8898 Mon Sep 17 00:00:00 2001 From: Scott Hardin Date: Fri, 3 Jul 2020 15:41:20 -0400 Subject: [PATCH 15/66] Fix: Clarify documentation on implicit ignore behavior (fixes #12348) (#12600) --- docs/user-guide/configuring.md | 53 +++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md index 7f880198bf4..373a0fb05b4 100644 --- a/docs/user-guide/configuring.md +++ b/docs/user-guide/configuring.md @@ -1070,18 +1070,36 @@ Of particular note is that like `.gitignore` files, all paths used as patterns f Please see `.gitignore`'s specification for further examples of valid syntax. -In addition to any patterns in a `.eslintignore` file, ESLint ignores files in `/**/node_modules/*` by default. It can still be added using `!`. +In addition to any patterns in the `.eslintignore` file, ESLint always follows a couple implicit ignore rules even if the `--no-ignore` flag is passed. The implicit rules are as follows: -For example, placing the following `.eslintignore` file in the current working directory will not ignore `node_modules/*` and ignore anything in the `build/` directory except `build/index.js`: +* `node_modules/` is ignored. +* Dotfiles (except for `.eslintrc.*`) as well as Dotfolders and their contents are ignored. -```text -# node_modules/* is ignored by default, but can be added using ! -!node_modules/* +There are also some exceptions to these rules: -# Ignore built files except build/index.js -build/* -!build/index.js -``` +* If the path to lint is a glob pattern or directory path and contains a Dotfolder, all Dotfiles and Dotfolders will be linted. This includes sub-dotfiles and sub-dotfolders that are buried deeper in the directory structure. + + For example, `eslint .config/` will lint all Dotfolders and Dotfiles in the `.config` directory, including immediate children as well as children that are deeper in the directory structure. + +* If the path to lint is a specific file path and the `--no-ignore` flag has been passed, ESLint will lint the file regardless of the implicit ignore rules. + + For example, `eslint .config/my-config-file.js --no-ignore` will cause `my-config-file.js` to be linted. It should be noted that the same command without the `--no-ignore` line will not lint the `my-config-file.js` file. + +* Allowlist and denylist rules specified via `--ignore-pattern` or `.eslintignore` are prioritized above implicit ignore rules. + + For example, in this scenario, `.build/test.js` is the desired file to allowlist. Because all Dotfolders and their children are ignored by default, `.build` must first be allowlisted so that eslint because aware of its children. Then, `.build/test.js` must be explicitly allowlisted, while the rest of the content is denylisted. This is done with the following `.eslintignore` file: + + ```text + # Allowlist 'test.js' in the '.build' folder + # But do not allow anything else in the '.build' folder to be linted + !.build + .build/* + !.build/test.js + ``` + + The following `--ignore-pattern` is also equivalent: + + eslint --ignore-pattern '!.build' --ignore-pattern '.build/*' --ignore-pattern '!.build/test.js' parent-folder/ ### Using an Alternate File @@ -1127,13 +1145,28 @@ You'll see this warning: ```text foo.js - 0:0 warning File ignored because of your .eslintignore file. Use --no-ignore to override. + 0:0 warning File ignored because of a matching ignore pattern. Use "--no-ignore" to override. ✖ 1 problem (0 errors, 1 warning) ``` This message occurs because ESLint is unsure if you wanted to actually lint the file or not. As the message indicates, you can use `--no-ignore` to omit using the ignore rules. +Consider another scenario where you may want to run ESLint on a specific Dotfile or Dotfolder, but have forgotten to specifically allow those files in your `.eslintignore` file. You would run something like this: + + eslint .config/foo.js + +You would see this warning: + +```text +.config/foo.js + 0:0 warning File ignored by default. Use a negated ignore pattern (like "--ignore-pattern '!'") to override + +✖ 1 problem (0 errors, 1 warning) +``` + +This messages occurs because, normally, this file would be ignored by ESLint's implicit ignore rules (as mentioned above). A negated ignore rule in your `.eslintignore` file would override the implicit rule and reinclude this file for linting. Additionally, in this specific case, `--no-ignore` could be used to lint the file as well. + ## Personal Configuration File (deprecated) ⚠️ **This feature has been deprecated**. This feature will be removed in the 8.0.0 release. If you want to continue to use personal configuration files, please use the [`--config` CLI option](https://eslint.org/docs/user-guide/command-line-interface#-c---config). For more information regarding this decision, please see [RFC 28](https://github.com/eslint/rfcs/pull/28) and [RFC 32](https://github.com/eslint/rfcs/pull/32). From 3f51930eea7cddc921a9ee3cb0328c7b649c0f83 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sat, 4 Jul 2020 04:43:34 +0900 Subject: [PATCH 16/66] Fix: false positive new with member in no-extra-parens (fixes #12740) (#13375) * Fix: false positive new with member in no-extra-parens (fixes #12740) * rename func * remove duplicate function * check whole node is wrapped or not * remove checking whole node * check member object --- lib/rules/no-extra-parens.js | 24 +++++++++++++++++++++--- tests/lib/rules/no-extra-parens.js | 17 +++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js index bae1a498cf0..1ece81eee81 100644 --- a/lib/rules/no-extra-parens.js +++ b/lib/rules/no-extra-parens.js @@ -710,6 +710,20 @@ module.exports = { reportsBuffer.reports = reportsBuffer.reports.filter(r => r.node !== node); } + /** + * Checks whether a node is a MemberExpression at NewExpression's callee. + * @param {ASTNode} node node to check. + * @returns {boolean} True if the node is a MemberExpression at NewExpression's callee. false otherwise. + */ + function isMemberExpInNewCallee(node) { + if (node.type === "MemberExpression") { + return node.parent.type === "NewExpression" && node.parent.callee === node + ? true + : node.parent.object === node && isMemberExpInNewCallee(node.parent); + } + return false; + } + return { ArrayExpression(node) { node.elements @@ -950,7 +964,11 @@ module.exports = { LogicalExpression: checkBinaryLogical, MemberExpression(node) { - const nodeObjHasExcessParens = hasExcessParens(node.object) && + const shouldAllowWrapOnce = isMemberExpInNewCallee(node) && + doesMemberExpressionContainCallExpression(node); + const nodeObjHasExcessParens = shouldAllowWrapOnce + ? hasDoubleExcessParens(node.object) + : hasExcessParens(node.object) && !( isImmediateFunctionPrototypeMethodCall(node.parent) && node.parent.callee === node && @@ -974,8 +992,8 @@ module.exports = { } if (nodeObjHasExcessParens && - node.object.type === "CallExpression" && - node.parent.type !== "NewExpression") { + node.object.type === "CallExpression" + ) { report(node.object); } diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js index 91099f8cba0..e2501bb45a0 100644 --- a/tests/lib/rules/no-extra-parens.js +++ b/tests/lib/rules/no-extra-parens.js @@ -612,6 +612,14 @@ ruleTester.run("no-extra-parens", rule, { "for (; a; a); a; a;", "for (let a = (b && c) === d; ;);", + "new (a()).b.c;", + "new (a().b).c;", + "new (a().b.c);", + "new (a().b().d);", + "new a().b().d;", + "new (a(b()).c)", + "new (a.b()).c", + // Nullish coalescing { code: "var v = (a ?? b) || c", parserOptions: { ecmaVersion: 2020 } }, { code: "var v = a ?? (b || c)", parserOptions: { ecmaVersion: 2020 } }, @@ -759,6 +767,7 @@ ruleTester.run("no-extra-parens", rule, { invalid("(new foo(bar)).baz", "new foo(bar).baz", "NewExpression"), invalid("(new foo.bar()).baz", "new foo.bar().baz", "NewExpression"), invalid("(new foo.bar()).baz()", "new foo.bar().baz()", "NewExpression"), + invalid("new a[(b()).c]", "new a[b().c]", "CallExpression"), invalid("(a)()", "a()", "Identifier"), invalid("(a.b)()", "a.b()", "MemberExpression"), @@ -772,6 +781,14 @@ ruleTester.run("no-extra-parens", rule, { invalid("((new A))()", "(new A)()", "NewExpression"), invalid("new (foo\n.baz\n.bar\n.foo.baz)", "new foo\n.baz\n.bar\n.foo.baz", "MemberExpression"), invalid("new (foo.baz.bar.baz)", "new foo.baz.bar.baz", "MemberExpression"), + invalid("new ((a.b())).c", "new (a.b()).c", "CallExpression"), + invalid("new ((a().b)).c", "new (a().b).c", "MemberExpression"), + invalid("new ((a().b().d))", "new (a().b().d)", "MemberExpression"), + invalid("new ((a())).b.d", "new (a()).b.d", "CallExpression"), + invalid("new (a.b).d;", "new a.b.d;", "MemberExpression"), + invalid("(a().b).d;", "a().b.d;", "MemberExpression"), + invalid("(a.b()).d;", "a.b().d;", "CallExpression"), + invalid("(a.b).d;", "a.b.d;", "MemberExpression"), invalid("0, (_ => 0)", "0, _ => 0", "ArrowFunctionExpression", 1), invalid("(_ => 0), 0", "_ => 0, 0", "ArrowFunctionExpression", 1), From ff5317e93425f93cfdf808609551ee67b2032543 Mon Sep 17 00:00:00 2001 From: "Philip (flip) Kromer" Date: Fri, 3 Jul 2020 14:43:54 -0500 Subject: [PATCH 17/66] Update: Improve array-callback-return report message (#13395) * Update: Improve array-callback-return report (explains why) The array-callback-return rule should explain that a return value is required because the surrounding method requires it. This makes it clear that eg. .filter() expects the passed function to return something, rather than a general expectation of that function. The error messages for expectedAtEnd/Inside now read 'Method .{{arrayMethodName}}() expected a value to be returned at the end of {{name}}.' and 'Method .{{arrayMethodName}}() expected a return value from {{name}}.' * Update lib/rules/array-callback-return.js Co-authored-by: Kai Cataldo * Update lib/rules/array-callback-return.js Co-authored-by: Kai Cataldo * Update: Improve array-callback-return report (explains why) patch * Made tests match proposed new message. * fix: forEach message, .from Owner * forEach has a message, and describes why no return value is expected. * present tense ('Array.from expects no' vs 'Array.from expected no') * Added explicit check of the forEach message * fix: cleaner production of full-qyalified method name Co-authored-by: Kai Cataldo --- lib/rules/array-callback-return.js | 31 ++-- tests/lib/rules/array-callback-return.js | 199 ++++++++++++----------- 2 files changed, 121 insertions(+), 109 deletions(-) diff --git a/lib/rules/array-callback-return.js b/lib/rules/array-callback-return.js index 62ba7b72d87..02a96e311e5 100644 --- a/lib/rules/array-callback-return.js +++ b/lib/rules/array-callback-return.js @@ -9,8 +9,6 @@ // Requirements //------------------------------------------------------------------------------ -const lodash = require("lodash"); - const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ @@ -43,6 +41,19 @@ function isTargetMethod(node) { ); } +/** + * Returns a human-legible description of an array method + * @param {string} arrayMethodName A method name to fully qualify + * @returns {string} the method name prefixed with `Array.` if it is a class method, + * or else `Array.prototype.` if it is an instance method. + */ +function fullMethodName(arrayMethodName) { + if (["from", "of", "isArray"].includes(arrayMethodName)) { + return "Array.".concat(arrayMethodName); + } + return "Array.prototype.".concat(arrayMethodName); +} + /** * Checks whether or not a given node is a function expression which is the * callback of an array method, returning the method name. @@ -153,10 +164,10 @@ module.exports = { ], messages: { - expectedAtEnd: "Expected to return a value at the end of {{name}}.", - expectedInside: "Expected to return a value in {{name}}.", - expectedReturnValue: "{{name}} expected a return value.", - expectedNoReturnValue: "{{name}} did not expect a return value." + expectedAtEnd: "{{arrayMethodName}}() expects a value to be returned at the end of {{name}}.", + expectedInside: "{{arrayMethodName}}() expects a return value from {{name}}.", + expectedReturnValue: "{{arrayMethodName}}() expects a return value from {{name}}.", + expectedNoReturnValue: "{{arrayMethodName}}() expects no useless return value from {{name}}." } }, @@ -202,14 +213,13 @@ module.exports = { } if (messageId) { - let name = astUtils.getFunctionNameWithKind(node); + const name = astUtils.getFunctionNameWithKind(node); - name = messageId === "expectedNoReturnValue" ? lodash.upperFirst(name) : name; context.report({ node, loc: astUtils.getFunctionHeadLoc(node, sourceCode), messageId, - data: { name } + data: { name, arrayMethodName: fullMethodName(funcInfo.arrayMethodName) } }); } } @@ -273,7 +283,8 @@ module.exports = { node, messageId, data: { - name: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node)) + name: astUtils.getFunctionNameWithKind(funcInfo.node), + arrayMethodName: fullMethodName(funcInfo.arrayMethodName) } }); } diff --git a/tests/lib/rules/array-callback-return.js b/tests/lib/rules/array-callback-return.js index 24b40cb5204..61aabaeb32c 100644 --- a/tests/lib/rules/array-callback-return.js +++ b/tests/lib/rules/array-callback-return.js @@ -115,90 +115,91 @@ ruleTester.run("array-callback-return", rule, { ], invalid: [ - { code: "Array.from(x, function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "Array.from(x, function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "Int32Array.from(x, function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "Int32Array.from(x, function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.every(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.every(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.filter(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.filter(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.find(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.find(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.findIndex(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.findIndex(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.flatMap(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.flatMap(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.map(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.map(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.reduce(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.reduce(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.reduceRight(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.reduceRight(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.some(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.some(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.sort(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.sort(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.bar.baz.every(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.bar.baz.every(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo[\"every\"](function() {})", errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo[\"every\"](function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo[`every`](function() {})", parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo[`every`](function foo() {})", parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.every(() => {})", parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Expected to return a value in arrow function.", column: 14 }] }, - { code: "foo.every(function() { if (a) return true; })", errors: [{ message: "Expected to return a value at the end of function.", column: 11 }] }, - { code: "foo.every(function cb() { if (a) return true; })", errors: [{ message: "Expected to return a value at the end of function 'cb'.", column: 11 }] }, - { code: "foo.every(function() { switch (a) { case 0: break; default: return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function" } }] }, - { code: "foo.every(function foo() { switch (a) { case 0: break; default: return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function 'foo'" } }] }, - { code: "foo.every(function() { try { bar(); } catch (err) { return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function" } }] }, - { code: "foo.every(function foo() { try { bar(); } catch (err) { return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function 'foo'" } }] }, - { code: "foo.every(function() { return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "Function" } }] }, - { code: "foo.every(function foo() { return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "Function 'foo'" } }] }, - { code: "foo.every(function() { if (a) return; })", errors: ["Expected to return a value at the end of function.", { messageId: "expectedReturnValue", data: { name: "Function" } }] }, - { code: "foo.every(function foo() { if (a) return; })", errors: ["Expected to return a value at the end of function 'foo'.", { messageId: "expectedReturnValue", data: { name: "Function 'foo'" } }] }, - { code: "foo.every(function() { if (a) return; else return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "Function" } }, { messageId: "expectedReturnValue", data: { name: "Function" } }] }, - { code: "foo.every(function foo() { if (a) return; else return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "Function 'foo'" } }, { messageId: "expectedReturnValue", data: { name: "Function 'foo'" } }] }, - { code: "foo.every(cb || function() {})", errors: ["Expected to return a value in function."] }, - { code: "foo.every(cb || function foo() {})", errors: ["Expected to return a value in function 'foo'."] }, - { code: "foo.every(a ? function() {} : function() {})", errors: ["Expected to return a value in function.", "Expected to return a value in function."] }, - { code: "foo.every(a ? function foo() {} : function bar() {})", errors: ["Expected to return a value in function 'foo'.", "Expected to return a value in function 'bar'."] }, - { code: "foo.every(function(){ return function() {}; }())", errors: [{ message: "Expected to return a value in function.", column: 30 }] }, - { code: "foo.every(function(){ return function foo() {}; }())", errors: [{ message: "Expected to return a value in function 'foo'.", column: 30 }] }, - { code: "foo.every(() => {})", options: [{ allowImplicit: false }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Expected to return a value in arrow function." }] }, - { code: "foo.every(() => {})", options: [{ allowImplicit: true }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Expected to return a value in arrow function." }] }, + { code: "Array.from(x, function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.from" } }] }, + { code: "Array.from(x, function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.from" } }] }, + { code: "Int32Array.from(x, function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.from" } }] }, + { code: "Int32Array.from(x, function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.from" } }] }, + { code: "foo.every(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.filter(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.filter" } }] }, + { code: "foo.filter(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, + { code: "foo.find(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.find" } }] }, + { code: "foo.find(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.find" } }] }, + { code: "foo.findIndex(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.findIndex" } }] }, + { code: "foo.findIndex(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.findIndex" } }] }, + { code: "foo.flatMap(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.flatMap" } }] }, + { code: "foo.flatMap(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.flatMap" } }] }, + { code: "foo.map(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.map" } }] }, + { code: "foo.map(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.map" } }] }, + { code: "foo.reduce(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.reduce" } }] }, + { code: "foo.reduce(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.reduce" } }] }, + { code: "foo.reduceRight(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.reduceRight" } }] }, + { code: "foo.reduceRight(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.reduceRight" } }] }, + { code: "foo.some(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.some" } }] }, + { code: "foo.some(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.some" } }] }, + { code: "foo.sort(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.sort" } }] }, + { code: "foo.sort(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.sort" } }] }, + { code: "foo.bar.baz.every(function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.bar.baz.every(function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo[\"every\"](function() {})", errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo[\"every\"](function foo() {})", errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo[`every`](function() {})", parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo[`every`](function foo() {})", parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(() => {})", parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Array.prototype.every() expects a return value from arrow function.", column: 14 }] }, + { code: "foo.every(function() { if (a) return true; })", errors: [{ message: "Array.prototype.every() expects a value to be returned at the end of function.", column: 11 }] }, + { code: "foo.every(function cb() { if (a) return true; })", errors: [{ message: "Array.prototype.every() expects a value to be returned at the end of function 'cb'.", column: 11 }] }, + { code: "foo.every(function() { switch (a) { case 0: break; default: return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function foo() { switch (a) { case 0: break; default: return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function() { try { bar(); } catch (err) { return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function foo() { try { bar(); } catch (err) { return true; } })", errors: [{ messageId: "expectedAtEnd", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function() { return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function foo() { return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function() { if (a) return; })", errors: ["Array.prototype.every() expects a value to be returned at the end of function.", { messageId: "expectedReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function foo() { if (a) return; })", errors: ["Array.prototype.every() expects a value to be returned at the end of function 'foo'.", { messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function() { if (a) return; else return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.every" } }, { messageId: "expectedReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(function foo() { if (a) return; else return; })", errors: [{ messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }, { messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(cb || function() {})", errors: ["Array.prototype.every() expects a return value from function."] }, + { code: "foo.every(cb || function foo() {})", errors: ["Array.prototype.every() expects a return value from function 'foo'."] }, + { code: "foo.every(a ? function() {} : function() {})", errors: ["Array.prototype.every() expects a return value from function.", "Array.prototype.every() expects a return value from function."] }, + { code: "foo.every(a ? function foo() {} : function bar() {})", errors: ["Array.prototype.every() expects a return value from function 'foo'.", "Array.prototype.every() expects a return value from function 'bar'."] }, + { code: "foo.every(function(){ return function() {}; }())", errors: [{ message: "Array.prototype.every() expects a return value from function.", column: 30 }] }, + { code: "foo.every(function(){ return function foo() {}; }())", errors: [{ message: "Array.prototype.every() expects a return value from function 'foo'.", column: 30 }] }, + { code: "foo.every(() => {})", options: [{ allowImplicit: false }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Array.prototype.every() expects a return value from arrow function." }] }, + { code: "foo.every(() => {})", options: [{ allowImplicit: true }], parserOptions: { ecmaVersion: 6 }, errors: [{ message: "Array.prototype.every() expects a return value from arrow function." }] }, // options: { allowImplicit: true } - { code: "Array.from(x, function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.every(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.filter(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.find(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.map(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.reduce(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.reduceRight(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.bar.baz.every(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.every(cb || function() {})", options: allowImplicitOptions, errors: ["Expected to return a value in function."] }, - { code: "[\"foo\",\"bar\"].sort(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.forEach(x => x)", options: allowImplicitCheckForEach, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Arrow function" } }] }, - { code: "foo.forEach(function(x) { if (a == b) {return x;}})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function" } }] }, - { code: "foo.forEach(function bar(x) { return x;})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function 'bar'" } }] }, + { code: "Array.from(x, function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.from" } }] }, + { code: "foo.every(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.filter(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, + { code: "foo.find(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.find" } }] }, + { code: "foo.map(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.map" } }] }, + { code: "foo.reduce(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.reduce" } }] }, + { code: "foo.reduceRight(function() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.reduceRight" } }] }, + { code: "foo.bar.baz.every(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.every(cb || function() {})", options: allowImplicitOptions, errors: ["Array.prototype.every() expects a return value from function."] }, + { code: "[\"foo\",\"bar\"].sort(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.sort" } }] }, + { code: "foo.forEach(x => x)", options: allowImplicitCheckForEach, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(function(x) { if (a == b) {return x;}})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(function bar(x) { return x;})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function 'bar'", arrayMethodName: "Array.prototype.forEach" } }] }, // // options: { checkForEach: true } - { code: "foo.forEach(x => x)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Arrow function" } }] }, - { code: "foo.forEach(val => y += val)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Arrow function" } }] }, - { code: "[\"foo\",\"bar\"].forEach(x => ++x)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Arrow function" } }] }, - { code: "foo.bar().forEach(x => x === y)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Arrow function" } }] }, - { code: "foo.forEach(function() {return function() { if (a == b) { return a; }}}())", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function" } }] }, - { code: "foo.forEach(function(x) { if (a == b) {return x;}})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function" } }] }, - { code: "foo.forEach(function(x) { if (a == b) {return undefined;}})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function" } }] }, - { code: "foo.forEach(function bar(x) { return x;})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function 'bar'" } }] }, - { code: "foo.bar().forEach(function bar(x) { return x;})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function 'bar'" } }] }, - { code: "[\"foo\",\"bar\"].forEach(function bar(x) { return x;})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Function 'bar'" } }] }, - { code: "foo.forEach((x) => { return x;})", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "Arrow function" } }] }, - { code: "Array.from(x, function() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.every(function() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function" } }] }, - { code: "foo.filter(function foo() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'" } }] }, - { code: "foo.filter(function foo() { return; })", options: checkForEachOptions, errors: [{ messageId: "expectedReturnValue", data: { name: "Function 'foo'" } }] }, - { code: "foo.every(cb || function() {})", options: checkForEachOptions, errors: ["Expected to return a value in function."] }, + { code: "foo.forEach(x => x)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(val => y += val)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "[\"foo\",\"bar\"].forEach(x => ++x)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.bar().forEach(x => x === y)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(function() {return function() { if (a == b) { return a; }}}())", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(function(x) { if (a == b) {return x;}})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(function(x) { if (a == b) {return undefined;}})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(function bar(x) { return x;})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function 'bar'", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(function bar(x) { return x;})", options: checkForEachOptions, errors: ["Array.prototype.forEach() expects no useless return value from function 'bar'."] }, + { code: "foo.bar().forEach(function bar(x) { return x;})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function 'bar'", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "[\"foo\",\"bar\"].forEach(function bar(x) { return x;})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function 'bar'", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach((x) => { return x;})", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "Array.from(x, function() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.from" } }] }, + { code: "foo.every(function() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function", arrayMethodName: "Array.prototype.every" } }] }, + { code: "foo.filter(function foo() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, + { code: "foo.filter(function foo() { return; })", options: checkForEachOptions, errors: [{ messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, + { code: "foo.every(cb || function() {})", options: checkForEachOptions, errors: ["Array.prototype.every() expects a return value from function."] }, // full location tests { @@ -206,7 +207,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", - data: { name: "arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" }, type: "ArrowFunctionExpression", line: 1, column: 16, @@ -219,7 +220,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", - data: { name: "arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" }, type: "ArrowFunctionExpression", line: 2, column: 4, @@ -232,7 +233,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", - data: { name: "arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" }, type: "ArrowFunctionExpression", line: 1, column: 26, @@ -245,7 +246,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedReturnValue", - data: { name: "Arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" }, type: "ReturnStatement", line: 1, column: 21, @@ -258,7 +259,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedInside", - data: { name: "arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.from" }, type: "ArrowFunctionExpression", line: 1, column: 21, @@ -272,7 +273,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", - data: { name: "Arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, type: "ArrowFunctionExpression", line: 1, column: 17, @@ -286,7 +287,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", - data: { name: "Arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, type: "ArrowFunctionExpression", line: 1, column: 41, @@ -300,7 +301,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", - data: { name: "Arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, type: "ArrowFunctionExpression", line: 2, column: 13, @@ -314,7 +315,7 @@ ruleTester.run("array-callback-return", rule, { parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", - data: { name: "Arrow function" }, + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, type: "ReturnStatement", line: 1, column: 52, @@ -326,7 +327,7 @@ ruleTester.run("array-callback-return", rule, { code: "foo.filter(function(){})", errors: [{ messageId: "expectedInside", - data: { name: "function" }, + data: { name: "function", arrayMethodName: "Array.prototype.filter" }, type: "FunctionExpression", line: 1, column: 12, @@ -338,7 +339,7 @@ ruleTester.run("array-callback-return", rule, { code: "foo.filter(function (){})", errors: [{ messageId: "expectedInside", - data: { name: "function" }, + data: { name: "function", arrayMethodName: "Array.prototype.filter" }, type: "FunctionExpression", line: 1, column: 12, @@ -350,7 +351,7 @@ ruleTester.run("array-callback-return", rule, { code: "foo.filter(function\n(){})", errors: [{ messageId: "expectedInside", - data: { name: "function" }, + data: { name: "function", arrayMethodName: "Array.prototype.filter" }, type: "FunctionExpression", line: 1, column: 12, @@ -362,7 +363,7 @@ ruleTester.run("array-callback-return", rule, { code: "foo.filter(function bar(){})", errors: [{ messageId: "expectedInside", - data: { name: "function 'bar'" }, + data: { name: "function 'bar'", arrayMethodName: "Array.prototype.filter" }, type: "FunctionExpression", line: 1, column: 12, @@ -374,7 +375,7 @@ ruleTester.run("array-callback-return", rule, { code: "foo.filter(function bar (){})", errors: [{ messageId: "expectedInside", - data: { name: "function 'bar'" }, + data: { name: "function 'bar'", arrayMethodName: "Array.prototype.filter" }, type: "FunctionExpression", line: 1, column: 12, @@ -386,7 +387,7 @@ ruleTester.run("array-callback-return", rule, { code: "foo.filter(function\n bar() {})", errors: [{ messageId: "expectedInside", - data: { name: "function 'bar'" }, + data: { name: "function 'bar'", arrayMethodName: "Array.prototype.filter" }, type: "FunctionExpression", line: 1, column: 12, @@ -398,7 +399,7 @@ ruleTester.run("array-callback-return", rule, { code: "Array.from(foo, function bar(){})", errors: [{ messageId: "expectedInside", - data: { name: "function 'bar'" }, + data: { name: "function 'bar'", arrayMethodName: "Array.from" }, type: "FunctionExpression", line: 1, column: 17, @@ -410,7 +411,7 @@ ruleTester.run("array-callback-return", rule, { code: "Array.from(foo, bar ? function (){} : baz)", errors: [{ messageId: "expectedInside", - data: { name: "function" }, + data: { name: "function", arrayMethodName: "Array.from" }, type: "FunctionExpression", line: 1, column: 23, @@ -422,7 +423,7 @@ ruleTester.run("array-callback-return", rule, { code: "foo.filter(function bar() { return \n })", errors: [{ messageId: "expectedReturnValue", - data: { name: "Function 'bar'" }, + data: { name: "function 'bar'", arrayMethodName: "Array.prototype.filter" }, type: "ReturnStatement", line: 1, column: 29, @@ -435,7 +436,7 @@ ruleTester.run("array-callback-return", rule, { options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", - data: { name: "Function" }, + data: { name: "function", arrayMethodName: "Array.prototype.forEach" }, type: "ReturnStatement", line: 2, column: 10, From 0a463dbf7cc5a77d442879c9117204d4d38db972 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 3 Jul 2020 21:44:38 +0200 Subject: [PATCH 18/66] Docs: fix no-multiple-empty-lines examples (fixes #13432) (#13433) --- docs/rules/no-multiple-empty-lines.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/rules/no-multiple-empty-lines.md b/docs/rules/no-multiple-empty-lines.md index 836cb18e471..aff6c2e6f1b 100644 --- a/docs/rules/no-multiple-empty-lines.md +++ b/docs/rules/no-multiple-empty-lines.md @@ -23,6 +23,8 @@ Examples of **incorrect** code for this rule with the default `{ "max": 2 }` opt var foo = 5; + + var bar = 3; ``` @@ -33,6 +35,7 @@ Examples of **correct** code for this rule with the default `{ "max": 2 }` optio var foo = 5; + var bar = 3; ``` @@ -45,7 +48,10 @@ Examples of **incorrect** code for this rule with the `{ max: 2, maxEOF: 0 }` op var foo = 5; + var bar = 3; + + ``` Examples of **correct** code for this rule with the `{ max: 2, maxEOF: 0 }` options: @@ -55,6 +61,7 @@ Examples of **correct** code for this rule with the `{ max: 2, maxEOF: 0 }` opti var foo = 5; + var bar = 3; ``` @@ -92,8 +99,10 @@ Examples of **incorrect** code for this rule with the `{ max: 2, maxBOF: 1 }` op ```js /*eslint no-multiple-empty-lines: ["error", { "max": 2, "maxBOF": 1 }]*/ + var foo = 5; + var bar = 3; ``` @@ -104,6 +113,7 @@ Examples of **correct** code for this rule with the `{ max: 2, maxBOF: 1 }` opti var foo = 5; + var bar = 3; ``` From 89ee01e083f1e02293bf8d1447f9b0fdb3cb9384 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, 4 Jul 2020 03:49:44 +0800 Subject: [PATCH 19/66] Fix: Revert config cloning (fixes #13447) (#13449) * Revert "Fix: Replace Infinity with Number.MAX_SAFE_INTEGER (fixes #13427) (#13435)" This reverts commit de77c11e7515f2097ff355ddc0d7b6db9c83c892. * Revert "Fix: clone config before validating (fixes #12592) (#13034)" This reverts commit 7fb45cf13e9908d489bd6d5fba3b7243c01508b9. --- lib/cli-engine/config-array-factory.js | 34 +--------- .../cloned-config/circularRefEslintConfig.js | 15 ----- .../cloned-config/configWithInfinity.js | 6 -- .../config-file/cloned-config/eslintConfig.js | 4 -- .../cloned-config/eslintConfigFail.js | 13 ---- .../config-file/cloned-config/index.js | 1 - .../config-file/cloned-config/inlineText.js | 3 - tests/lib/cli.js | 63 ------------------- 8 files changed, 1 insertion(+), 138 deletions(-) delete mode 100644 tests/fixtures/config-file/cloned-config/circularRefEslintConfig.js delete mode 100644 tests/fixtures/config-file/cloned-config/configWithInfinity.js delete mode 100644 tests/fixtures/config-file/cloned-config/eslintConfig.js delete mode 100644 tests/fixtures/config-file/cloned-config/eslintConfigFail.js delete mode 100644 tests/fixtures/config-file/cloned-config/index.js delete mode 100644 tests/fixtures/config-file/cloned-config/inlineText.js diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index 39f62bb6da1..7c0fba65c67 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -697,38 +697,6 @@ class ConfigArrayFactory { ctx.matchBasePath ); - /** - * Cloning the rule's config as we are setting `useDefaults` to true` - * which mutates the config with its default value if present. And when we - * refer to a same variable for config for different rules, that referred variable will - * be mutated and it will be used for both. - * - * Example: - * - * const commonRuleConfig = ['error', {}]; - * - * Now if we use this variable as a config for rules like this - * - * { - * rules: { - * "a" : commonRuleConfig, - * "b" : commonRuleConfig, - * } - * } - * - * And if these rules have default values in their schema, their - * config will be mutated with default values, the mutated `commonRuleConfig` will be used for `b` as well and it probably - * throw schema voilation errors. - * - * Refer https://github.com/eslint/eslint/issues/12592 - */ - const clonedRulesConfig = rules && JSON.parse( - JSON.stringify( - rules, - (key, value) => (value === Infinity ? Number.MAX_SAFE_INTEGER : value) - ) - ); - // Flatten `extends`. for (const extendName of extendList.filter(Boolean)) { yield* this._loadExtends(extendName, ctx); @@ -763,7 +731,7 @@ class ConfigArrayFactory { processor, reportUnusedDisableDirectives, root, - rules: clonedRulesConfig, + rules, settings }; diff --git a/tests/fixtures/config-file/cloned-config/circularRefEslintConfig.js b/tests/fixtures/config-file/cloned-config/circularRefEslintConfig.js deleted file mode 100644 index cc9fcdbcc7c..00000000000 --- a/tests/fixtures/config-file/cloned-config/circularRefEslintConfig.js +++ /dev/null @@ -1,15 +0,0 @@ -let errorConfig = ["error", {}]; - -class CircularRef { - constructor() { - this.self = this; - } -} - -module.exports = { - settings: { - sharedData: new CircularRef() - }, - - rules: { camelcase: errorConfig, "default-case": errorConfig } -}; diff --git a/tests/fixtures/config-file/cloned-config/configWithInfinity.js b/tests/fixtures/config-file/cloned-config/configWithInfinity.js deleted file mode 100644 index 73e3c93c085..00000000000 --- a/tests/fixtures/config-file/cloned-config/configWithInfinity.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - - rules: { - "max-len": [ "error", { code: Infinity }] - } -}; diff --git a/tests/fixtures/config-file/cloned-config/eslintConfig.js b/tests/fixtures/config-file/cloned-config/eslintConfig.js deleted file mode 100644 index 8602e60b0fd..00000000000 --- a/tests/fixtures/config-file/cloned-config/eslintConfig.js +++ /dev/null @@ -1,4 +0,0 @@ -let errorConfig = ["error", {}]; -module.exports = { - rules: { camelcase: errorConfig, "default-case": errorConfig } -}; diff --git a/tests/fixtures/config-file/cloned-config/eslintConfigFail.js b/tests/fixtures/config-file/cloned-config/eslintConfigFail.js deleted file mode 100644 index b8702c6ffb8..00000000000 --- a/tests/fixtures/config-file/cloned-config/eslintConfigFail.js +++ /dev/null @@ -1,13 +0,0 @@ -let errorConfig = ["error", {}]; -module.exports = { - rules: { - camelcase: errorConfig, - "default-case": errorConfig , - "camelcase" : [ - 'error', - { - "ignoreDestructuring": new Date().getUTCFullYear // returns a function - } - ] - } -}; diff --git a/tests/fixtures/config-file/cloned-config/index.js b/tests/fixtures/config-file/cloned-config/index.js deleted file mode 100644 index b792f78cad3..00000000000 --- a/tests/fixtures/config-file/cloned-config/index.js +++ /dev/null @@ -1 +0,0 @@ -var someValue = "bar"; diff --git a/tests/fixtures/config-file/cloned-config/inlineText.js b/tests/fixtures/config-file/cloned-config/inlineText.js deleted file mode 100644 index e000713abaf..00000000000 --- a/tests/fixtures/config-file/cloned-config/inlineText.js +++ /dev/null @@ -1,3 +0,0 @@ -/*eslint default-case: ["error", {}]*/ -/*eslint camelcase: ["error", {}]*/ -var someValue = "bar"; diff --git a/tests/lib/cli.js b/tests/lib/cli.js index 2f84900eefb..a1d9a23e491 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -187,7 +187,6 @@ describe("cli", () => { }); }); - describe("when given a config with environment set to Node.js", () => { it("should execute without any errors", async () => { const configPath = getFixturePath("configurations", "env-node.json"); @@ -1161,66 +1160,4 @@ describe("cli", () => { }); }); - describe("testing the cloned config", () => { - describe("config file and input file", () => { - it("should not modify original configuration object", async () => { - const configPath = getFixturePath("config-file", "cloned-config", "eslintConfig.js"); - const filePath = getFixturePath("config-file", "cloned-config", "index.js"); - const args = `--config ${configPath} ${filePath}`; - - const exit = await cli.execute(args); - - assert.strictEqual(exit, 0); - }); - - it("should exit with 1 as camelcase has wrong property type", async () => { - const configPath = getFixturePath("config-file", "cloned-config", "eslintConfigFail.js"); - const filePath = getFixturePath("config-file", "cloned-config", "index.js"); - const args = `--config ${configPath} ${filePath}`; - - try { - await cli.execute(args); - } catch (error) { - assert.strictEqual(/Configuration for rule "camelcase" is invalid:/u.test(error), true); - } - - }); - - it("should not cause an error when a rule configuration has `Infinity`", async () => { - const configPath = getFixturePath("config-file", "cloned-config", "configWithInfinity.js"); - const filePath = getFixturePath("config-file", "cloned-config", "index.js"); - const args = `--config ${configPath} ${filePath}`; - - const exit = await cli.execute(args); - - assert.strictEqual(exit, 0); - }); - }); - - describe("inline config and input file", () => { - it("should not modify original configuration object", async () => { - const filePath = getFixturePath("config-file", "cloned-config", "inlineText.js"); - const args = `${filePath}`; - - const exit = await cli.execute(args); - - assert.strictEqual(exit, 0); - }); - }); - - }); - - describe("handling circular reference while cloning", () => { - it("should handle circular ref", async () => { - const configPath = getFixturePath("config-file", "cloned-config", "circularRefEslintConfig.js"); - const filePath = getFixturePath("config-file", "cloned-config", "index.js"); - const args = `--config ${configPath} ${filePath}`; - - try { - await cli.execute(args); - } catch (error) { - assert.instanceOf(error, Error); - } - }); - }); }); From ada2c891298382f82dfabf37cacd59a1057b2bb7 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 3 Jul 2020 21:50:40 +0200 Subject: [PATCH 20/66] Fix: support typescript generics in arrow-parens (fixes #12570) (#13451) --- lib/rules/arrow-parens.js | 199 +++++++------- .../arrow-parens/generics-extends-complex.js | 249 ++++++++++++++++++ .../parsers/arrow-parens/generics-extends.js | 151 +++++++++++ .../arrow-parens/generics-simple-async.js | 128 +++++++++ .../arrow-parens/generics-simple-no-params.js | 106 ++++++++ .../parsers/arrow-parens/generics-simple.js | 119 +++++++++ tests/lib/rules/arrow-parens.js | 134 ++++++++++ 7 files changed, 978 insertions(+), 108 deletions(-) create mode 100644 tests/fixtures/parsers/arrow-parens/generics-extends-complex.js create mode 100644 tests/fixtures/parsers/arrow-parens/generics-extends.js create mode 100644 tests/fixtures/parsers/arrow-parens/generics-simple-async.js create mode 100644 tests/fixtures/parsers/arrow-parens/generics-simple-no-params.js create mode 100644 tests/fixtures/parsers/arrow-parens/generics-simple.js diff --git a/lib/rules/arrow-parens.js b/lib/rules/arrow-parens.js index bfd32447ac6..eaa1aab0238 100644 --- a/lib/rules/arrow-parens.js +++ b/lib/rules/arrow-parens.js @@ -15,15 +15,12 @@ const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ /** - * Get location should be reported by AST node. - * @param {ASTNode} node AST Node. - * @returns {Location} Location information. + * Determines if the given arrow function has block body. + * @param {ASTNode} node `ArrowFunctionExpression` node. + * @returns {boolean} `true` if the function has block body. */ -function getLocation(node) { - return { - start: node.params[0].loc.start, - end: node.params[node.params.length - 1].loc.end - }; +function hasBlockBody(node) { + return node.body.type === "BlockStatement"; } //------------------------------------------------------------------------------ @@ -75,126 +72,112 @@ module.exports = { const sourceCode = context.getSourceCode(); /** - * Determines whether a arrow function argument end with `)` - * @param {ASTNode} node The arrow function node. - * @returns {void} + * Finds opening paren of parameters for the given arrow function, if it exists. + * It is assumed that the given arrow function has exactly one parameter. + * @param {ASTNode} node `ArrowFunctionExpression` node. + * @returns {Token|null} the opening paren, or `null` if the given arrow function doesn't have parens of parameters. */ - function parens(node) { - const isAsync = node.async; - const firstTokenOfParam = sourceCode.getFirstToken(node, isAsync ? 1 : 0); - - /** - * Remove the parenthesis around a parameter - * @param {Fixer} fixer Fixer - * @returns {string} fixed parameter - */ - function fixParamsWithParenthesis(fixer) { - const paramToken = sourceCode.getTokenAfter(firstTokenOfParam); - - /* - * ES8 allows Trailing commas in function parameter lists and calls - * https://github.com/eslint/eslint/issues/8834 - */ - const closingParenToken = sourceCode.getTokenAfter(paramToken, astUtils.isClosingParenToken); - const asyncToken = isAsync ? sourceCode.getTokenBefore(firstTokenOfParam) : null; - const shouldAddSpaceForAsync = asyncToken && (asyncToken.range[1] === firstTokenOfParam.range[0]); - - return fixer.replaceTextRange([ - firstTokenOfParam.range[0], - closingParenToken.range[1] - ], `${shouldAddSpaceForAsync ? " " : ""}${paramToken.value}`); + function findOpeningParenOfParams(node) { + const tokenBeforeParams = sourceCode.getTokenBefore(node.params[0]); + + if ( + tokenBeforeParams && + astUtils.isOpeningParenToken(tokenBeforeParams) && + node.range[0] <= tokenBeforeParams.range[0] + ) { + return tokenBeforeParams; } - /** - * Checks whether there are comments inside the params or not. - * @returns {boolean} `true` if there are comments inside of parens, else `false` - */ - function hasCommentsInParens() { - if (astUtils.isOpeningParenToken(firstTokenOfParam)) { - const closingParenToken = sourceCode.getTokenAfter(node.params[0], astUtils.isClosingParenToken); + return null; + } - return closingParenToken && sourceCode.commentsExistBetween(firstTokenOfParam, closingParenToken); - } - return false; + /** + * Finds closing paren of parameters for the given arrow function. + * It is assumed that the given arrow function has parens of parameters and that it has exactly one parameter. + * @param {ASTNode} node `ArrowFunctionExpression` node. + * @returns {Token} the closing paren of parameters. + */ + function getClosingParenOfParams(node) { + return sourceCode.getTokenAfter(node.params[0], astUtils.isClosingParenToken); + } - } + /** + * Determines whether the given arrow function has comments inside parens of parameters. + * It is assumed that the given arrow function has parens of parameters. + * @param {ASTNode} node `ArrowFunctionExpression` node. + * @param {Token} openingParen Opening paren of parameters. + * @returns {boolean} `true` if the function has at least one comment inside of parens of parameters. + */ + function hasCommentsInParensOfParams(node, openingParen) { + return sourceCode.commentsExistBetween(openingParen, getClosingParenOfParams(node)); + } - if (hasCommentsInParens()) { - return; - } + /** + * Determines whether the given arrow function has unexpected tokens before opening paren of parameters, + * in which case it will be assumed that the existing parens of parameters are necessary. + * Only tokens within the range of the arrow function (tokens that are part of the arrow function) are taken into account. + * Example: (a) => b + * @param {ASTNode} node `ArrowFunctionExpression` node. + * @param {Token} openingParen Opening paren of parameters. + * @returns {boolean} `true` if the function has at least one unexpected token. + */ + function hasUnexpectedTokensBeforeOpeningParen(node, openingParen) { + const expectedCount = node.async ? 1 : 0; - // "as-needed", { "requireForBlockBody": true }: x => x - if ( - requireForBlockBody && - node.params[0].type === "Identifier" && - !node.params[0].typeAnnotation && - node.body.type !== "BlockStatement" && - !node.returnType - ) { - if (astUtils.isOpeningParenToken(firstTokenOfParam)) { - context.report({ - node, - messageId: "unexpectedParensInline", - loc: getLocation(node), - fix: fixParamsWithParenthesis - }); - } - return; - } + return sourceCode.getFirstToken(node, { skip: expectedCount }) !== openingParen; + } - if ( - requireForBlockBody && - node.body.type === "BlockStatement" - ) { - if (!astUtils.isOpeningParenToken(firstTokenOfParam)) { - context.report({ - node, - messageId: "expectedParensBlock", - loc: getLocation(node), - fix(fixer) { - return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`); - } - }); - } - return; - } + return { + "ArrowFunctionExpression[params.length=1]"(node) { + const shouldHaveParens = !asNeeded || requireForBlockBody && hasBlockBody(node); + const openingParen = findOpeningParenOfParams(node); + const hasParens = openingParen !== null; + const [param] = node.params; - // "as-needed": x => x - if (asNeeded && - node.params[0].type === "Identifier" && - !node.params[0].typeAnnotation && - !node.returnType - ) { - if (astUtils.isOpeningParenToken(firstTokenOfParam)) { + if (shouldHaveParens && !hasParens) { context.report({ node, - messageId: "unexpectedParens", - loc: getLocation(node), - fix: fixParamsWithParenthesis + messageId: requireForBlockBody ? "expectedParensBlock" : "expectedParens", + loc: param.loc, + *fix(fixer) { + yield fixer.insertTextBefore(param, "("); + yield fixer.insertTextAfter(param, ")"); + } }); } - return; - } - if (firstTokenOfParam.type === "Identifier") { - const after = sourceCode.getTokenAfter(firstTokenOfParam); - - // (x) => x - if (after.value !== ")") { + if ( + !shouldHaveParens && + hasParens && + param.type === "Identifier" && + !param.typeAnnotation && + !node.returnType && + !hasCommentsInParensOfParams(node, openingParen) && + !hasUnexpectedTokensBeforeOpeningParen(node, openingParen) + ) { context.report({ node, - messageId: "expectedParens", - loc: getLocation(node), - fix(fixer) { - return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`); + messageId: requireForBlockBody ? "unexpectedParensInline" : "unexpectedParens", + loc: param.loc, + *fix(fixer) { + const tokenBeforeOpeningParen = sourceCode.getTokenBefore(openingParen); + const closingParen = getClosingParenOfParams(node); + + if ( + tokenBeforeOpeningParen && + tokenBeforeOpeningParen.range[1] === openingParen.range[0] && + !astUtils.canTokensBeAdjacent(tokenBeforeOpeningParen, sourceCode.getFirstToken(param)) + ) { + yield fixer.insertTextBefore(openingParen, " "); + } + + // remove parens, whitespace inside parens, and possible trailing comma + yield fixer.removeRange([openingParen.range[0], param.range[0]]); + yield fixer.removeRange([param.range[1], closingParen.range[1]]); } }); } } - } - - return { - "ArrowFunctionExpression[params.length=1]": parens }; } }; diff --git a/tests/fixtures/parsers/arrow-parens/generics-extends-complex.js b/tests/fixtures/parsers/arrow-parens/generics-extends-complex.js new file mode 100644 index 00000000000..4a27b495277 --- /dev/null +++ b/tests/fixtures/parsers/arrow-parens/generics-extends-complex.js @@ -0,0 +1,249 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser@3.5.0 + * Source code: + * (a) => b + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [ + { + type: "Identifier", + name: "a", + range: [24, 25], + loc: { + start: { line: 1, column: 24 }, + end: { line: 1, column: 25 }, + }, + }, + ], + body: { + type: "Identifier", + name: "b", + range: [30, 31], + loc: { start: { line: 1, column: 30 }, end: { line: 1, column: 31 } }, + }, + async: false, + expression: true, + range: [0, 31], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 31 } }, + typeParameters: { + type: "TSTypeParameterDeclaration", + range: [0, 23], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 23 } }, + params: [ + { + type: "TSTypeParameter", + name: { + type: "Identifier", + name: "T", + range: [1, 2], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 2 }, + }, + }, + constraint: { + type: "TSIntersectionType", + types: [ + { + type: "TSParenthesizedType", + typeAnnotation: { + type: "TSUnionType", + types: [ + { + type: "TSTypeReference", + typeName: { + type: "Identifier", + name: "A", + range: [12, 13], + loc: { + start: { line: 1, column: 12 }, + end: { line: 1, column: 13 }, + }, + }, + range: [12, 13], + loc: { + start: { line: 1, column: 12 }, + end: { line: 1, column: 13 }, + }, + }, + { + type: "TSTypeReference", + typeName: { + type: "Identifier", + name: "B", + range: [16, 17], + loc: { + start: { line: 1, column: 16 }, + end: { line: 1, column: 17 }, + }, + }, + range: [16, 17], + loc: { + start: { line: 1, column: 16 }, + end: { line: 1, column: 17 }, + }, + }, + ], + range: [12, 17], + loc: { + start: { line: 1, column: 12 }, + end: { line: 1, column: 17 }, + }, + }, + range: [11, 18], + loc: { + start: { line: 1, column: 11 }, + end: { line: 1, column: 18 }, + }, + }, + { + type: "TSTypeReference", + typeName: { + type: "Identifier", + name: "C", + range: [21, 22], + loc: { + start: { line: 1, column: 21 }, + end: { line: 1, column: 22 }, + }, + }, + range: [21, 22], + loc: { + start: { line: 1, column: 21 }, + end: { line: 1, column: 22 }, + }, + }, + ], + range: [11, 22], + loc: { + start: { line: 1, column: 11 }, + end: { line: 1, column: 22 }, + }, + }, + range: [1, 22], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 22 }, + }, + }, + ], + }, + }, + range: [0, 31], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 31 } }, + }, + ], + sourceType: "script", + range: [0, 31], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 31 } }, + tokens: [ + { + type: "Punctuator", + value: "<", + range: [0, 1], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }, + }, + { + type: "Identifier", + value: "T", + range: [1, 2], + loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, + }, + { + type: "Keyword", + value: "extends", + range: [3, 10], + loc: { start: { line: 1, column: 3 }, end: { line: 1, column: 10 } }, + }, + { + type: "Punctuator", + value: "(", + range: [11, 12], + loc: { start: { line: 1, column: 11 }, end: { line: 1, column: 12 } }, + }, + { + type: "Identifier", + value: "A", + range: [12, 13], + loc: { start: { line: 1, column: 12 }, end: { line: 1, column: 13 } }, + }, + { + type: "Punctuator", + value: "|", + range: [14, 15], + loc: { start: { line: 1, column: 14 }, end: { line: 1, column: 15 } }, + }, + { + type: "Identifier", + value: "B", + range: [16, 17], + loc: { start: { line: 1, column: 16 }, end: { line: 1, column: 17 } }, + }, + { + type: "Punctuator", + value: ")", + range: [17, 18], + loc: { start: { line: 1, column: 17 }, end: { line: 1, column: 18 } }, + }, + { + type: "Punctuator", + value: "&", + range: [19, 20], + loc: { start: { line: 1, column: 19 }, end: { line: 1, column: 20 } }, + }, + { + type: "Identifier", + value: "C", + range: [21, 22], + loc: { start: { line: 1, column: 21 }, end: { line: 1, column: 22 } }, + }, + { + type: "Punctuator", + value: ">", + range: [22, 23], + loc: { start: { line: 1, column: 22 }, end: { line: 1, column: 23 } }, + }, + { + type: "Punctuator", + value: "(", + range: [23, 24], + loc: { start: { line: 1, column: 23 }, end: { line: 1, column: 24 } }, + }, + { + type: "Identifier", + value: "a", + range: [24, 25], + loc: { start: { line: 1, column: 24 }, end: { line: 1, column: 25 } }, + }, + { + type: "Punctuator", + value: ")", + range: [25, 26], + loc: { start: { line: 1, column: 25 }, end: { line: 1, column: 26 } }, + }, + { + type: "Punctuator", + value: "=>", + range: [27, 29], + loc: { start: { line: 1, column: 27 }, end: { line: 1, column: 29 } }, + }, + { + type: "Identifier", + value: "b", + range: [30, 31], + loc: { start: { line: 1, column: 30 }, end: { line: 1, column: 31 } }, + }, + ], + comments: [], + }); diff --git a/tests/fixtures/parsers/arrow-parens/generics-extends.js b/tests/fixtures/parsers/arrow-parens/generics-extends.js new file mode 100644 index 00000000000..ee70b565635 --- /dev/null +++ b/tests/fixtures/parsers/arrow-parens/generics-extends.js @@ -0,0 +1,151 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser@3.5.0 + * Source code: + * (a) => b + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [ + { + type: "Identifier", + name: "a", + range: [14, 15], + loc: { + start: { line: 1, column: 14 }, + end: { line: 1, column: 15 }, + }, + }, + ], + body: { + type: "Identifier", + name: "b", + range: [20, 21], + loc: { start: { line: 1, column: 20 }, end: { line: 1, column: 21 } }, + }, + async: false, + expression: true, + range: [0, 21], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 21 } }, + typeParameters: { + type: "TSTypeParameterDeclaration", + range: [0, 13], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 13 } }, + params: [ + { + type: "TSTypeParameter", + name: { + type: "Identifier", + name: "T", + range: [1, 2], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 2 }, + }, + }, + constraint: { + type: "TSTypeReference", + typeName: { + type: "Identifier", + name: "A", + range: [11, 12], + loc: { + start: { line: 1, column: 11 }, + end: { line: 1, column: 12 }, + }, + }, + range: [11, 12], + loc: { + start: { line: 1, column: 11 }, + end: { line: 1, column: 12 }, + }, + }, + range: [1, 12], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 12 }, + }, + }, + ], + }, + }, + range: [0, 21], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 21 } }, + }, + ], + sourceType: "script", + range: [0, 21], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 21 } }, + tokens: [ + { + type: "Punctuator", + value: "<", + range: [0, 1], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }, + }, + { + type: "Identifier", + value: "T", + range: [1, 2], + loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, + }, + { + type: "Keyword", + value: "extends", + range: [3, 10], + loc: { start: { line: 1, column: 3 }, end: { line: 1, column: 10 } }, + }, + { + type: "Identifier", + value: "A", + range: [11, 12], + loc: { start: { line: 1, column: 11 }, end: { line: 1, column: 12 } }, + }, + { + type: "Punctuator", + value: ">", + range: [12, 13], + loc: { start: { line: 1, column: 12 }, end: { line: 1, column: 13 } }, + }, + { + type: "Punctuator", + value: "(", + range: [13, 14], + loc: { start: { line: 1, column: 13 }, end: { line: 1, column: 14 } }, + }, + { + type: "Identifier", + value: "a", + range: [14, 15], + loc: { start: { line: 1, column: 14 }, end: { line: 1, column: 15 } }, + }, + { + type: "Punctuator", + value: ")", + range: [15, 16], + loc: { start: { line: 1, column: 15 }, end: { line: 1, column: 16 } }, + }, + { + type: "Punctuator", + value: "=>", + range: [17, 19], + loc: { start: { line: 1, column: 17 }, end: { line: 1, column: 19 } }, + }, + { + type: "Identifier", + value: "b", + range: [20, 21], + loc: { start: { line: 1, column: 20 }, end: { line: 1, column: 21 } }, + }, + ], + comments: [], + }); diff --git a/tests/fixtures/parsers/arrow-parens/generics-simple-async.js b/tests/fixtures/parsers/arrow-parens/generics-simple-async.js new file mode 100644 index 00000000000..68926127f2b --- /dev/null +++ b/tests/fixtures/parsers/arrow-parens/generics-simple-async.js @@ -0,0 +1,128 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser@3.5.0 + * Source code: + * async (a) => b + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [ + { + type: "Identifier", + name: "a", + range: [10, 11], + loc: { + start: { line: 1, column: 10 }, + end: { line: 1, column: 11 }, + }, + }, + ], + body: { + type: "Identifier", + name: "b", + range: [16, 17], + loc: { start: { line: 1, column: 16 }, end: { line: 1, column: 17 } }, + }, + async: true, + expression: true, + range: [0, 17], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 17 } }, + typeParameters: { + type: "TSTypeParameterDeclaration", + range: [6, 9], + loc: { start: { line: 1, column: 6 }, end: { line: 1, column: 9 } }, + params: [ + { + type: "TSTypeParameter", + name: { + type: "Identifier", + name: "T", + range: [7, 8], + loc: { + start: { line: 1, column: 7 }, + end: { line: 1, column: 8 }, + }, + }, + range: [7, 8], + loc: { + start: { line: 1, column: 7 }, + end: { line: 1, column: 8 }, + }, + }, + ], + }, + }, + range: [0, 17], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 17 } }, + }, + ], + sourceType: "script", + range: [0, 17], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 17 } }, + tokens: [ + { + type: "Identifier", + value: "async", + range: [0, 5], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 5 } }, + }, + { + type: "Punctuator", + value: "<", + range: [6, 7], + loc: { start: { line: 1, column: 6 }, end: { line: 1, column: 7 } }, + }, + { + type: "Identifier", + value: "T", + range: [7, 8], + loc: { start: { line: 1, column: 7 }, end: { line: 1, column: 8 } }, + }, + { + type: "Punctuator", + value: ">", + range: [8, 9], + loc: { start: { line: 1, column: 8 }, end: { line: 1, column: 9 } }, + }, + { + type: "Punctuator", + value: "(", + range: [9, 10], + loc: { start: { line: 1, column: 9 }, end: { line: 1, column: 10 } }, + }, + { + type: "Identifier", + value: "a", + range: [10, 11], + loc: { start: { line: 1, column: 10 }, end: { line: 1, column: 11 } }, + }, + { + type: "Punctuator", + value: ")", + range: [11, 12], + loc: { start: { line: 1, column: 11 }, end: { line: 1, column: 12 } }, + }, + { + type: "Punctuator", + value: "=>", + range: [13, 15], + loc: { start: { line: 1, column: 13 }, end: { line: 1, column: 15 } }, + }, + { + type: "Identifier", + value: "b", + range: [16, 17], + loc: { start: { line: 1, column: 16 }, end: { line: 1, column: 17 } }, + }, + ], + comments: [], + }); diff --git a/tests/fixtures/parsers/arrow-parens/generics-simple-no-params.js b/tests/fixtures/parsers/arrow-parens/generics-simple-no-params.js new file mode 100644 index 00000000000..ab17a7d8804 --- /dev/null +++ b/tests/fixtures/parsers/arrow-parens/generics-simple-no-params.js @@ -0,0 +1,106 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser@3.5.0 + * Source code: + * () => b + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [], + body: { + type: "Identifier", + name: "b", + range: [9, 10], + loc: { start: { line: 1, column: 9 }, end: { line: 1, column: 10 } }, + }, + async: false, + expression: true, + range: [0, 10], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } }, + typeParameters: { + type: "TSTypeParameterDeclaration", + range: [0, 3], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 3 } }, + params: [ + { + type: "TSTypeParameter", + name: { + type: "Identifier", + name: "T", + range: [1, 2], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 2 }, + }, + }, + range: [1, 2], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 2 }, + }, + }, + ], + }, + }, + range: [0, 10], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } }, + }, + ], + sourceType: "script", + range: [0, 10], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } }, + tokens: [ + { + type: "Punctuator", + value: "<", + range: [0, 1], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }, + }, + { + type: "Identifier", + value: "T", + range: [1, 2], + loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, + }, + { + type: "Punctuator", + value: ">", + range: [2, 3], + loc: { start: { line: 1, column: 2 }, end: { line: 1, column: 3 } }, + }, + { + type: "Punctuator", + value: "(", + range: [3, 4], + loc: { start: { line: 1, column: 3 }, end: { line: 1, column: 4 } }, + }, + { + type: "Punctuator", + value: ")", + range: [4, 5], + loc: { start: { line: 1, column: 4 }, end: { line: 1, column: 5 } }, + }, + { + type: "Punctuator", + value: "=>", + range: [6, 8], + loc: { start: { line: 1, column: 6 }, end: { line: 1, column: 8 } }, + }, + { + type: "Identifier", + value: "b", + range: [9, 10], + loc: { start: { line: 1, column: 9 }, end: { line: 1, column: 10 } }, + }, + ], + comments: [], + }); diff --git a/tests/fixtures/parsers/arrow-parens/generics-simple.js b/tests/fixtures/parsers/arrow-parens/generics-simple.js new file mode 100644 index 00000000000..92f1a5bdb81 --- /dev/null +++ b/tests/fixtures/parsers/arrow-parens/generics-simple.js @@ -0,0 +1,119 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser@3.5.0 + * Source code: + * (a) => b + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "ArrowFunctionExpression", + generator: false, + id: null, + params: [ + { + type: "Identifier", + name: "a", + range: [4, 5], + loc: { start: { line: 1, column: 4 }, end: { line: 1, column: 5 } }, + }, + ], + body: { + type: "Identifier", + name: "b", + range: [10, 11], + loc: { start: { line: 1, column: 10 }, end: { line: 1, column: 11 } }, + }, + async: false, + expression: true, + range: [0, 11], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 11 } }, + typeParameters: { + type: "TSTypeParameterDeclaration", + range: [0, 3], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 3 } }, + params: [ + { + type: "TSTypeParameter", + name: { + type: "Identifier", + name: "T", + range: [1, 2], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 2 }, + }, + }, + range: [1, 2], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 2 }, + }, + }, + ], + }, + }, + range: [0, 11], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 11 } }, + }, + ], + sourceType: "script", + range: [0, 11], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 11 } }, + tokens: [ + { + type: "Punctuator", + value: "<", + range: [0, 1], + loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }, + }, + { + type: "Identifier", + value: "T", + range: [1, 2], + loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }, + }, + { + type: "Punctuator", + value: ">", + range: [2, 3], + loc: { start: { line: 1, column: 2 }, end: { line: 1, column: 3 } }, + }, + { + type: "Punctuator", + value: "(", + range: [3, 4], + loc: { start: { line: 1, column: 3 }, end: { line: 1, column: 4 } }, + }, + { + type: "Identifier", + value: "a", + range: [4, 5], + loc: { start: { line: 1, column: 4 }, end: { line: 1, column: 5 } }, + }, + { + type: "Punctuator", + value: ")", + range: [5, 6], + loc: { start: { line: 1, column: 5 }, end: { line: 1, column: 6 } }, + }, + { + type: "Punctuator", + value: "=>", + range: [7, 9], + loc: { start: { line: 1, column: 7 }, end: { line: 1, column: 9 } }, + }, + { + type: "Identifier", + value: "b", + range: [10, 11], + loc: { start: { line: 1, column: 10 }, end: { line: 1, column: 11 } }, + }, + ], + comments: [], + }); diff --git a/tests/lib/rules/arrow-parens.js b/tests/lib/rules/arrow-parens.js index edd1ae6da8a..61d76358440 100644 --- a/tests/lib/rules/arrow-parens.js +++ b/tests/lib/rules/arrow-parens.js @@ -45,16 +45,22 @@ const valid = [ { code: "a.then((foo) => {});", options: ["always"] }, { code: "a.then((foo) => { if (true) {}; });", options: ["always"] }, { code: "a.then(async (foo) => { if (true) {}; });", options: ["always"], parserOptions: { ecmaVersion: 8 } }, + { code: "(a: T) => a", options: ["always"], parser: parser("identifer-type") }, + { code: "(a): T => a", options: ["always"], parser: parser("return-type") }, // "as-needed" { code: "() => {}", options: ["as-needed"] }, { code: "a => {}", options: ["as-needed"] }, { code: "a => a", options: ["as-needed"] }, + { code: "a => (a)", options: ["as-needed"] }, + { code: "(a => a)", options: ["as-needed"] }, + { code: "((a => a))", options: ["as-needed"] }, { code: "([a, b]) => {}", options: ["as-needed"] }, { code: "({ a, b }) => {}", options: ["as-needed"] }, { code: "(a = 10) => {}", options: ["as-needed"] }, { code: "(...a) => a[0]", options: ["as-needed"] }, { code: "(a, b) => {}", options: ["as-needed"] }, + { code: "async a => a", options: ["as-needed"], parserOptions: { ecmaVersion: 8 } }, { code: "async ([a, b]) => {}", options: ["as-needed"], parserOptions: { ecmaVersion: 8 } }, { code: "async (a, b) => {}", options: ["as-needed"], parserOptions: { ecmaVersion: 8 } }, { code: "(a: T) => a", options: ["as-needed"], parser: parser("identifer-type") }, @@ -63,6 +69,9 @@ const valid = [ // "as-needed", { "requireForBlockBody": true } { code: "() => {}", options: ["as-needed", { requireForBlockBody: true }] }, { code: "a => a", options: ["as-needed", { requireForBlockBody: true }] }, + { code: "a => (a)", options: ["as-needed", { requireForBlockBody: true }] }, + { code: "(a => a)", options: ["as-needed", { requireForBlockBody: true }] }, + { code: "((a => a))", options: ["as-needed", { requireForBlockBody: true }] }, { code: "([a, b]) => {}", options: ["as-needed", { requireForBlockBody: true }] }, { code: "([a, b]) => a", options: ["as-needed", { requireForBlockBody: true }] }, { code: "({ a, b }) => {}", options: ["as-needed", { requireForBlockBody: true }] }, @@ -136,6 +145,83 @@ const valid = [ { code: "var bar = (/*comment here*/{a}) => a", options: ["as-needed"] + }, + + // generics + { + code: "(a) => b", + options: ["always"], + parser: parser("generics-simple") + }, + { + code: "(a) => b", + options: ["as-needed"], + parser: parser("generics-simple") + }, + { + code: "(a) => b", + options: ["as-needed", { requireForBlockBody: true }], + parser: parser("generics-simple") + }, + { + code: "async (a) => b", + options: ["always"], + parser: parser("generics-simple-async") + }, + { + code: "async (a) => b", + options: ["as-needed"], + parser: parser("generics-simple-async") + }, + { + code: "async (a) => b", + options: ["as-needed", { requireForBlockBody: true }], + parser: parser("generics-simple-async") + }, + { + code: "() => b", + options: ["always"], + parser: parser("generics-simple-no-params") + }, + { + code: "() => b", + options: ["as-needed"], + parser: parser("generics-simple-no-params") + }, + { + code: "() => b", + options: ["as-needed", { requireForBlockBody: true }], + parser: parser("generics-simple-no-params") + }, + { + code: "(a) => b", + options: ["always"], + parser: parser("generics-extends") + }, + { + code: "(a) => b", + options: ["as-needed"], + parser: parser("generics-extends") + }, + { + code: "(a) => b", + options: ["as-needed", { requireForBlockBody: true }], + parser: parser("generics-extends") + }, + { + code: "(a) => b", + options: ["always"], + parser: parser("generics-extends-complex") + }, + { + code: "(a) => b", + options: ["as-needed"], + parser: parser("generics-extends-complex") + }, + { + code: "(a) => b", + options: ["as-needed", { requireForBlockBody: true }], + parser: parser("generics-extends-complex") } ]; @@ -236,6 +322,30 @@ const invalid = [ type }] }, + { + code: "( a ) => b", + output: "a => b", + options: ["as-needed"], + errors: [{ + line: 1, + column: 4, + endColumn: 5, + messageId: "unexpectedParens", + type + }] + }, + { + code: "(\na\n) => b", + output: "a => b", + options: ["as-needed"], + errors: [{ + line: 2, + column: 1, + endColumn: 2, + messageId: "unexpectedParens", + type + }] + }, { code: "(a,) => a", output: "a => a", @@ -275,6 +385,30 @@ const invalid = [ type }] }, + { + code: "typeof((a) => {})", + output: "typeof(a => {})", + options: ["as-needed"], + errors: [{ + line: 1, + column: 9, + endColumn: 10, + messageId: "unexpectedParens", + type + }] + }, + { + code: "function *f() { yield(a) => a; }", + output: "function *f() { yield a => a; }", + options: ["as-needed"], + errors: [{ + line: 1, + column: 23, + endColumn: 24, + messageId: "unexpectedParens", + type + }] + }, // "as-needed", { "requireForBlockBody": true } { From f21bad2680406a2671b877f8dba47f4475d0cc64 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 3 Jul 2020 21:51:26 +0200 Subject: [PATCH 21/66] Docs: fix description for `never` in multiline-ternary (fixes #13368) (#13452) --- docs/rules/multiline-ternary.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/rules/multiline-ternary.md b/docs/rules/multiline-ternary.md index e9461a1ca08..0c38bf7d3f1 100644 --- a/docs/rules/multiline-ternary.md +++ b/docs/rules/multiline-ternary.md @@ -27,7 +27,7 @@ This rule has a string option: * `"always"` (default) enforces newlines between the operands of a ternary expression. * `"always-multiline"` enforces newlines between the operands of a ternary expression if the expression spans multiple lines. -* `"never"` disallows newlines between the operands of a ternary expression (enforcing that the entire ternary expression is on one line). +* `"never"` disallows newlines between the operands of a ternary expression. ### always @@ -134,6 +134,10 @@ Examples of **correct** code for this rule with the `"never"` option: foo > bar ? value1 : value2; foo > bar ? (baz > qux ? value1 : value2) : value3; + +foo > bar ? ( + baz > qux ? value1 : value2 +) : value3; ``` ## When Not To Use It From ed86b158041ac04e118eadf5d5b8767f6b38e526 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 3 Jul 2020 16:07:42 -0400 Subject: [PATCH 22/66] Build: changelog update for 7.4.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3474642e4be..7129c2bf442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +v7.4.0 - July 3, 2020 + +* [`f21bad2`](https://github.com/eslint/eslint/commit/f21bad2680406a2671b877f8dba47f4475d0cc64) Docs: fix description for `never` in multiline-ternary (fixes #13368) (#13452) (Milos Djermanovic) +* [`ada2c89`](https://github.com/eslint/eslint/commit/ada2c891298382f82dfabf37cacd59a1057b2bb7) Fix: support typescript generics in arrow-parens (fixes #12570) (#13451) (Milos Djermanovic) +* [`89ee01e`](https://github.com/eslint/eslint/commit/89ee01e083f1e02293bf8d1447f9b0fdb3cb9384) Fix: Revert config cloning (fixes #13447) (#13449) (薛定谔的猫) +* [`0a463db`](https://github.com/eslint/eslint/commit/0a463dbf7cc5a77d442879c9117204d4d38db972) Docs: fix no-multiple-empty-lines examples (fixes #13432) (#13433) (Milos Djermanovic) +* [`ff5317e`](https://github.com/eslint/eslint/commit/ff5317e93425f93cfdf808609551ee67b2032543) Update: Improve array-callback-return report message (#13395) (Philip (flip) Kromer) +* [`3f51930`](https://github.com/eslint/eslint/commit/3f51930eea7cddc921a9ee3cb0328c7b649c0f83) Fix: false positive new with member in no-extra-parens (fixes #12740) (#13375) (YeonJuan) +* [`825a5b9`](https://github.com/eslint/eslint/commit/825a5b98d3d84f6eb72b75f7d8519de763cc8898) Fix: Clarify documentation on implicit ignore behavior (fixes #12348) (#12600) (Scott Hardin) +* [`c139156`](https://github.com/eslint/eslint/commit/c1391566a5f765f25716527de7b5cdee16c0ce36) Sponsors: Sync README with website (ESLint Jenkins) +* [`0c17e9d`](https://github.com/eslint/eslint/commit/0c17e9d2ac307cc288eea6ed7971bd5a7d33321a) Sponsors: Sync README with website (ESLint Jenkins) +* [`c680387`](https://github.com/eslint/eslint/commit/c680387ba61f6dccf0390d24a85d871fa83e9fea) Sponsors: Sync README with website (ESLint Jenkins) +* [`bf3939b`](https://github.com/eslint/eslint/commit/bf3939bbd9a33d0eb96cebe6a53bf61c855f9ba6) Sponsors: Sync README with website (ESLint Jenkins) +* [`7baf02e`](https://github.com/eslint/eslint/commit/7baf02e983af909800261263f125cca901a5bd0f) Sponsors: Sync README with website (ESLint Jenkins) +* [`5c4c3fd`](https://github.com/eslint/eslint/commit/5c4c3fdfbda18a13223ad36f44283adbfee8c496) Sponsors: Sync README with website (ESLint Jenkins) +* [`53912aa`](https://github.com/eslint/eslint/commit/53912aab1856327b399cca26cbb2ba81fd01bfa2) Sponsors: Sync README with website (ESLint Jenkins) +* [`51e42ec`](https://github.com/eslint/eslint/commit/51e42eca3e87d8259815d736ffe81e604f184057) Update: Add option "ignoreGlobals" to camelcase rule (fixes #11716) (#12782) (David Gasperoni) +* [`0655f66`](https://github.com/eslint/eslint/commit/0655f66525d167ca1288167b79a77087cfc8fcf6) Update: improve report location in arrow-body-style (refs #12334) (#13424) (YeonJuan) +* [`d53d69a`](https://github.com/eslint/eslint/commit/d53d69af08cfe55f42e0a0ca725b1014dabccc21) Update: prefer-regex-literal detect regex literals (fixes #12840) (#12842) (Mathias Schreck) +* [`004adae`](https://github.com/eslint/eslint/commit/004adae3f959414f56e44e5884f6221e9dcda142) Update: rename id-blacklist to id-denylist (fixes #13407) (#13408) (Kai Cataldo) v7.3.1 - June 22, 2020 * [`de77c11`](https://github.com/eslint/eslint/commit/de77c11e7515f2097ff355ddc0d7b6db9c83c892) Fix: Replace Infinity with Number.MAX_SAFE_INTEGER (fixes #13427) (#13435) (Nicholas C. Zakas) From 30fa423d2808cb0a21835895808a7862d00d2adf Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 3 Jul 2020 16:07:42 -0400 Subject: [PATCH 23/66] 7.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 78a6ebeed62..f541bfe4df8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "7.3.1", + "version": "7.4.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From b55fd3b8c05a29a465a794a524b06c1a28cddf0c Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Sat, 4 Jul 2020 01:11:40 -0400 Subject: [PATCH 24/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b5a911b2c1..1f0f435bef4 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Shopify Salesforce Airbnb

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

+

My True Media Norgekasino Japanesecasino EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

## Technology Sponsors From 2ea7ee51a4e05ee76a6dae5954c3b6263b0970a3 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Tue, 7 Jul 2020 01:11:55 -0400 Subject: [PATCH 25/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f0f435bef4..4b5a911b2c1 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Shopify Salesforce Airbnb

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

My True Media Norgekasino Japanesecasino EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

+

My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

## Technology Sponsors From 10251bbaeba80ac15244f385fc42cf2f2a30e5d2 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Tue, 7 Jul 2020 22:23:30 +0200 Subject: [PATCH 26/66] Fix: add end location to reports in keyword-spacing (refs #12334) (#13461) --- lib/rules/keyword-spacing.js | 4 +- tests/lib/rules/keyword-spacing.js | 80 ++++++++++++++++++++++-------- 2 files changed, 62 insertions(+), 22 deletions(-) diff --git a/lib/rules/keyword-spacing.js b/lib/rules/keyword-spacing.js index 99979a32a5b..913cf4682f9 100644 --- a/lib/rules/keyword-spacing.js +++ b/lib/rules/keyword-spacing.js @@ -126,7 +126,7 @@ module.exports = { !sourceCode.isSpaceBetweenTokens(prevToken, token) ) { context.report({ - loc: token.loc.start, + loc: token.loc, messageId: "expectedBefore", data: token, fix(fixer) { @@ -178,7 +178,7 @@ module.exports = { !sourceCode.isSpaceBetweenTokens(token, nextToken) ) { context.report({ - loc: token.loc.start, + loc: token.loc, messageId: "expectedAfter", data: token, fix(fixer) { diff --git a/tests/lib/rules/keyword-spacing.js b/tests/lib/rules/keyword-spacing.js index b67ee52384b..64ee5a43474 100644 --- a/tests/lib/rules/keyword-spacing.js +++ b/tests/lib/rules/keyword-spacing.js @@ -1364,14 +1364,14 @@ ruleTester.run("keyword-spacing", rule, { code: "import *as a from \"foo\"", output: "import * as a from \"foo\"", parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: expectedBefore("as") - }, - { - code: "import* as a from\"foo\"", - output: "import*as a from\"foo\"", - options: [NEITHER], - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: unexpectedBefore("as") + errors: [{ + messageId: "expectedBefore", + data: { value: "as" }, + line: 1, + column: 9, + endLine: 1, + endColumn: 11 + }] }, { code: "import* as a from\"foo\"", @@ -1380,27 +1380,26 @@ ruleTester.run("keyword-spacing", rule, { parserOptions: { ecmaVersion: 6, sourceType: "module" }, errors: [{ messageId: "unexpectedBefore", + data: { value: "as" }, line: 1, column: 8, endLine: 1, endColumn: 9 - } - ] + }] }, { - code: "import *as a from\"foo\"", + code: "import* as a from\"foo\"", output: "import*as a from\"foo\"", options: [NEITHER], parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [ - { - messageId: "unexpectedAfter", - line: 1, - column: 7, - endLine: 1, - endColumn: 8 - } - ] + errors: [{ + messageId: "unexpectedBefore", + data: { value: "as" }, + line: 1, + column: 8, + endLine: 1, + endColumn: 11 + }] }, { code: "import*as a from\"foo\"", @@ -2510,6 +2509,47 @@ ruleTester.run("keyword-spacing", rule, { // import //---------------------------------------------------------------------- + { + code: "import* as a from \"foo\"", + output: "import * as a from \"foo\"", + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "expectedAfter", + data: { value: "import" }, + line: 1, + column: 1, + endLine: 1, + endColumn: 7 + }] + }, + { + code: "import *as a from\"foo\"", + output: "import*as a from\"foo\"", + options: [NEITHER], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "unexpectedAfter", + data: { value: "import" }, + line: 1, + column: 7, + endLine: 1, + endColumn: 8 + }] + }, + { + code: "import *as a from\"foo\"", + output: "import*as a from\"foo\"", + options: [NEITHER], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "unexpectedAfter", + data: { value: "import" }, + line: 1, + column: 7, + endLine: 1, + endColumn: 10 + }] + }, { code: "{}import{a} from \"foo\"", output: "{} import {a} from \"foo\"", From 095194c0fc0eb02aa69fde6b4280696e0e4de214 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Tue, 7 Jul 2020 22:23:58 +0200 Subject: [PATCH 27/66] Fix: add end location to reports in object-curly-newline (refs #12334) (#13460) --- lib/rules/object-curly-newline.js | 8 +++---- tests/lib/rules/object-curly-newline.js | 32 +++++++++++++++++++++---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/lib/rules/object-curly-newline.js b/lib/rules/object-curly-newline.js index b48b2526a0b..616d59851d6 100644 --- a/lib/rules/object-curly-newline.js +++ b/lib/rules/object-curly-newline.js @@ -224,7 +224,7 @@ module.exports = { context.report({ messageId: "expectedLinebreakAfterOpeningBrace", node, - loc: openBrace.loc.start, + loc: openBrace.loc, fix(fixer) { if (hasCommentsFirstToken) { return null; @@ -238,7 +238,7 @@ module.exports = { context.report({ messageId: "expectedLinebreakBeforeClosingBrace", node, - loc: closeBrace.loc.start, + loc: closeBrace.loc, fix(fixer) { if (hasCommentsLastToken) { return null; @@ -260,7 +260,7 @@ module.exports = { context.report({ messageId: "unexpectedLinebreakAfterOpeningBrace", node, - loc: openBrace.loc.start, + loc: openBrace.loc, fix(fixer) { if (hasCommentsFirstToken) { return null; @@ -280,7 +280,7 @@ module.exports = { context.report({ messageId: "unexpectedLinebreakBeforeClosingBrace", node, - loc: closeBrace.loc.start, + loc: closeBrace.loc, fix(fixer) { if (hasCommentsLastToken) { return null; diff --git a/tests/lib/rules/object-curly-newline.js b/tests/lib/rules/object-curly-newline.js index ad3b45218b3..86e95609920 100644 --- a/tests/lib/rules/object-curly-newline.js +++ b/tests/lib/rules/object-curly-newline.js @@ -607,8 +607,20 @@ ruleTester.run("object-curly-newline", rule, { ].join("\n"), options: ["always"], errors: [ - { line: 1, column: 9, messageId: "expectedLinebreakAfterOpeningBrace" }, - { line: 1, column: 14, messageId: "expectedLinebreakBeforeClosingBrace" } + { + line: 1, + column: 9, + endLine: 1, + endColumn: 10, + messageId: "expectedLinebreakAfterOpeningBrace" + }, + { + line: 1, + column: 14, + endLine: 1, + endColumn: 15, + messageId: "expectedLinebreakBeforeClosingBrace" + } ] }, { @@ -717,8 +729,20 @@ ruleTester.run("object-curly-newline", rule, { ].join("\n"), options: ["never"], errors: [ - { line: 1, column: 9, messageId: "unexpectedLinebreakAfterOpeningBrace" }, - { line: 3, column: 1, messageId: "unexpectedLinebreakBeforeClosingBrace" } + { + line: 1, + column: 9, + endLine: 1, + endColumn: 10, + messageId: "unexpectedLinebreakAfterOpeningBrace" + }, + { + line: 3, + column: 1, + endLine: 3, + endColumn: 2, + messageId: "unexpectedLinebreakBeforeClosingBrace" + } ] }, { From b77b4202bd1d5d1306f6f645e88d7a41a51715db Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Tue, 7 Jul 2020 22:24:33 +0200 Subject: [PATCH 28/66] Update: Improve report location for max-len (refs #12334) (#13458) --- lib/rules/max-len.js | 15 ++- tests/lib/rules/max-len.js | 220 +++++++++++++++++++++++++++---------- 2 files changed, 178 insertions(+), 57 deletions(-) diff --git a/lib/rules/max-len.js b/lib/rules/max-len.js index 995e0c52026..dd76760c505 100644 --- a/lib/rules/max-len.js +++ b/lib/rules/max-len.js @@ -383,11 +383,22 @@ module.exports = { return; } + const loc = { + start: { + line: lineNumber, + column: 0 + }, + end: { + line: lineNumber, + column: textToMeasure.length + } + }; + if (commentLengthApplies) { if (lineLength > maxCommentLength) { context.report({ node, - loc: { line: lineNumber, column: 0 }, + loc, messageId: "maxComment", data: { lineLength, @@ -398,7 +409,7 @@ module.exports = { } else if (lineLength > maxLength) { context.report({ node, - loc: { line: lineNumber, column: 0 }, + loc, messageId: "max", data: { lineLength, diff --git a/tests/lib/rules/max-len.js b/tests/lib/rules/max-len.js index f6296b636b9..557b5506f89 100644 --- a/tests/lib/rules/max-len.js +++ b/tests/lib/rules/max-len.js @@ -341,7 +341,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 86, maxLength: 80 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 30 } ] }, @@ -354,7 +356,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 24, maxLength: 10 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 25 } ] }, @@ -367,7 +371,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 22, maxLength: 15 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 14 } ] }, @@ -380,14 +386,18 @@ ruleTester.run("max-len", rule, { data: { lineLength: 22, maxLength: 15 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 14 }, { messageId: "max", data: { lineLength: 22, maxLength: 15 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 14 } ] }, @@ -400,7 +410,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 56, maxLength: 20 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 57 } ] }, @@ -415,7 +427,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 54, maxLength: 20 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 55 } ] }, @@ -428,7 +442,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 30, maxLength: 10 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 31 } ] }, @@ -441,7 +457,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 62, maxLength: 40 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 63 } ] }, @@ -454,7 +472,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 57, maxLength: 40 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 58 } ] }, { @@ -466,7 +486,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 53, maxLength: 40 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 54 } ] }, { @@ -478,7 +500,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 49, maxCommentLength: 20 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 50 } ] }, { @@ -490,7 +514,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 119, maxCommentLength: 80 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 120 } ] }, { @@ -502,7 +528,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 49, maxLength: 20 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 50 } ] }, { @@ -514,7 +542,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 73, maxLength: 40 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 74 } ] }, @@ -531,7 +561,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 29, maxCommentLength: 28 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 30 } ] }, { @@ -545,7 +577,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 33, maxCommentLength: 32 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 34 } ] }, { @@ -560,14 +594,18 @@ ruleTester.run("max-len", rule, { data: { lineLength: 29, maxCommentLength: 28 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 30 }, { messageId: "maxComment", data: { lineLength: 32, maxCommentLength: 28 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 33 } ] }, { @@ -582,14 +620,18 @@ ruleTester.run("max-len", rule, { data: { lineLength: 33, maxCommentLength: 32 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 34 }, { messageId: "maxComment", data: { lineLength: 36, maxCommentLength: 32 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 37 } ] }, { @@ -604,14 +646,18 @@ ruleTester.run("max-len", rule, { data: { lineLength: 40, maxLength: 39 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 41 }, { messageId: "maxComment", data: { lineLength: 36, maxCommentLength: 35 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 37 } ] }, { @@ -626,14 +672,18 @@ ruleTester.run("max-len", rule, { data: { lineLength: 33, maxCommentLength: 32 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 34 }, { messageId: "max", data: { lineLength: 43, maxLength: 42 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 44 } ] }, @@ -649,7 +699,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 51, maxLength: 20 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 52 } ] }, @@ -664,7 +716,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 39, maxLength: 29 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 40 } ] }, @@ -677,7 +731,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 45, maxLength: 29 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 46 } ] }, @@ -690,7 +746,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 57, maxLength: 29 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 58 } ] }, @@ -703,7 +761,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 39, maxLength: 29 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 40 } ] }, @@ -717,7 +777,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 39, maxLength: 29 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 40 } ] }, @@ -731,14 +793,18 @@ ruleTester.run("max-len", rule, { data: { lineLength: 37, maxLength: 29 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 38 }, { messageId: "max", data: { lineLength: 44, maxLength: 29 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 45 } ] }, @@ -752,7 +818,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 58, maxLength: 29 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 59 } ] }, @@ -767,7 +835,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 12, maxLength: 10 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 21 } ] }, @@ -781,7 +851,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 1, maxLength: 0 }, type: "Program", line: 1, - column: 1 + column: 1, + endLine: 1, + endColumn: 2 } ] }, @@ -799,7 +871,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 38, maxCommentLength: 37 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 39 } ] }, @@ -815,7 +889,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 44, maxCommentLength: 40 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 39 } ] }, @@ -831,7 +907,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 38, maxLength: 15 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 39 } ] }, @@ -847,7 +925,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 38, maxLength: 37 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 39 } ] }, @@ -863,7 +943,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 38, maxLength: 37 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 39 } ] }, @@ -879,7 +961,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 50, maxLength: 49 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 51 } ] }, @@ -896,7 +980,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 44, maxLength: 37 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 45 } ] }, @@ -912,7 +998,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 57, maxLength: 56 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 58 } ] }, @@ -928,7 +1016,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 57, maxLength: 56 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 58 } ] }, @@ -944,7 +1034,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 56, maxLength: 55 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 57 } ] }, @@ -961,7 +1053,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 51, maxLength: 30 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 52 } ] }, @@ -978,7 +1072,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 80, maxLength: 79 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 81 } ] }, @@ -994,7 +1090,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 87, maxLength: 85 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 88 } ] }, @@ -1011,7 +1109,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 87, maxLength: 37 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 88 } ] }, @@ -1028,7 +1128,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 119, maxLength: 37 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 120 } ] }, @@ -1045,7 +1147,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 55, maxLength: 37 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 56 } ] }, @@ -1062,7 +1166,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 55, maxLength: 37 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 56 } ] }, @@ -1079,7 +1185,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 15, maxLength: 14 }, type: "Program", line: 2, - column: 1 + column: 1, + endLine: 2, + endColumn: 16 } ] }, @@ -1096,7 +1204,9 @@ ruleTester.run("max-len", rule, { data: { lineLength: 31, maxLength: 30 }, type: "Program", line: 3, - column: 1 + column: 1, + endLine: 3, + endColumn: 32 } ] } From 1050ee78a95da9484ff333dc1c74dac64c05da6f Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Tue, 7 Jul 2020 22:25:13 +0200 Subject: [PATCH 29/66] Update: Improve report location for no-unneeded-ternary (refs #12334) (#13456) --- lib/rules/no-unneeded-ternary.js | 2 - tests/lib/rules/no-unneeded-ternary.js | 116 ++++++++++++++++++------- 2 files changed, 87 insertions(+), 31 deletions(-) diff --git a/lib/rules/no-unneeded-ternary.js b/lib/rules/no-unneeded-ternary.js index 0fefc42b909..06c615f3824 100644 --- a/lib/rules/no-unneeded-ternary.js +++ b/lib/rules/no-unneeded-ternary.js @@ -122,7 +122,6 @@ module.exports = { if (isBooleanLiteral(node.alternate) && isBooleanLiteral(node.consequent)) { context.report({ node, - loc: node.consequent.loc.start, messageId: "unnecessaryConditionalExpression", fix(fixer) { if (node.consequent.value === node.alternate.value) { @@ -144,7 +143,6 @@ module.exports = { } else if (!defaultAssignment && matchesDefaultAssignment(node)) { context.report({ node, - loc: node.consequent.loc.start, messageId: "unnecessaryConditionalAssignment", fix: fixer => { const shouldParenthesizeAlternate = diff --git a/tests/lib/rules/no-unneeded-ternary.js b/tests/lib/rules/no-unneeded-ternary.js index 82479a94cb3..7ad11d2b2e5 100644 --- a/tests/lib/rules/no-unneeded-ternary.js +++ b/tests/lib/rules/no-unneeded-ternary.js @@ -54,7 +54,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 19 + column: 9, + endLine: 1, + endColumn: 31 }] }, { @@ -64,7 +66,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 18 + column: 9, + endLine: 1, + endColumn: 30 }] }, { @@ -74,7 +78,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 25 }] }, { @@ -84,7 +90,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 19 + column: 9, + endLine: 1, + endColumn: 31 }] }, { @@ -94,7 +102,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 18 + column: 9, + endLine: 1, + endColumn: 30 }] }, { @@ -104,7 +114,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 17 + column: 9, + endLine: 1, + endColumn: 29 }] }, { @@ -114,7 +126,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 18 + column: 9, + endLine: 1, + endColumn: 30 }] }, { @@ -124,7 +138,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 21 + column: 9, + endLine: 1, + endColumn: 33 }] }, { @@ -134,7 +150,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 28 + column: 9, + endLine: 1, + endColumn: 40 }] }, { @@ -144,7 +162,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 15 + column: 9, + endLine: 1, + endColumn: 28 }] }, { @@ -154,7 +174,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 17 + column: 9, + endLine: 1, + endColumn: 30 }] }, { @@ -164,7 +186,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 28 + column: 9, + endLine: 1, + endColumn: 40 }] }, { @@ -174,7 +198,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalExpression", type: "ConditionalExpression", line: 1, - column: 16 + column: 9, + endLine: 1, + endColumn: 28 }] }, { @@ -193,7 +219,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 4, - column: 38 + column: 30, + endLine: 4, + endColumn: 78 }] }, { @@ -204,7 +232,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 7 + column: 1, + endLine: 1, + endColumn: 30 }] }, { @@ -216,7 +246,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 24 + column: 18, + endLine: 1, + endColumn: 39 }] }, { @@ -227,7 +259,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 15 + column: 9, + endLine: 1, + endColumn: 25 }] }, { @@ -238,7 +272,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 24 + column: 9, + endLine: 1, + endColumn: 66 }] }, { @@ -250,7 +286,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 23 }] }, { @@ -262,7 +300,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 22 }] }, { @@ -274,7 +314,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 25 }] }, { @@ -286,7 +328,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 24 }] }, { @@ -298,7 +342,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 27 }] }, { @@ -310,7 +356,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 18 }] }, { @@ -322,7 +370,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 13 + column: 9, + endLine: 1, + endColumn: 23 }] }, { @@ -333,7 +383,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 7 + column: 3, + endLine: 1, + endColumn: 12 }] }, { @@ -344,7 +396,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 5 + column: 1, + endLine: 1, + endColumn: 10 }] }, { @@ -355,7 +409,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 15 + column: 9, + endLine: 1, + endColumn: 24 }] }, { @@ -367,7 +423,9 @@ ruleTester.run("no-unneeded-ternary", rule, { messageId: "unnecessaryConditionalAssignment", type: "ConditionalExpression", line: 1, - column: 15 + column: 9, + endLine: 1, + endColumn: 27 }] } ] From 0af1d2828d27885483737867653ba1659af72005 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Tue, 7 Jul 2020 22:26:16 +0200 Subject: [PATCH 30/66] Update: add allowSeparatedGroups option to sort-imports (fixes #12951) (#13455) --- docs/rules/sort-imports.md | 51 +++++++++- lib/rules/sort-imports.js | 28 ++++++ tests/lib/rules/sort-imports.js | 159 +++++++++++++++++++++++++++++++- 3 files changed, 236 insertions(+), 2 deletions(-) diff --git a/docs/rules/sort-imports.md b/docs/rules/sort-imports.md index f6fd04f0683..637c9fcf3f3 100644 --- a/docs/rules/sort-imports.md +++ b/docs/rules/sort-imports.md @@ -41,6 +41,7 @@ This rule accepts an object with its properties as * `all` = import all members provided by exported bindings. * `multiple` = import multiple members. * `single` = import single member. +* `allowSeparatedGroups` (default: `false`) Default option settings are: @@ -50,7 +51,8 @@ Default option settings are: "ignoreCase": false, "ignoreDeclarationSort": false, "ignoreMemberSort": false, - "memberSyntaxSortOrder": ["none", "all", "multiple", "single"] + "memberSyntaxSortOrder": ["none", "all", "multiple", "single"], + "allowSeparatedGroups": false }] } ``` @@ -226,6 +228,53 @@ import {a, b} from 'foo.js'; Default is `["none", "all", "multiple", "single"]`. +### `allowSeparatedGroups` + +When `true` the rule checks the sorting of import declaration statements only for those that appear on consecutive lines. + +In other words, a blank line or a comment line or line with any other statement after an import declaration statement will reset the sorting of import declaration statements. + +Examples of **incorrect** code for this rule with the `{ "allowSeparatedGroups": true }` option: + +```js +/*eslint sort-imports: ["error", { "allowSeparatedGroups": true }]*/ + +import b from 'foo.js'; +import c from 'bar.js'; +import a from 'baz.js'; +``` + +Examples of **correct** code for this rule with the `{ "allowSeparatedGroups": true }` option: + +```js +/*eslint sort-imports: ["error", { "allowSeparatedGroups": true }]*/ + +import b from 'foo.js'; +import c from 'bar.js'; + +import a from 'baz.js'; +``` + +```js +/*eslint sort-imports: ["error", { "allowSeparatedGroups": true }]*/ + +import b from 'foo.js'; +import c from 'bar.js'; +// comment +import a from 'baz.js'; +``` + +```js +/*eslint sort-imports: ["error", { "allowSeparatedGroups": true }]*/ + +import b from 'foo.js'; +import c from 'bar.js'; +quux(); +import a from 'baz.js'; +``` + +Default is `false`. + ## When Not To Use It This rule is a formatting preference and not following it won't negatively affect the quality of your code. If alphabetizing imports isn't a part of your coding standards, then you can leave this rule disabled. diff --git a/lib/rules/sort-imports.js b/lib/rules/sort-imports.js index 65ad9a18a93..4c3ddec7669 100644 --- a/lib/rules/sort-imports.js +++ b/lib/rules/sort-imports.js @@ -44,6 +44,10 @@ module.exports = { ignoreMemberSort: { type: "boolean", default: false + }, + allowSeparatedGroups: { + type: "boolean", + default: false } }, additionalProperties: false @@ -66,6 +70,7 @@ module.exports = { ignoreDeclarationSort = configuration.ignoreDeclarationSort || false, ignoreMemberSort = configuration.ignoreMemberSort || false, memberSyntaxSortOrder = configuration.memberSyntaxSortOrder || ["none", "all", "multiple", "single"], + allowSeparatedGroups = configuration.allowSeparatedGroups || false, sourceCode = context.getSourceCode(); let previousDeclaration = null; @@ -115,9 +120,32 @@ module.exports = { } + /** + * Calculates number of lines between two nodes. It is assumed that the given `left` node appears before + * the given `right` node in the source code. Lines are counted from the end of the `left` node till the + * start of the `right` node. If the given nodes are on the same line, it returns `0`, same as if they were + * on two consecutive lines. + * @param {ASTNode} left node that appears before the given `right` node. + * @param {ASTNode} right node that appears after the given `left` node. + * @returns {number} number of lines between nodes. + */ + function getNumberOfLinesBetween(left, right) { + return Math.max(right.loc.start.line - left.loc.end.line - 1, 0); + } + return { ImportDeclaration(node) { if (!ignoreDeclarationSort) { + if ( + previousDeclaration && + allowSeparatedGroups && + getNumberOfLinesBetween(previousDeclaration, node) > 0 + ) { + + // reset declaration sort + previousDeclaration = null; + } + if (previousDeclaration) { const currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(node), previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex(previousDeclaration); diff --git a/tests/lib/rules/sort-imports.js b/tests/lib/rules/sort-imports.js index 51f3ee3a804..b5da5018919 100644 --- a/tests/lib/rules/sort-imports.js +++ b/tests/lib/rules/sort-imports.js @@ -101,7 +101,45 @@ ruleTester.run("sort-imports", rule, { }, // https://github.com/eslint/eslint/issues/5305 - "import React, {Component} from 'react';" + "import React, {Component} from 'react';", + + // allowSeparatedGroups + { + code: "import b from 'b';\n\nimport a from 'a';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import a from 'a';\n\nimport 'b';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import { b } from 'b';\n\n\nimport { a } from 'a';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import b from 'b';\n// comment\nimport a from 'a';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import b from 'b';\nfoo();\nimport a from 'a';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import { b } from 'b';/*\n comment \n*/import { a } from 'a';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import b from\n'b';\n\nimport\n a from 'a';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import c from 'c';\n\nimport a from 'a';\nimport b from 'b';", + options: [{ allowSeparatedGroups: true }] + }, + { + code: "import c from 'c';\n\nimport b from 'b';\n\nimport a from 'a';", + options: [{ allowSeparatedGroups: true }] + } ], invalid: [ { @@ -287,6 +325,125 @@ ruleTester.run("sort-imports", rule, { data: { memberName: "qux" }, type: "ImportSpecifier" }] + }, + + // allowSeparatedGroups + { + code: "import b from 'b';\nimport a from 'a';", + output: null, + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import b from 'b';\nimport a from 'a';", + output: null, + options: [{}], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import b from 'b';\nimport a from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import b from 'b';import a from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import b from 'b'; /* comment */ import a from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import b from 'b'; // comment\nimport a from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import b from 'b'; // comment 1\n/* comment 2 */import a from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import { b } from 'b'; /* comment line 1 \n comment line 2 */ import { a } from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import b\nfrom 'b'; import a\nfrom 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import { b } from \n'b'; /* comment */ import\n { a } from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import { b } from \n'b';\nimport\n { a } from 'a';", + output: null, + options: [{ allowSeparatedGroups: false }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration" + }] + }, + { + code: "import c from 'c';\n\nimport b from 'b';\nimport a from 'a';", + output: null, + options: [{ allowSeparatedGroups: true }], + errors: [{ + messageId: "sortImportsAlphabetically", + type: "ImportDeclaration", + line: 4 + }] + }, + { + code: "import b from 'b';\n\nimport { c, a } from 'c';", + output: "import b from 'b';\n\nimport { a, c } from 'c';", + options: [{ allowSeparatedGroups: true }], + errors: [{ + messageId: "sortMembersAlphabetically", + type: "ImportSpecifier" + }] } ] }); From e951457b7aaa1b12b135588d36e3f4db4d7b8463 Mon Sep 17 00:00:00 2001 From: Piper Date: Thu, 9 Jul 2020 12:57:13 -0700 Subject: [PATCH 31/66] Docs: fix wording in configuring.md (#13469) * Fix wording in configuring.md * Update docs/user-guide/configuring.md Co-authored-by: Nicholas C. Zakas Co-authored-by: Nicholas C. Zakas --- docs/user-guide/configuring.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md index 373a0fb05b4..50a0341fd06 100644 --- a/docs/user-guide/configuring.md +++ b/docs/user-guide/configuring.md @@ -91,7 +91,7 @@ To specify processors in a configuration file, use the `processor` key with the } ``` -To specify processors for a specific kind of files, use the combination of the `overrides` key and the `processor` key. For example, the following uses the processor `a-plugin/markdown` for `*.md` files. +To specify processors for specific kinds of files, use the combination of the `overrides` key and the `processor` key. For example, the following uses the processor `a-plugin/markdown` for `*.md` files. ```json { From 748734fdd497fbf61f3a616ff4a09169135b9396 Mon Sep 17 00:00:00 2001 From: odidev Date: Fri, 10 Jul 2020 04:28:37 +0530 Subject: [PATCH 32/66] Upgrade: Updated puppeteer version to v4.0.0 (#13444) Added architecture check to set CHROME_BIN path Signed-off-by: odidev --- karma.conf.js | 9 ++++++++- package.json | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index dfc6bab4899..974c8972136 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,6 +1,13 @@ "use strict"; +const os = require("os"); -process.env.CHROME_BIN = require("puppeteer").executablePath(); +if (os.arch() === "arm64") { + + // For arm64 architecture, install chromium-browser using "apt-get install chromium-browser" + process.env.CHROME_BIN = "/usr/bin/chromium-browser"; +} else { + process.env.CHROME_BIN = require("puppeteer").executablePath(); +} module.exports = function(config) { config.set({ diff --git a/package.json b/package.json index f541bfe4df8..ebb81cd488f 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "npm-license": "^0.3.3", "nyc": "^15.0.1", "proxyquire": "^2.0.1", - "puppeteer": "^2.1.1", + "puppeteer": "^4.0.0", "recast": "^0.19.0", "regenerator-runtime": "^0.13.2", "shelljs": "^0.8.2", From a96bc5ec06f3a48bfe458bccd68d4d3b2a280ed9 Mon Sep 17 00:00:00 2001 From: Anix Date: Sat, 11 Jul 2020 21:35:57 +0530 Subject: [PATCH 33/66] Fix: arrow-body-style fixer for `in` wrap (fixes #11849) (#13228) * Fix: add parens when in operator (fixes #11849) * Chore: some more tests * Chore: update jsdoc comments Co-authored-by: YeonJuan * Update: add paren only for ForStatement * Update: fixed adding extra parens * Update: minified traversing using visitor query * Chore: isInOp to false while existing * Update: fixed the logic * Update: logic * Update: checking inside of for loop * Chore: updated is for in check Co-authored-by: YeonJuan --- lib/rules/arrow-body-style.js | 47 +++++- tests/lib/rules/arrow-body-style.js | 242 +++++++++++++++++++++++++++- 2 files changed, 279 insertions(+), 10 deletions(-) diff --git a/lib/rules/arrow-body-style.js b/lib/rules/arrow-body-style.js index 5954cf4a18d..7b318ea8b3a 100644 --- a/lib/rules/arrow-body-style.js +++ b/lib/rules/arrow-body-style.js @@ -75,6 +75,7 @@ module.exports = { const never = options[0] === "never"; const requireReturnForObjectLiteral = options[1] && options[1].requireReturnForObjectLiteral; const sourceCode = context.getSourceCode(); + let funcInfo = null; /** * Checks whether the given node has ASI problem or not. @@ -99,6 +100,21 @@ module.exports = { return sourceCode.getTokenAfter(node); } + /** + * Check whether the node is inside of a for loop's init + * @param {ASTNode} node node is inside for loop + * @returns {boolean} `true` if the node is inside of a for loop, else `false` + */ + function isInsideForLoopInitializer(node) { + if (node && node.parent) { + if (node.parent.type === "ForStatement" && node.parent.init === node) { + return true; + } + return isInsideForLoopInitializer(node.parent); + } + return false; + } + /** * Determines whether a arrow function body needs braces * @param {ASTNode} node The arrow function node. @@ -178,11 +194,13 @@ module.exports = { * If the first token of the reutrn value is `{` or the return value is a sequence expression, * enclose the return value by parentheses to avoid syntax error. */ - if (astUtils.isOpeningBraceToken(firstValueToken) || blockBody[0].argument.type === "SequenceExpression") { - fixes.push( - fixer.insertTextBefore(firstValueToken, "("), - fixer.insertTextAfter(lastValueToken, ")") - ); + if (astUtils.isOpeningBraceToken(firstValueToken) || blockBody[0].argument.type === "SequenceExpression" || (funcInfo.hasInOperator && isInsideForLoopInitializer(node))) { + if (!astUtils.isParenthesised(sourceCode, blockBody[0].argument)) { + fixes.push( + fixer.insertTextBefore(firstValueToken, "("), + fixer.insertTextAfter(lastValueToken, ")") + ); + } } /* @@ -245,7 +263,24 @@ module.exports = { } return { - "ArrowFunctionExpression:exit": validate + "BinaryExpression[operator='in']"() { + let info = funcInfo; + + while (info) { + info.hasInOperator = true; + info = info.upper; + } + }, + ArrowFunctionExpression() { + funcInfo = { + upper: funcInfo, + hasInOperator: false + }; + }, + "ArrowFunctionExpression:exit"(node) { + validate(node); + funcInfo = funcInfo.upper; + } }; } }; diff --git a/tests/lib/rules/arrow-body-style.js b/tests/lib/rules/arrow-body-style.js index c624a847c6a..7a8de4fe5ef 100644 --- a/tests/lib/rules/arrow-body-style.js +++ b/tests/lib/rules/arrow-body-style.js @@ -45,6 +45,239 @@ ruleTester.run("arrow-body-style", rule, { { code: "var foo = () => { return { bar: 0 }; };", options: ["as-needed", { requireReturnForObjectLiteral: true }] } ], invalid: [ + { + code: "for (var foo = () => { return a in b ? bar : () => {} } ;;);", + output: "for (var foo = () => (a in b ? bar : () => {}) ;;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 22, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "a in b; for (var f = () => { return c };;);", + output: "a in b; for (var f = () => c;;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 28, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (a = b => { return c in d ? e : f } ;;);", + output: "for (a = b => (c in d ? e : f) ;;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 15, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (var f = () => { return a };;);", + output: "for (var f = () => a;;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 20, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (var f;f = () => { return a };);", + output: "for (var f;f = () => a;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 22, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (var f = () => { return a in c };;);", + output: "for (var f = () => (a in c);;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 20, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (var f;f = () => { return a in c };);", + output: "for (var f;f = () => a in c;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 22, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (;;){var f = () => { return a in c }}", + output: "for (;;){var f = () => a in c}", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 24, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (a = b => { return c = d in e } ;;);", + output: "for (a = b => (c = d in e) ;;);", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 15, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (var a;;a = b => { return c = d in e } );", + output: "for (var a;;a = b => c = d in e );", + options: ["as-needed"], + errors: [ + { + line: 1, + column: 22, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (let a = (b, c, d) => { return vb && c in d; }; ;);", + output: "for (let a = (b, c, d) => (vb && c in d); ;);", + errors: [ + { + line: 1, + column: 27, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (let a = (b, c, d) => { return v in b && c in d; }; ;);", + output: "for (let a = (b, c, d) => (v in b && c in d); ;);", + errors: [ + { + line: 1, + column: 27, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "function foo(){ for (let a = (b, c, d) => { return v in b && c in d; }; ;); }", + output: "function foo(){ for (let a = (b, c, d) => (v in b && c in d); ;); }", + errors: [ + { + line: 1, + column: 43, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for ( a = (b, c, d) => { return v in b && c in d; }; ;);", + output: "for ( a = (b, c, d) => (v in b && c in d); ;);", + errors: [ + { + line: 1, + column: 24, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for ( a = (b) => { return (c in d) }; ;);", + output: "for ( a = (b) => (c in d); ;);", + errors: [ + { + line: 1, + column: 18, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (let a = (b, c, d) => { return vb in dd ; }; ;);", + output: "for (let a = (b, c, d) => (vb in dd ); ;);", + errors: [ + { + line: 1, + column: 27, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "for (let a = (b, c, d) => { return vb in c in dd ; }; ;);", + output: "for (let a = (b, c, d) => (vb in c in dd ); ;);", + errors: [ + { + line: 1, + column: 27, + messageId: "unexpectedSingleBlock" + } + ] + }, + { + code: "do{let a = () => {return f in ff}}while(true){}", + output: "do{let a = () => f in ff}while(true){}", + errors: [{ + line: 1, + column: 18, + messageId: "unexpectedSingleBlock" + }] + }, + { + code: "do{for (let a = (b, c, d) => { return vb in c in dd ; }; ;);}while(true){}", + output: "do{for (let a = (b, c, d) => (vb in c in dd ); ;);}while(true){}", + errors: [{ + line: 1, + column: 30, + messageId: "unexpectedSingleBlock" + }] + }, + { + code: "scores.map(score => { return x in +(score / maxScore).toFixed(2)});", + output: "scores.map(score => x in +(score / maxScore).toFixed(2));", + errors: [{ + line: 1, + column: 21, + messageId: "unexpectedSingleBlock" + }] + }, + { + code: "const fn = (a, b) => { return a + x in Number(b) };", + output: "const fn = (a, b) => a + x in Number(b);", + errors: [{ + line: 1, + column: 22, + messageId: "unexpectedSingleBlock" + }] + }, { code: "var foo = () => 0", output: "var foo = () => {return 0}", @@ -370,8 +603,8 @@ ruleTester.run("arrow-body-style", rule, { // Not fixed; fixing would cause ASI issues. code: - "var foo = () => { return bar }\n" + - "[1, 2, 3].map(foo)", + "var foo = () => { return bar }\n" + + "[1, 2, 3].map(foo)", output: null, options: ["never"], errors: [ @@ -380,10 +613,11 @@ ruleTester.run("arrow-body-style", rule, { }, { + // Not fixed; fixing would cause ASI issues. code: - "var foo = () => { return bar }\n" + - "(1).toString();", + "var foo = () => { return bar }\n" + + "(1).toString();", output: null, options: ["never"], errors: [ From f2e68ec1d6cee6299e8a5cdf76c522c11d3008dd Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 11 Jul 2020 23:34:07 +0200 Subject: [PATCH 34/66] Build: update webpack resolve.mainFields to match website config (#13457) --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 29d60cb4d27..2b56290b346 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -43,7 +43,7 @@ module.exports = { ] }, resolve: { - mainFields: ["main", "module"] + mainFields: ["browser", "main", "module"] }, stats: "errors-only" }; From c8f9c8210cf4b9da8f07922093d7b219abad9f10 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 11 Jul 2020 23:41:43 +0200 Subject: [PATCH 35/66] Update: Improve report location no-irregular-whitespace (refs #12334) (#13462) --- lib/rules/no-irregular-whitespace.js | 34 ++- tests/lib/rules/no-irregular-whitespace.js | 329 +++++++++++++++++++++ 2 files changed, 351 insertions(+), 12 deletions(-) diff --git a/lib/rules/no-irregular-whitespace.js b/lib/rules/no-irregular-whitespace.js index 21842331f21..0bf69b128e6 100644 --- a/lib/rules/no-irregular-whitespace.js +++ b/lib/rules/no-irregular-whitespace.js @@ -91,7 +91,7 @@ module.exports = { const locStart = node.loc.start; const locEnd = node.loc.end; - errors = errors.filter(({ loc: errorLoc }) => { + errors = errors.filter(({ loc: { start: errorLoc } }) => { if (errorLoc.line >= locStart.line && errorLoc.line <= locEnd.line) { if (errorLoc.column >= locStart.column && (errorLoc.column <= locEnd.column || errorLoc.line < locEnd.line)) { return false; @@ -160,15 +160,19 @@ module.exports = { let match; while ((match = IRREGULAR_WHITESPACE.exec(sourceLine)) !== null) { - const location = { - line: lineNumber, - column: match.index - }; - errors.push({ node, messageId: "noIrregularWhitespace", - loc: location + loc: { + start: { + line: lineNumber, + column: match.index + }, + end: { + line: lineNumber, + column: match.index + match[0].length + } + } }); } }); @@ -189,16 +193,22 @@ module.exports = { while ((match = IRREGULAR_LINE_TERMINATORS.exec(source)) !== null) { const lineIndex = linebreaks.indexOf(match[0], lastLineIndex + 1) || 0; - const location = { - line: lineIndex + 1, - column: sourceLines[lineIndex].length - }; errors.push({ node, messageId: "noIrregularWhitespace", - loc: location + loc: { + start: { + line: lineIndex + 1, + column: sourceLines[lineIndex].length + }, + end: { + line: lineIndex + 2, + column: 0 + } + } }); + lastLineIndex = lineIndex; } } diff --git a/tests/lib/rules/no-irregular-whitespace.js b/tests/lib/rules/no-irregular-whitespace.js index 7852d5e5c27..4055d360150 100644 --- a/tests/lib/rules/no-irregular-whitespace.js +++ b/tests/lib/rules/no-irregular-whitespace.js @@ -540,6 +540,335 @@ ruleTester.run("no-irregular-whitespace", rule, { column: 14 } ] + }, + + // full location tests + { + code: "var foo = \u000B bar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 1, + endColumn: 12 + } + ] + }, + { + code: "var foo =\u000Bbar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 10, + endLine: 1, + endColumn: 11 + } + ] + }, + { + code: "var foo = \u000B\u000B bar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 1, + endColumn: 13 + } + ] + }, + { + code: "var foo = \u000B\u000C bar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 1, + endColumn: 13 + } + ] + }, + { + code: "var foo = \u000B \u000B bar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 1, + endColumn: 12 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 13, + endLine: 1, + endColumn: 14 + } + ] + }, + { + code: "var foo = \u000Bbar\u000B;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 1, + endColumn: 12 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 15, + endLine: 1, + endColumn: 16 + } + ] + }, + { + code: "\u000B", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 1, + endLine: 1, + endColumn: 2 + } + ] + }, + { + code: "\u00A0\u2002\u2003", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 1, + endLine: 1, + endColumn: 4 + } + ] + }, + { + code: "var foo = \u000B\nbar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 1, + endColumn: 12 + } + ] + }, + { + code: "var foo =\u000B\n\u000Bbar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 10, + endLine: 1, + endColumn: 11 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 2, + column: 1, + endLine: 2, + endColumn: 2 + } + ] + }, + { + code: "var foo = \u000C\u000B\n\u000C\u000B\u000Cbar\n;\u000B\u000C\n", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 1, + endColumn: 13 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 2, + column: 1, + endLine: 2, + endColumn: 4 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 3, + column: 2, + endLine: 3, + endColumn: 4 + } + ] + }, + { + code: "var foo = \u2028bar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 11, + endLine: 2, + endColumn: 1 + } + ] + }, + { + code: "var foo =\u2029 bar;", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 10, + endLine: 2, + endColumn: 1 + } + ] + }, + { + code: "var foo = bar;\u2028", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 15, + endLine: 2, + endColumn: 1 + } + ] + }, + { + code: "\u2029", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 1, + endLine: 2, + endColumn: 1 + } + ] + }, + { + code: "foo\u2028\u2028", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 4, + endLine: 2, + endColumn: 1 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 2, + column: 1, + endLine: 3, + endColumn: 1 + } + ] + }, + { + code: "foo\u2029\u2028", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 4, + endLine: 2, + endColumn: 1 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 2, + column: 1, + endLine: 3, + endColumn: 1 + } + ] + }, + { + code: "foo\u2028\n\u2028", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 4, + endLine: 2, + endColumn: 1 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 3, + column: 1, + endLine: 4, + endColumn: 1 + } + ] + }, + { + code: "foo\u000B\u2028\u000B", + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 4, + endLine: 1, + endColumn: 5 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 5, + endLine: 2, + endColumn: 1 + }, + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 2, + column: 1, + endLine: 2, + endColumn: 2 + } + ] } ] }); From 61097fe5cc275d414a0c8e19b31c6060cb5568b7 Mon Sep 17 00:00:00 2001 From: Brandon Mills Date: Mon, 13 Jul 2020 18:09:41 -0400 Subject: [PATCH 36/66] Docs: Update int rule level to string (#13483) --- 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 17058b4058a..12864856195 100644 --- a/docs/developer-guide/working-with-rules.md +++ b/docs/developer-guide/working-with-rules.md @@ -733,5 +733,5 @@ The thing that makes ESLint different from other linters is the ability to defin Runtime rules are written in the same format as all other rules. Create your rule as you would any other and then follow these steps: 1. Place all of your runtime rules in the same directory (e.g., `eslint_rules`). -2. Create a [configuration file](../user-guide/configuring.md) and specify your rule ID error level under the `rules` key. Your rule will not run unless it has a value of `1` or `2` in the configuration file. +2. Create a [configuration file](../user-guide/configuring.md) and specify your rule ID error level under the `rules` key. Your rule will not run unless it has a value of `"warn"` or `"error"` in the configuration file. 3. Run the [command line interface](../user-guide/command-line-interface.md) using the `--rulesdir` option to specify the location of your runtime rules. From e14a645aa495558081490f990ba221e21aa6b27c Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Tue, 14 Jul 2020 00:22:54 +0200 Subject: [PATCH 37/66] Chore: use espree.latestEcmaVersion in fuzzer (#13484) --- tools/code-sample-minimizer.js | 2 +- tools/eslint-fuzzer.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/code-sample-minimizer.js b/tools/code-sample-minimizer.js index 512b692fe74..c68f4582050 100644 --- a/tools/code-sample-minimizer.js +++ b/tools/code-sample-minimizer.js @@ -52,7 +52,7 @@ function reduceBadExampleSize({ comment: true, eslintVisitorKeys: true, eslintScopeManager: true, - ecmaVersion: 2018, + ecmaVersion: espree.latestEcmaVersion, sourceType: "script" }) }, diff --git a/tools/eslint-fuzzer.js b/tools/eslint-fuzzer.js index ff77f44f6e1..4104b8516b2 100644 --- a/tools/eslint-fuzzer.js +++ b/tools/eslint-fuzzer.js @@ -129,7 +129,10 @@ function fuzz(options) { const text = codeGenerator({ sourceType }); const config = { rules: lodash.mapValues(ruleConfigs, lodash.sample), - parserOptions: { sourceType, ecmaVersion: 2020 } + parserOptions: { + sourceType, + ecmaVersion: espree.latestEcmaVersion + } }; let autofixResult; From f4d7b9e1a599346b2f21ff9de003b311b51411e6 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Tue, 14 Jul 2020 00:19:02 -0400 Subject: [PATCH 38/66] Update: deprecate id-blacklist rule (#13465) * Fix: deprecates id-blacklist (and documents deprecation) * Update docs/rules/id-blacklist.md Co-authored-by: Milos Djermanovic * copies contents of id-denylist to id-blacklist verbatim * deprecates id-blacklist and updates docs url * adds replacedBy metadata Co-authored-by: Milos Djermanovic --- docs/rules/id-blacklist.md | 3 + lib/rules/id-blacklist.js | 233 +++++++++++++++++++++++++++++++++++++ lib/rules/index.js | 4 +- tools/rule-types.json | 1 + 4 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 docs/rules/id-blacklist.md create mode 100644 lib/rules/id-blacklist.js diff --git a/docs/rules/id-blacklist.md b/docs/rules/id-blacklist.md new file mode 100644 index 00000000000..87a7d9503bb --- /dev/null +++ b/docs/rules/id-blacklist.md @@ -0,0 +1,3 @@ +# disallow specified identifiers (id-blacklist) + +This rule was **deprecated** in ESLint v7.5.0 and replaced by the [id-denylist](id-denylist.md) rule. diff --git a/lib/rules/id-blacklist.js b/lib/rules/id-blacklist.js new file mode 100644 index 00000000000..4fbba909fde --- /dev/null +++ b/lib/rules/id-blacklist.js @@ -0,0 +1,233 @@ +/** + * @fileoverview Rule that warns when identifier names that are + * specified in the configuration are used. + * @author Keith Cirkel (http://keithcirkel.co.uk) + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Checks whether the given node represents assignment target in a normal assignment or destructuring. + * @param {ASTNode} node The node to check. + * @returns {boolean} `true` if the node is assignment target. + */ +function isAssignmentTarget(node) { + const parent = node.parent; + + return ( + + // normal assignment + ( + parent.type === "AssignmentExpression" && + parent.left === node + ) || + + // destructuring + parent.type === "ArrayPattern" || + parent.type === "RestElement" || + ( + parent.type === "Property" && + parent.value === node && + parent.parent.type === "ObjectPattern" + ) || + ( + parent.type === "AssignmentPattern" && + parent.left === node + ) + ); +} + +/** + * Checks whether the given node represents an imported name that is renamed in the same import/export specifier. + * + * Examples: + * import { a as b } from 'mod'; // node `a` is renamed import + * export { a as b } from 'mod'; // node `a` is renamed import + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a renamed import. + */ +function isRenamedImport(node) { + const parent = node.parent; + + return ( + ( + parent.type === "ImportSpecifier" && + parent.imported !== parent.local && + parent.imported === node + ) || + ( + parent.type === "ExportSpecifier" && + parent.parent.source && // re-export + parent.local !== parent.exported && + parent.local === node + ) + ); +} + +/** + * Checks whether the given node is a renamed identifier node in an ObjectPattern destructuring. + * + * Examples: + * const { a : b } = foo; // node `a` is renamed node. + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a renamed node in an ObjectPattern destructuring. + */ +function isRenamedInDestructuring(node) { + const parent = node.parent; + + return ( + ( + !parent.computed && + parent.type === "Property" && + parent.parent.type === "ObjectPattern" && + parent.value !== node && + parent.key === node + ) + ); +} + +/** + * Checks whether the given node represents shorthand definition of a property in an object literal. + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a shorthand property definition. + */ +function isShorthandPropertyDefinition(node) { + const parent = node.parent; + + return ( + parent.type === "Property" && + parent.parent.type === "ObjectExpression" && + parent.shorthand + ); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + deprecated: true, + replacedBy: ["id-denylist"], + + type: "suggestion", + + docs: { + description: "disallow specified identifiers", + category: "Stylistic Issues", + recommended: false, + url: "https://eslint.org/docs/rules/id-blacklist" + }, + + schema: { + type: "array", + items: { + type: "string" + }, + uniqueItems: true + }, + messages: { + restricted: "Identifier '{{name}}' is restricted." + } + }, + + create(context) { + + const denyList = new Set(context.options); + const reportedNodes = new Set(); + + let globalScope; + + /** + * Checks whether the given name is restricted. + * @param {string} name The name to check. + * @returns {boolean} `true` if the name is restricted. + * @private + */ + function isRestricted(name) { + return denyList.has(name); + } + + /** + * Checks whether the given node represents a reference to a global variable that is not declared in the source code. + * These identifiers will be allowed, as it is assumed that user has no control over the names of external global variables. + * @param {ASTNode} node `Identifier` node to check. + * @returns {boolean} `true` if the node is a reference to a global variable. + */ + function isReferenceToGlobalVariable(node) { + const variable = globalScope.set.get(node.name); + + return variable && variable.defs.length === 0 && + variable.references.some(ref => ref.identifier === node); + } + + /** + * Determines whether the given node should be checked. + * @param {ASTNode} node `Identifier` node. + * @returns {boolean} `true` if the node should be checked. + */ + function shouldCheck(node) { + const parent = node.parent; + + /* + * Member access has special rules for checking property names. + * Read access to a property with a restricted name is allowed, because it can be on an object that user has no control over. + * Write access isn't allowed, because it potentially creates a new property with a restricted name. + */ + if ( + parent.type === "MemberExpression" && + parent.property === node && + !parent.computed + ) { + return isAssignmentTarget(parent); + } + + return ( + parent.type !== "CallExpression" && + parent.type !== "NewExpression" && + !isRenamedImport(node) && + !isRenamedInDestructuring(node) && + !( + isReferenceToGlobalVariable(node) && + !isShorthandPropertyDefinition(node) + ) + ); + } + + /** + * Reports an AST node as a rule violation. + * @param {ASTNode} node The node to report. + * @returns {void} + * @private + */ + function report(node) { + if (!reportedNodes.has(node)) { + context.report({ + node, + messageId: "restricted", + data: { + name: node.name + } + }); + reportedNodes.add(node); + } + } + + return { + + Program() { + globalScope = context.getScope(); + }, + + Identifier(node) { + if (isRestricted(node.name) && shouldCheck(node)) { + report(node); + } + } + }; + } +}; diff --git a/lib/rules/index.js b/lib/rules/index.js index e34d090899a..3cf26e51bc8 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -56,9 +56,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({ "grouped-accessor-pairs": () => require("./grouped-accessor-pairs"), "guard-for-in": () => require("./guard-for-in"), "handle-callback-err": () => require("./handle-callback-err"), - - // Renamed to id-denylist. - "id-blacklist": () => require("./id-denylist"), + "id-blacklist": () => require("./id-blacklist"), "id-denylist": () => require("./id-denylist"), "id-length": () => require("./id-length"), "id-match": () => require("./id-match"), diff --git a/tools/rule-types.json b/tools/rule-types.json index 2421b9fdaad..84700de70d0 100644 --- a/tools/rule-types.json +++ b/tools/rule-types.json @@ -43,6 +43,7 @@ "grouped-accessor-pairs": "suggestion", "guard-for-in": "suggestion", "handle-callback-err": "suggestion", + "id-blacklist": "suggestion", "id-denylist": "suggestion", "id-length": "suggestion", "id-match": "suggestion", From f1cc725ba1b8646dcf06a83716d96ad9bb726172 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Tue, 14 Jul 2020 23:39:53 +0200 Subject: [PATCH 39/66] Docs: fix linebreaks between versions in changelog (#13488) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7129c2bf442..f1c83077629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,11 @@ v7.4.0 - July 3, 2020 * [`0655f66`](https://github.com/eslint/eslint/commit/0655f66525d167ca1288167b79a77087cfc8fcf6) Update: improve report location in arrow-body-style (refs #12334) (#13424) (YeonJuan) * [`d53d69a`](https://github.com/eslint/eslint/commit/d53d69af08cfe55f42e0a0ca725b1014dabccc21) Update: prefer-regex-literal detect regex literals (fixes #12840) (#12842) (Mathias Schreck) * [`004adae`](https://github.com/eslint/eslint/commit/004adae3f959414f56e44e5884f6221e9dcda142) Update: rename id-blacklist to id-denylist (fixes #13407) (#13408) (Kai Cataldo) + v7.3.1 - June 22, 2020 * [`de77c11`](https://github.com/eslint/eslint/commit/de77c11e7515f2097ff355ddc0d7b6db9c83c892) Fix: Replace Infinity with Number.MAX_SAFE_INTEGER (fixes #13427) (#13435) (Nicholas C. Zakas) + v7.3.0 - June 19, 2020 * [`638a6d6`](https://github.com/eslint/eslint/commit/638a6d6be18b4a37cfdc7223e1f5acd3718694be) Update: add missing `additionalProperties: false` to some rules' schema (#13198) (Milos Djermanovic) @@ -45,6 +47,7 @@ v7.3.0 - June 19, 2020 * [`0f1f5ed`](https://github.com/eslint/eslint/commit/0f1f5ed2a20b8fb575d4360316861cf4c2b9b7bc) Docs: Add security policy link to README (#13403) (Nicholas C. Zakas) * [`9e9ba89`](https://github.com/eslint/eslint/commit/9e9ba897c566601cfe90522099c635ea316b235f) Sponsors: Sync README with website (ESLint Jenkins) * [`ca59fb9`](https://github.com/eslint/eslint/commit/ca59fb95a395c0a02ed23768a70e086480ab1f6d) Sponsors: Sync README with website (ESLint Jenkins) + v7.2.0 - June 5, 2020 * [`b735a48`](https://github.com/eslint/eslint/commit/b735a485e77bcc791e4c4c6b8716801d94e98b2c) Update: add enforceForFunctionPrototypeMethods option to no-extra-parens (#12895) (Milos Djermanovic) From 45cdf00da6aeff3d584d37b0710fc8d6ad9456d6 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 17 Jul 2020 17:11:53 -0400 Subject: [PATCH 40/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b5a911b2c1..a07082a2bd6 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

-

Shopify Salesforce Airbnb

Silver Sponsors

+

Shopify Salesforce Airbnb Microsoft FOSS Fund Sponsorships

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

From 6ed9e8e4ff038c0259b0e7fe7ab7f4fd4ec55801 Mon Sep 17 00:00:00 2001 From: Yohan Siguret Date: Sat, 18 Jul 2020 02:29:42 +0200 Subject: [PATCH 41/66] Upgrade: lodash@4.17.19 (#13499) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ebb81cd488f..442639e6d67 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", - "lodash": "^4.17.14", + "lodash": "^4.17.19", "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", From 1a01b420eaab0de03dab5cc190a9f2a860c21a84 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 17 Jul 2020 17:45:18 -0700 Subject: [PATCH 42/66] Docs: Update technology sponsors in README (#13478) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a07082a2bd6..c2c34b5c2a2 100644 --- a/README.md +++ b/README.md @@ -262,3 +262,5 @@ The following companies, organizations, and individuals support ESLint's ongoing ## Technology Sponsors * Site search ([eslint.org](https://eslint.org)) is sponsored by [Algolia](https://www.algolia.com) +* Hosting for ([eslint.org](https://eslint.org)) is sponsored by [Netlify](https://www.netlify.com) +* Password management is sponsored by [1Password](https://www.1password.com) From 885a1455691265db88dc0befe9b48a69d69e8b9c Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 18 Jul 2020 02:46:05 +0200 Subject: [PATCH 43/66] Docs: clarify behavior if `meta.fixable` is omitted (refs #13349) (#13493) --- 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 12864856195..2f47c316ccb 100644 --- a/docs/developer-guide/working-with-rules.md +++ b/docs/developer-guide/working-with-rules.md @@ -68,7 +68,7 @@ The source file for a rule exports an object with the following properties. * `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:** Without the `fixable` property, ESLint does not [apply fixes](#applying-fixes) even if the rule implements `fix` functions. Omit the `fixable` property if the rule is not fixable. + **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. * `schema` (array) specifies the [options](#options-schemas) so ESLint can prevent invalid [rule configurations](../user-guide/configuring.md#configuring-rules) @@ -301,7 +301,7 @@ context.report({ Here, the `fix()` function is used to insert a semicolon after the node. Note that a fix is not immediately applied, and may not be applied at all if there are conflicts with other fixes. After applying fixes, ESLint will run all of the enabled rules again on the fixed code, potentially applying more fixes. This process will repeat up to 10 times, or until no more fixable problems are found. Afterwards, any remaining problems will be reported as usual. -**Important:** Unless the rule [exports](#rule-basics) the `meta.fixable` property, ESLint does not apply fixes even if the rule implements `fix` functions. +**Important:** The `meta.fixable` property is mandatory for fixable rules. ESLint will throw an error if a rule that implements `fix` functions does not [export](#rule-basics) the `meta.fixable` property. The `fixer` object has the following methods: From 540b1af77278ae649b621aa8d4bf8d6de03c3155 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 18 Jul 2020 02:47:02 +0200 Subject: [PATCH 44/66] Chore: enable consistent-meta-messages internal rule (#13487) --- .eslintrc.js | 8 ++------ tests/tools/internal-rules/consistent-docs-url.js | 10 +++++++--- tools/internal-rules/consistent-docs-url.js | 14 ++++++++++---- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index a53fedba15b..1457d9b553c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -95,12 +95,8 @@ module.exports = { files: ["lib/rules/*", "tools/internal-rules/*"], excludedFiles: ["index.js"], rules: { - "internal-rules/no-invalid-meta": "error" - - /* - * TODO: enable it when all the rules using meta.messages - * "internal-rules/consistent-meta-messages": "error" - */ + "internal-rules/no-invalid-meta": "error", + "internal-rules/consistent-meta-messages": "error" } }, { diff --git a/tests/tools/internal-rules/consistent-docs-url.js b/tests/tools/internal-rules/consistent-docs-url.js index 3c61a181f39..4b3d3a05b67 100644 --- a/tests/tools/internal-rules/consistent-docs-url.js +++ b/tests/tools/internal-rules/consistent-docs-url.js @@ -55,7 +55,7 @@ ruleTester.run("consistent-docs-url", rule, { "};" ].join("\n"), errors: [{ - message: "Rule is missing a meta.docs property", + messageId: "missingMetaDocs", line: 2, column: 5 }] @@ -73,7 +73,7 @@ ruleTester.run("consistent-docs-url", rule, { "};" ].join("\n"), errors: [{ - message: "Rule is missing a meta.docs.url property", + messageId: "missingMetaDocsUrl", line: 3, column: 9 }] @@ -92,7 +92,11 @@ ruleTester.run("consistent-docs-url", rule, { "};" ].join("\n"), errors: [{ - message: "Incorrect url. Expected \"https://eslint.org/docs/rules/\" but got \"http://example.com/wrong-url\"", + messageId: "incorrectUrl", + data: { + expected: "https://eslint.org/docs/rules/", + url: "http://example.com/wrong-url" + }, line: 4, column: 18 }] diff --git a/tools/internal-rules/consistent-docs-url.js b/tools/internal-rules/consistent-docs-url.js index 1a52bd59280..052fe55f28a 100644 --- a/tools/internal-rules/consistent-docs-url.js +++ b/tools/internal-rules/consistent-docs-url.js @@ -55,7 +55,7 @@ function checkMetaDocsUrl(context, exportsNode) { if (!metaDocs) { context.report({ node: metaProperty, - message: "Rule is missing a meta.docs property" + messageId: "missingMetaDocs" }); return; } @@ -63,7 +63,7 @@ function checkMetaDocsUrl(context, exportsNode) { if (!metaDocsUrl) { context.report({ node: metaDocs, - message: "Rule is missing a meta.docs.url property" + messageId: "missingMetaDocsUrl" }); return; } @@ -75,7 +75,8 @@ function checkMetaDocsUrl(context, exportsNode) { if (url !== expected) { context.report({ node: metaDocsUrl.value, - message: `Incorrect url. Expected "${expected}" but got "${url}"` + messageId: "incorrectUrl", + data: { expected, url } }); } @@ -93,7 +94,12 @@ module.exports = { recommended: false }, type: "suggestion", - schema: [] + schema: [], + messages: { + missingMetaDocs: "Rule is missing a meta.docs property.", + missingMetaDocsUrl: "Rule is missing a meta.docs.url property.", + incorrectUrl: 'Incorrect url. Expected "{{ expected }}" but got "{{ url }}".' + } }, create(context) { From 6ea3178776eae0e40c3f5498893e8aab0e23686b Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 19 Jul 2020 01:40:43 +0900 Subject: [PATCH 45/66] Update: optional chaining support (fixes #12642) (#13416) * update deps (to branch) * trivial fix for debug output * update code path analysis * update accessor-pairs * update array-callback-return * add tests for camelcase * add tests for computed-property-spacing * update dot-location * update dot-notation * update func-name-matching * update global-require * update indent * add tests for getter-return * update new-cap * update newline-per-chained-call * update no-alert * update no-extend-native * update no-extra-bind * update no-extra-parens * update no-eval * update no-implicit-coercion * update eslint-utils * update no-implied-eval * update no-import-assign * update no-magic-numbers * update no-obj-calls * update no-prototype-builtins * add tests for no-restricted-syntax * update no-self-assign * update no-setter-return * update no-unexpected-multiline * update no-unused-expression * update no-useless-call * update no-whitespace-before-property * update operator-assignment * update padding-line-between-statements * update prefer-arrow-callback * add tests for prefer-destructuring * update prefer-exponentiation-operator * update prefer-numeric-literals * update prefer-promise-reject-errors * update prefer-regex-literals * update prefer-spread * update use-isnan * update yoda * update wrap-iife * remove __proto__ * fix no-import-assign for delete op * update eslint-visitor-keys * fix no-unexpected-multiline to just ignore optional chaining * update func-call-spacing * update constructor-super * update dot-location for unstable sort * update no-extra-boolean-cast * update func-call-spacing * update no-extra-parens for false positive on IIFE * update array-callback-return * update no-invalid-this (astUtils.isDefaultThisBinding) * update radix * update a comment in no-implicit-coercion * update comments in no-extra-bind * remove unnecessary change from array-callback-return * update dot-notation for autofix about `let?.[` * update new-cap * update wrap-iife * update prefer-arrow-callback * change isSameReference to handle `a.b` and `a?.b` are same * fix code path analysis for `node.arguments.length == 0` case * update `astUtils.couldBeError` * update `astUtils.isMethodWhichHasThisArg` * improve coverage * fix isMethodWhichHasThisArg * update no-self-assign * Upgrade: espree@7.2.0 Co-authored-by: Kai Cataldo --- .../code-path-analysis/code-path-analyzer.js | 38 + .../code-path-analysis/code-path-segment.js | 1 - .../code-path-analysis/code-path-state.js | 59 + .../code-path-analysis/debug-helpers.js | 45 +- lib/rules/accessor-pairs.js | 15 +- lib/rules/array-callback-return.js | 12 +- lib/rules/consistent-return.js | 13 +- lib/rules/constructor-super.js | 1 + lib/rules/dot-location.js | 34 +- lib/rules/dot-notation.js | 69 +- lib/rules/func-call-spacing.js | 48 +- lib/rules/func-name-matching.js | 5 +- lib/rules/global-require.js | 3 +- lib/rules/indent.js | 19 + lib/rules/new-cap.js | 24 +- lib/rules/newline-per-chained-call.js | 20 +- lib/rules/no-alert.js | 13 +- lib/rules/no-eval.js | 46 +- lib/rules/no-extend-native.js | 77 +- lib/rules/no-extra-bind.js | 74 +- lib/rules/no-extra-boolean-cast.js | 7 + lib/rules/no-extra-parens.js | 34 +- lib/rules/no-implicit-coercion.js | 17 +- lib/rules/no-implied-eval.js | 35 +- lib/rules/no-import-assign.js | 65 +- lib/rules/no-magic-numbers.js | 12 +- lib/rules/no-obj-calls.js | 11 +- lib/rules/no-prototype-builtins.js | 16 +- lib/rules/no-self-assign.js | 56 +- lib/rules/no-setter-return.js | 13 +- lib/rules/no-unexpected-multiline.js | 4 +- lib/rules/no-unused-expressions.js | 78 +- lib/rules/no-useless-call.js | 17 +- lib/rules/no-whitespace-before-property.js | 20 +- lib/rules/operator-assignment.js | 45 +- lib/rules/padding-line-between-statements.js | 4 +- lib/rules/prefer-arrow-callback.js | 115 +- lib/rules/prefer-exponentiation-operator.js | 2 +- lib/rules/prefer-numeric-literals.js | 17 +- lib/rules/prefer-promise-reject-errors.js | 4 +- lib/rules/prefer-regex-literals.js | 7 +- lib/rules/prefer-spread.js | 8 +- lib/rules/radix.js | 7 +- lib/rules/use-isnan.js | 2 +- lib/rules/utils/ast-utils.js | 470 ++++-- lib/rules/wrap-iife.js | 11 +- lib/rules/yoda.js | 57 +- package.json | 6 +- .../code-path-analysis/logical--if-qdot-1.js | 38 + .../optional-chaining--complex-1.js | 38 + .../optional-chaining--complex-2.js | 38 + .../optional-chaining--complex-3.js | 41 + .../optional-chaining--simple-1.js | 19 + .../optional-chaining--simple-2.js | 19 + .../optional-chaining--simple-3.js | 25 + .../optional-chaining--simple-4.js | 19 + tests/lib/rules/accessor-pairs.js | 40 + tests/lib/rules/array-callback-return.js | 27 + tests/lib/rules/camelcase.js | 14 + tests/lib/rules/computed-property-spacing.js | 22 + tests/lib/rules/constructor-super.js | 7 +- tests/lib/rules/dot-location.js | 85 ++ tests/lib/rules/dot-notation.js | 28 + tests/lib/rules/func-call-spacing.js | 134 +- tests/lib/rules/func-name-matching.js | 73 + tests/lib/rules/getter-return.js | 26 +- tests/lib/rules/global-require.js | 8 +- tests/lib/rules/id-blacklist.js | 1359 +++++++++++++++++ tests/lib/rules/indent.js | 99 ++ tests/lib/rules/new-cap.js | 51 +- tests/lib/rules/newline-per-chained-call.js | 66 +- tests/lib/rules/no-alert.js | 12 + tests/lib/rules/no-eval.js | 27 +- tests/lib/rules/no-extend-native.js | 27 +- tests/lib/rules/no-extra-bind.js | 48 + tests/lib/rules/no-extra-boolean-cast.js | 15 + tests/lib/rules/no-extra-parens.js | 55 +- tests/lib/rules/no-implicit-coercion.js | 22 + tests/lib/rules/no-implied-eval.js | 14 + tests/lib/rules/no-import-assign.js | 17 + tests/lib/rules/no-invalid-this.js | 48 + tests/lib/rules/no-magic-numbers.js | 19 + tests/lib/rules/no-obj-calls.js | 12 + tests/lib/rules/no-prototype-builtins.js | 12 + tests/lib/rules/no-restricted-syntax.js | 29 + tests/lib/rules/no-self-assign.js | 14 +- tests/lib/rules/no-setter-return.js | 14 +- tests/lib/rules/no-throw-literal.js | 4 +- tests/lib/rules/no-unexpected-multiline.js | 18 + tests/lib/rules/no-unused-expressions.js | 25 + tests/lib/rules/no-useless-call.js | 88 +- .../rules/no-whitespace-before-property.js | 90 +- tests/lib/rules/operator-assignment.js | 18 +- .../rules/padding-line-between-statements.js | 16 + tests/lib/rules/prefer-arrow-callback.js | 40 +- tests/lib/rules/prefer-destructuring.js | 8 +- .../rules/prefer-exponentiation-operator.js | 11 +- tests/lib/rules/prefer-numeric-literals.js | 29 +- .../lib/rules/prefer-promise-reject-errors.js | 17 +- tests/lib/rules/prefer-regex-literals.js | 9 +- tests/lib/rules/prefer-spread.js | 46 +- tests/lib/rules/radix.js | 22 + tests/lib/rules/use-isnan.js | 18 + tests/lib/rules/utils/ast-utils.js | 151 ++ tests/lib/rules/wrap-iife.js | 30 + tests/lib/rules/yoda.js | 5 + 106 files changed, 4293 insertions(+), 769 deletions(-) create mode 100644 tests/fixtures/code-path-analysis/logical--if-qdot-1.js create mode 100644 tests/fixtures/code-path-analysis/optional-chaining--complex-1.js create mode 100644 tests/fixtures/code-path-analysis/optional-chaining--complex-2.js create mode 100644 tests/fixtures/code-path-analysis/optional-chaining--complex-3.js create mode 100644 tests/fixtures/code-path-analysis/optional-chaining--simple-1.js create mode 100644 tests/fixtures/code-path-analysis/optional-chaining--simple-2.js create mode 100644 tests/fixtures/code-path-analysis/optional-chaining--simple-3.js create mode 100644 tests/fixtures/code-path-analysis/optional-chaining--simple-4.js diff --git a/lib/linter/code-path-analysis/code-path-analyzer.js b/lib/linter/code-path-analysis/code-path-analyzer.js index b612cf43566..3a0cda51118 100644 --- a/lib/linter/code-path-analysis/code-path-analyzer.js +++ b/lib/linter/code-path-analysis/code-path-analyzer.js @@ -244,6 +244,19 @@ function preprocess(analyzer, node) { const parent = node.parent; switch (parent.type) { + + // The `arguments.length == 0` case is in `postprocess` function. + case "CallExpression": + if (parent.optional === true && parent.arguments.length >= 1 && parent.arguments[0] === node) { + state.makeOptionalRight(); + } + break; + case "MemberExpression": + if (parent.optional === true && parent.property === node) { + state.makeOptionalRight(); + } + break; + case "LogicalExpression": if ( parent.right === node && @@ -377,6 +390,20 @@ function processCodePathToEnter(analyzer, node) { analyzer.emitter.emit("onCodePathStart", codePath, node); break; + case "ChainExpression": + state.pushChainContext(); + break; + case "CallExpression": + if (node.optional === true) { + state.makeOptionalNode(); + } + break; + case "MemberExpression": + if (node.optional === true) { + state.makeOptionalNode(); + } + break; + case "LogicalExpression": if (isHandledLogicalOperator(node.operator)) { state.pushChoiceContext( @@ -449,6 +476,10 @@ function processCodePathToExit(analyzer, node) { let dontForward = false; switch (node.type) { + case "ChainExpression": + state.popChainContext(); + break; + case "IfStatement": case "ConditionalExpression": state.popChoiceContext(); @@ -583,6 +614,13 @@ function postprocess(analyzer, node) { break; } + // The `arguments.length >= 1` case is in `preprocess` function. + case "CallExpression": + if (node.optional === true && node.arguments.length === 0) { + CodePath.getState(analyzer.codePath).makeOptionalRight(); + } + break; + default: break; } diff --git a/lib/linter/code-path-analysis/code-path-segment.js b/lib/linter/code-path-analysis/code-path-segment.js index 6b17b25c7fd..ca96ad34189 100644 --- a/lib/linter/code-path-analysis/code-path-segment.js +++ b/lib/linter/code-path-analysis/code-path-segment.js @@ -92,7 +92,6 @@ class CodePathSegment { /* istanbul ignore if */ if (debug.enabled) { this.internal.nodes = []; - this.internal.exitNodes = []; } } diff --git a/lib/linter/code-path-analysis/code-path-state.js b/lib/linter/code-path-analysis/code-path-state.js index 9e760601a0f..f2b16d07e0d 100644 --- a/lib/linter/code-path-analysis/code-path-state.js +++ b/lib/linter/code-path-analysis/code-path-state.js @@ -234,6 +234,7 @@ class CodePathState { this.tryContext = null; this.loopContext = null; this.breakContext = null; + this.chainContext = null; this.currentSegments = []; this.initialSegment = this.forkContext.head[0]; @@ -555,6 +556,64 @@ class CodePathState { ); } + //-------------------------------------------------------------------------- + // ChainExpression + //-------------------------------------------------------------------------- + + /** + * Push a new `ChainExpression` context to the stack. + * This method is called on entering to each `ChainExpression` node. + * This context is used to count forking in the optional chain then merge them on the exiting from the `ChainExpression` node. + * @returns {void} + */ + pushChainContext() { + this.chainContext = { + upper: this.chainContext, + countChoiceContexts: 0 + }; + } + + /** + * Pop a `ChainExpression` context from the stack. + * This method is called on exiting from each `ChainExpression` node. + * This merges all forks of the last optional chaining. + * @returns {void} + */ + popChainContext() { + const context = this.chainContext; + + this.chainContext = context.upper; + + // pop all choice contexts of this. + for (let i = context.countChoiceContexts; i > 0; --i) { + this.popChoiceContext(); + } + } + + /** + * Create a choice context for optional access. + * This method is called on entering to each `(Call|Member)Expression[optional=true]` node. + * This creates a choice context as similar to `LogicalExpression[operator="??"]` node. + * @returns {void} + */ + makeOptionalNode() { + if (this.chainContext) { + this.chainContext.countChoiceContexts += 1; + this.pushChoiceContext("??", false); + } + } + + /** + * Create a fork. + * This method is called on entering to the `arguments|property` property of each `(Call|Member)Expression` node. + * @returns {void} + */ + makeOptionalRight() { + if (this.chainContext) { + this.makeLogicalRight(); + } + } + //-------------------------------------------------------------------------- // SwitchStatement //-------------------------------------------------------------------------- diff --git a/lib/linter/code-path-analysis/debug-helpers.js b/lib/linter/code-path-analysis/debug-helpers.js index bde4e0a38ad..a4cb99a22e0 100644 --- a/lib/linter/code-path-analysis/debug-helpers.js +++ b/lib/linter/code-path-analysis/debug-helpers.js @@ -25,6 +25,22 @@ function getId(segment) { // eslint-disable-line jsdoc/require-jsdoc return segment.id + (segment.reachable ? "" : "!"); } +/** + * Get string for the given node and operation. + * @param {ASTNode} node The node to convert. + * @param {"enter" | "exit" | undefined} label The operation label. + * @returns {string} The string representation. + */ +function nodeToString(node, label) { + const suffix = label ? `:${label}` : ""; + + switch (node.type) { + case "Identifier": return `${node.type}${suffix} (${node.name})`; + case "Literal": return `${node.type}${suffix} (${node.value})`; + default: return `${node.type}${suffix}`; + } +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -56,9 +72,15 @@ module.exports = { const segInternal = state.currentSegments[i].internal; if (leaving) { - segInternal.exitNodes.push(node); + const last = segInternal.nodes.length - 1; + + if (last >= 0 && segInternal.nodes[last] === nodeToString(node, "enter")) { + segInternal.nodes[last] = nodeToString(node, void 0); + } else { + segInternal.nodes.push(nodeToString(node, "exit")); + } } else { - segInternal.nodes.push(node); + segInternal.nodes.push(nodeToString(node, "enter")); } } @@ -104,23 +126,8 @@ module.exports = { text += "style=\"rounded,dashed,filled\",fillcolor=\"#FF9800\",label=\"<>\\n"; } - if (segment.internal.nodes.length > 0 || segment.internal.exitNodes.length > 0) { - text += [].concat( - segment.internal.nodes.map(node => { - switch (node.type) { - case "Identifier": return `${node.type} (${node.name})`; - case "Literal": return `${node.type} (${node.value})`; - default: return node.type; - } - }), - segment.internal.exitNodes.map(node => { - switch (node.type) { - case "Identifier": return `${node.type}:exit (${node.name})`; - case "Literal": return `${node.type}:exit (${node.value})`; - default: return `${node.type}:exit`; - } - }) - ).join("\\n"); + if (segment.internal.nodes.length > 0) { + text += segment.internal.nodes.join("\\n"); } else { text += "????"; } diff --git a/lib/rules/accessor-pairs.js b/lib/rules/accessor-pairs.js index cf994ad2574..0e0d07a00c9 100644 --- a/lib/rules/accessor-pairs.js +++ b/lib/rules/accessor-pairs.js @@ -86,16 +86,6 @@ function isAccessorKind(node) { return node.kind === "get" || node.kind === "set"; } -/** - * Checks whether or not a given node is an `Identifier` node which was named a given name. - * @param {ASTNode} node A node to check. - * @param {string} name An expected name of the node. - * @returns {boolean} `true` if the node is an `Identifier` node which was named as expected. - */ -function isIdentifier(node, name) { - return node.type === "Identifier" && node.name === name; -} - /** * Checks whether or not a given node is an argument of a specified method call. * @param {ASTNode} node A node to check. @@ -109,10 +99,7 @@ function isArgumentOfMethodCall(node, index, object, property) { return ( parent.type === "CallExpression" && - parent.callee.type === "MemberExpression" && - parent.callee.computed === false && - isIdentifier(parent.callee.object, object) && - isIdentifier(parent.callee.property, property) && + astUtils.isSpecificMemberAccess(parent.callee, object, property) && parent.arguments[index] === node ); } diff --git a/lib/rules/array-callback-return.js b/lib/rules/array-callback-return.js index 02a96e311e5..7267347149d 100644 --- a/lib/rules/array-callback-return.js +++ b/lib/rules/array-callback-return.js @@ -28,17 +28,14 @@ function isReachable(segment) { } /** - * Checks a given node is a MemberExpression node which has the specified name's + * Checks a given node is a member access which has the specified name's * property. * @param {ASTNode} node A node to check. - * @returns {boolean} `true` if the node is a MemberExpression node which has - * the specified name's property + * @returns {boolean} `true` if the node is a member access which has + * the specified name's property. The node may be a `(Chain|Member)Expression` node. */ function isTargetMethod(node) { - return ( - node.type === "MemberExpression" && - TARGET_METHODS.test(astUtils.getStaticPropertyName(node) || "") - ); + return astUtils.isSpecificMemberAccess(node, null, TARGET_METHODS); } /** @@ -76,6 +73,7 @@ function getArrayMethodName(node) { */ case "LogicalExpression": case "ConditionalExpression": + case "ChainExpression": currentNode = parent; break; diff --git a/lib/rules/consistent-return.js b/lib/rules/consistent-return.js index 22667fa4707..94db253d25b 100644 --- a/lib/rules/consistent-return.js +++ b/lib/rules/consistent-return.js @@ -9,23 +9,12 @@ //------------------------------------------------------------------------------ const lodash = require("lodash"); - const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ -/** - * Checks whether or not a given node is an `Identifier` node which was named a given name. - * @param {ASTNode} node A node to check. - * @param {string} name An expected name of the node. - * @returns {boolean} `true` if the node is an `Identifier` node which was named as expected. - */ -function isIdentifier(node, name) { - return node.type === "Identifier" && node.name === name; -} - /** * Checks whether or not a given code path segment is unreachable. * @param {CodePathSegment} segment A CodePathSegment to check. @@ -165,7 +154,7 @@ module.exports = { let hasReturnValue = Boolean(argument); if (treatUndefinedAsUnspecified && hasReturnValue) { - hasReturnValue = !isIdentifier(argument, "undefined") && argument.operator !== "void"; + hasReturnValue = !astUtils.isSpecificId(argument, "undefined") && argument.operator !== "void"; } if (!funcInfo.hasReturn) { diff --git a/lib/rules/constructor-super.js b/lib/rules/constructor-super.js index 5a848f210ca..65ed7422c25 100644 --- a/lib/rules/constructor-super.js +++ b/lib/rules/constructor-super.js @@ -50,6 +50,7 @@ function isPossibleConstructor(node) { case "MemberExpression": case "CallExpression": case "NewExpression": + case "ChainExpression": case "YieldExpression": case "TaggedTemplateExpression": case "MetaProperty": diff --git a/lib/rules/dot-location.js b/lib/rules/dot-location.js index d483e217a94..0a739b1712b 100644 --- a/lib/rules/dot-location.js +++ b/lib/rules/dot-location.js @@ -52,31 +52,37 @@ module.exports = { */ function checkDotLocation(node) { const property = node.property; - const dot = sourceCode.getTokenBefore(property); - - // `obj` expression can be parenthesized, but those paren tokens are not a part of the `obj` node. - const tokenBeforeDot = sourceCode.getTokenBefore(dot); - - const textBeforeDot = sourceCode.getText().slice(tokenBeforeDot.range[1], dot.range[0]); - const textAfterDot = sourceCode.getText().slice(dot.range[1], property.range[0]); + const dotToken = sourceCode.getTokenBefore(property); if (onObject) { - if (!astUtils.isTokenOnSameLine(tokenBeforeDot, dot)) { - const neededTextAfterToken = astUtils.isDecimalIntegerNumericToken(tokenBeforeDot) ? " " : ""; + // `obj` expression can be parenthesized, but those paren tokens are not a part of the `obj` node. + const tokenBeforeDot = sourceCode.getTokenBefore(dotToken); + + if (!astUtils.isTokenOnSameLine(tokenBeforeDot, dotToken)) { context.report({ node, - loc: dot.loc, + loc: dotToken.loc, messageId: "expectedDotAfterObject", - fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], property.range[0]], `${neededTextAfterToken}.${textBeforeDot}${textAfterDot}`) + *fix(fixer) { + if (dotToken.value.startsWith(".") && astUtils.isDecimalIntegerNumericToken(tokenBeforeDot)) { + yield fixer.insertTextAfter(tokenBeforeDot, ` ${dotToken.value}`); + } else { + yield fixer.insertTextAfter(tokenBeforeDot, dotToken.value); + } + yield fixer.remove(dotToken); + } }); } - } else if (!astUtils.isTokenOnSameLine(dot, property)) { + } else if (!astUtils.isTokenOnSameLine(dotToken, property)) { context.report({ node, - loc: dot.loc, + loc: dotToken.loc, messageId: "expectedDotBeforeProperty", - fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], property.range[0]], `${textBeforeDot}${textAfterDot}.`) + *fix(fixer) { + yield fixer.remove(dotToken); + yield fixer.insertTextBefore(property, dotToken.value); + } }); } } diff --git a/lib/rules/dot-notation.js b/lib/rules/dot-notation.js index 2e8fff8b90e..751b4628edc 100644 --- a/lib/rules/dot-notation.js +++ b/lib/rules/dot-notation.js @@ -87,28 +87,36 @@ module.exports = { data: { key: formattedValue }, - fix(fixer) { + *fix(fixer) { const leftBracket = sourceCode.getTokenAfter(node.object, astUtils.isOpeningBracketToken); const rightBracket = sourceCode.getLastToken(node); + const nextToken = sourceCode.getTokenAfter(node); - if (sourceCode.getFirstTokenBetween(leftBracket, rightBracket, { includeComments: true, filter: astUtils.isCommentToken })) { - - // Don't perform any fixes if there are comments inside the brackets. - return null; + // 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 } - const tokenAfterProperty = sourceCode.getTokenAfter(rightBracket); - const needsSpaceAfterProperty = tokenAfterProperty && - rightBracket.range[1] === tokenAfterProperty.range[0] && - !astUtils.canTokensBeAdjacent(String(value), tokenAfterProperty); - - const textBeforeDot = astUtils.isDecimalInteger(node.object) ? " " : ""; - const textAfterProperty = needsSpaceAfterProperty ? " " : ""; - - return fixer.replaceTextRange( + // Replace the brackets by an identifier. + if (!node.optional) { + yield fixer.insertTextBefore( + leftBracket, + astUtils.isDecimalInteger(node.object) ? " ." : "." + ); + } + yield fixer.replaceTextRange( [leftBracket.range[0], rightBracket.range[1]], - `${textBeforeDot}.${value}${textAfterProperty}` + value ); + + // Insert a space after the property if it will be connected to the next token. + if ( + nextToken && + rightBracket.range[1] === nextToken.range[0] && + !astUtils.canTokensBeAdjacent(String(value), nextToken) + ) { + yield fixer.insertTextAfter(node, " "); + } } }); } @@ -141,29 +149,24 @@ module.exports = { data: { key: node.property.name }, - fix(fixer) { - const dot = sourceCode.getTokenBefore(node.property); - const textAfterDot = sourceCode.text.slice(dot.range[1], node.property.range[0]); - - if (textAfterDot.trim()) { + *fix(fixer) { + const dotToken = sourceCode.getTokenBefore(node.property); - // Don't perform any fixes if there are comments between the dot and the property name. - return null; + // 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 } - if (node.object.type === "Identifier" && node.object.name === "let") { - - /* - * A statement that starts with `let[` is parsed as a destructuring variable declaration, not - * a MemberExpression. - */ - return null; + // 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 fixer.replaceTextRange( - [dot.range[0], node.property.range[1]], - `[${textAfterDot}"${node.property.name}"]` - ); + // Replace the identifier to brackets. + if (!node.optional) { + yield fixer.remove(dotToken); + } + yield fixer.replaceText(node.property, `["${node.property.name}"]`); } }); } diff --git a/lib/rules/func-call-spacing.js b/lib/rules/func-call-spacing.js index 5ecb63ecfa7..8fe690d4a6b 100644 --- a/lib/rules/func-call-spacing.js +++ b/lib/rules/func-call-spacing.js @@ -126,15 +126,24 @@ module.exports = { messageId: "unexpectedWhitespace", fix(fixer) { + // Don't remove comments. + if (sourceCode.commentsExistBetween(leftToken, rightToken)) { + return null; + } + + // If `?.` exsits, it doesn't hide no-undexpected-multiline errors + if (node.optional) { + return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], "?."); + } + /* * Only autofix if there is no newline * https://github.com/eslint/eslint/issues/7787 */ - if (!hasNewline) { - return fixer.removeRange([leftToken.range[1], rightToken.range[0]]); + if (hasNewline) { + return null; } - - return null; + return fixer.removeRange([leftToken.range[1], rightToken.range[0]]); } }); } else if (!never && !hasWhitespace) { @@ -149,6 +158,9 @@ module.exports = { }, messageId: "missing", fix(fixer) { + if (node.optional) { + return null; // Not sure if inserting a space to either before/after `?.` token. + } return fixer.insertTextBefore(rightToken, " "); } }); @@ -161,7 +173,31 @@ module.exports = { }, messageId: "unexpectedNewline", fix(fixer) { - return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], " "); + + /* + * Only autofix if there is no newline + * https://github.com/eslint/eslint/issues/7787 + * But if `?.` exsits, it doesn't hide no-undexpected-multiline errors + */ + if (!node.optional) { + return null; + } + + // Don't remove comments. + if (sourceCode.commentsExistBetween(leftToken, rightToken)) { + return null; + } + + const range = [leftToken.range[1], rightToken.range[0]]; + const qdToken = sourceCode.getTokenAfter(leftToken); + + if (qdToken.range[0] === leftToken.range[1]) { + return fixer.replaceTextRange(range, "?. "); + } + if (qdToken.range[1] === rightToken.range[0]) { + return fixer.replaceTextRange(range, " ?."); + } + return fixer.replaceTextRange(range, " ?. "); } }); } @@ -172,7 +208,7 @@ module.exports = { const lastToken = sourceCode.getLastToken(node); const lastCalleeToken = sourceCode.getLastToken(node.callee); const parenToken = sourceCode.getFirstTokenBetween(lastCalleeToken, lastToken, astUtils.isOpeningParenToken); - const prevToken = parenToken && sourceCode.getTokenBefore(parenToken); + const prevToken = parenToken && sourceCode.getTokenBefore(parenToken, astUtils.isNotQuestionDotToken); // Parens in NewExpression are optional if (!(parenToken && parenToken.range[1] < node.range[1])) { diff --git a/lib/rules/func-name-matching.js b/lib/rules/func-name-matching.js index 83430ffadfc..755c2ee5075 100644 --- a/lib/rules/func-name-matching.js +++ b/lib/rules/func-name-matching.js @@ -117,10 +117,7 @@ module.exports = { if (!node) { return false; } - return node.type === "CallExpression" && - node.callee.type === "MemberExpression" && - node.callee.object.name === objName && - node.callee.property.name === funcName; + return node.type === "CallExpression" && astUtils.isSpecificMemberAccess(node.callee, objName, funcName); } /** diff --git a/lib/rules/global-require.js b/lib/rules/global-require.js index 469c0175d25..09d0332007e 100644 --- a/lib/rules/global-require.js +++ b/lib/rules/global-require.js @@ -13,7 +13,8 @@ const ACCEPTABLE_PARENTS = [ "CallExpression", "ConditionalExpression", "Program", - "VariableDeclaration" + "VariableDeclaration", + "ChainExpression" ]; /** diff --git a/lib/rules/indent.js b/lib/rules/indent.js index d576fde0382..22b633845b5 100644 --- a/lib/rules/indent.js +++ b/lib/rules/indent.js @@ -32,6 +32,7 @@ const KNOWN_NODES = new Set([ "BreakStatement", "CallExpression", "CatchClause", + "ChainExpression", "ClassBody", "ClassDeclaration", "ClassExpression", @@ -934,6 +935,24 @@ module.exports = { parameterParens.add(openingParen); parameterParens.add(closingParen); + /* + * If `?.` token exists, set desired offset for that. + * This logic is copied from `MemberExpression`'s. + */ + if (node.optional) { + const dotToken = sourceCode.getTokenAfter(node.callee, astUtils.isQuestionDotToken); + const calleeParenCount = sourceCode.getTokensBetween(node.callee, dotToken, { filter: astUtils.isClosingParenToken }).length; + const firstTokenOfCallee = calleeParenCount + ? sourceCode.getTokenBefore(node.callee, { skip: calleeParenCount - 1 }) + : sourceCode.getFirstToken(node.callee); + const lastTokenOfCallee = sourceCode.getTokenBefore(dotToken); + const offsetBase = lastTokenOfCallee.loc.end.line === openingParen.loc.start.line + ? lastTokenOfCallee + : firstTokenOfCallee; + + offsets.setDesiredOffset(dotToken, offsetBase, 1); + } + const offsetAfterToken = node.callee.type === "TaggedTemplateExpression" ? sourceCode.getFirstToken(node.callee.quasi) : openingParen; const offsetToken = sourceCode.getTokenBefore(offsetAfterToken); diff --git a/lib/rules/new-cap.js b/lib/rules/new-cap.js index 0faf45efb92..4249a542802 100644 --- a/lib/rules/new-cap.js +++ b/lib/rules/new-cap.js @@ -158,15 +158,9 @@ module.exports = { * @returns {string} name */ function extractNameFromExpression(node) { - - let name = ""; - - if (node.callee.type === "MemberExpression") { - name = astUtils.getStaticPropertyName(node.callee) || ""; - } else { - name = node.callee.name; - } - return name; + return node.callee.type === "Identifier" + ? node.callee.name + : astUtils.getStaticPropertyName(node.callee) || ""; } /** @@ -212,14 +206,16 @@ module.exports = { return true; } - if (calleeName === "UTC" && node.callee.type === "MemberExpression") { + const callee = astUtils.skipChainExpression(node.callee); + + if (calleeName === "UTC" && callee.type === "MemberExpression") { // allow if callee is Date.UTC - return node.callee.object.type === "Identifier" && - node.callee.object.name === "Date"; + return callee.object.type === "Identifier" && + callee.object.name === "Date"; } - return skipProperties && node.callee.type === "MemberExpression"; + return skipProperties && callee.type === "MemberExpression"; } /** @@ -229,7 +225,7 @@ module.exports = { * @returns {void} */ function report(node, messageId) { - let callee = node.callee; + let callee = astUtils.skipChainExpression(node.callee); if (callee.type === "MemberExpression") { callee = callee.property; diff --git a/lib/rules/newline-per-chained-call.js b/lib/rules/newline-per-chained-call.js index 4254fec185e..46c9d6c10f8 100644 --- a/lib/rules/newline-per-chained-call.js +++ b/lib/rules/newline-per-chained-call.js @@ -57,7 +57,16 @@ module.exports = { * @returns {string} The prefix of the node. */ function getPrefix(node) { - return node.computed ? "[" : "."; + if (node.computed) { + if (node.optional) { + return "?.["; + } + return "["; + } + if (node.optional) { + return "?."; + } + return "."; } /** @@ -76,17 +85,18 @@ module.exports = { return { "CallExpression:exit"(node) { - if (!node.callee || node.callee.type !== "MemberExpression") { + const callee = astUtils.skipChainExpression(node.callee); + + if (callee.type !== "MemberExpression") { return; } - const callee = node.callee; - let parent = callee.object; + let parent = astUtils.skipChainExpression(callee.object); let depth = 1; while (parent && parent.callee) { depth += 1; - parent = parent.callee.object; + parent = astUtils.skipChainExpression(astUtils.skipChainExpression(parent.callee).object); } if (depth > ignoreChainWithDepth && astUtils.isTokenOnSameLine(callee.object, callee.property)) { diff --git a/lib/rules/no-alert.js b/lib/rules/no-alert.js index 22d0dd57bdd..702b4d2ba7c 100644 --- a/lib/rules/no-alert.js +++ b/lib/rules/no-alert.js @@ -10,7 +10,8 @@ const { getStaticPropertyName: getPropertyName, - getVariableByName + getVariableByName, + skipChainExpression } = require("./utils/ast-utils"); //------------------------------------------------------------------------------ @@ -64,7 +65,13 @@ function isGlobalThisReferenceOrGlobalWindow(scope, node) { if (scope.type === "global" && node.type === "ThisExpression") { return true; } - if (node.name === "window" || (node.name === "globalThis" && getVariableByName(scope, "globalThis"))) { + if ( + node.type === "Identifier" && + ( + node.name === "window" || + (node.name === "globalThis" && getVariableByName(scope, "globalThis")) + ) + ) { return !isShadowed(scope, node); } @@ -96,7 +103,7 @@ module.exports = { create(context) { return { CallExpression(node) { - const callee = node.callee, + const callee = skipChainExpression(node.callee), currentScope = context.getScope(); // without window. diff --git a/lib/rules/no-eval.js b/lib/rules/no-eval.js index 811ad4e5d73..a020fdee014 100644 --- a/lib/rules/no-eval.js +++ b/lib/rules/no-eval.js @@ -21,38 +21,6 @@ const candidatesOfGlobalObject = Object.freeze([ "globalThis" ]); -/** - * Checks a given node is a Identifier node of the specified name. - * @param {ASTNode} node A node to check. - * @param {string} name A name to check. - * @returns {boolean} `true` if the node is a Identifier node of the name. - */ -function isIdentifier(node, name) { - return node.type === "Identifier" && node.name === name; -} - -/** - * Checks a given node is a Literal node of the specified string value. - * @param {ASTNode} node A node to check. - * @param {string} name A name to check. - * @returns {boolean} `true` if the node is a Literal node of the name. - */ -function isConstant(node, name) { - switch (node.type) { - case "Literal": - return node.value === name; - - case "TemplateLiteral": - return ( - node.expressions.length === 0 && - node.quasis[0].value.cooked === name - ); - - default: - return false; - } -} - /** * Checks a given node is a MemberExpression node which has the specified name's * property. @@ -62,10 +30,7 @@ function isConstant(node, name) { * the specified name's property */ function isMember(node, name) { - return ( - node.type === "MemberExpression" && - (node.computed ? isConstant : isIdentifier)(node.property, name) - ); + return astUtils.isSpecificMemberAccess(node, null, name); } //------------------------------------------------------------------------------ @@ -230,7 +195,12 @@ module.exports = { "CallExpression:exit"(node) { const callee = node.callee; - if (isIdentifier(callee, "eval")) { + /* + * Optional call (`eval?.("code")`) is not direct eval. + * The direct eval is only step 6.a.vi of https://tc39.es/ecma262/#sec-function-calls-runtime-semantics-evaluation + * But the optional call is https://tc39.es/ecma262/#sec-optional-chaining-chain-evaluation + */ + if (!node.optional && astUtils.isSpecificId(callee, "eval")) { report(callee); } } @@ -241,7 +211,7 @@ module.exports = { "CallExpression:exit"(node) { const callee = node.callee; - if (isIdentifier(callee, "eval")) { + if (astUtils.isSpecificId(callee, "eval")) { report(callee); } }, diff --git a/lib/rules/no-extend-native.js b/lib/rules/no-extend-native.js index 7ab25ab4895..db365b50924 100644 --- a/lib/rules/no-extend-native.js +++ b/lib/rules/no-extend-native.js @@ -12,12 +12,6 @@ const astUtils = require("./utils/ast-utils"); const globals = require("globals"); -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -const propertyDefinitionMethods = new Set(["defineProperty", "defineProperties"]); - //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -100,40 +94,30 @@ module.exports = { } /** - * Checks that an identifier is an object of a prototype whose member - * is being assigned in an AssignmentExpression. - * Example: Object.prototype.foo = "bar" - * @param {ASTNode} identifierNode The identifier to check. - * @returns {boolean} True if the identifier's prototype is modified. + * Check if it's an assignment to the property of the given node. + * Example: `*.prop = 0` // the `*` is the given node. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if an assignment to the property of the node. */ - function isInPrototypePropertyAssignment(identifierNode) { - return Boolean( - isPrototypePropertyAccessed(identifierNode) && - identifierNode.parent.parent.type === "MemberExpression" && - identifierNode.parent.parent.parent.type === "AssignmentExpression" && - identifierNode.parent.parent.parent.left === identifierNode.parent.parent + function isAssigningToPropertyOf(node) { + return ( + node.parent.type === "MemberExpression" && + node.parent.object === node && + node.parent.parent.type === "AssignmentExpression" && + node.parent.parent.left === node.parent ); } /** - * Checks that an identifier is an object of a prototype whose member - * is being extended via the Object.defineProperty() or - * Object.defineProperties() methods. - * Example: Object.defineProperty(Array.prototype, "foo", ...) - * Example: Object.defineProperties(Array.prototype, ...) - * @param {ASTNode} identifierNode The identifier to check. - * @returns {boolean} True if the identifier's prototype is modified. + * Checks if the given node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`. + * @param {ASTNode} node The node to check. + * @returns {boolean} True if the node is at the first argument of the method call of `Object.defineProperty()` or `Object.defineProperties()`. */ - function isInDefinePropertyCall(identifierNode) { - return Boolean( - isPrototypePropertyAccessed(identifierNode) && - identifierNode.parent.parent.type === "CallExpression" && - identifierNode.parent.parent.arguments[0] === identifierNode.parent && - identifierNode.parent.parent.callee.type === "MemberExpression" && - identifierNode.parent.parent.callee.object.type === "Identifier" && - identifierNode.parent.parent.callee.object.name === "Object" && - identifierNode.parent.parent.callee.property.type === "Identifier" && - propertyDefinitionMethods.has(identifierNode.parent.parent.callee.property.name) + function isInDefinePropertyCall(node) { + return ( + node.parent.type === "CallExpression" && + node.parent.arguments[0] === node && + astUtils.isSpecificMemberAccess(node.parent.callee, "Object", /^definePropert(?:y|ies)$/u) ); } @@ -149,14 +133,27 @@ module.exports = { * @returns {void} */ function checkAndReportPrototypeExtension(identifierNode) { - if (isInPrototypePropertyAssignment(identifierNode)) { + if (!isPrototypePropertyAccessed(identifierNode)) { + return; // This is not `*.prototype` access. + } + + /* + * `identifierNode.parent` is a MamberExpression `*.prototype`. + * If it's an optional member access, it may be wrapped by a `ChainExpression` node. + */ + const prototypeNode = + identifierNode.parent.parent.type === "ChainExpression" + ? identifierNode.parent.parent + : identifierNode.parent; + + if (isAssigningToPropertyOf(prototypeNode)) { - // Identifier --> MemberExpression --> MemberExpression --> AssignmentExpression - reportNode(identifierNode.parent.parent.parent, identifierNode.name); - } else if (isInDefinePropertyCall(identifierNode)) { + // `*.prototype` -> MemberExpression -> AssignmentExpression + reportNode(prototypeNode.parent.parent, identifierNode.name); + } else if (isInDefinePropertyCall(prototypeNode)) { - // Identifier --> MemberExpression --> CallExpression - reportNode(identifierNode.parent.parent, identifierNode.name); + // `*.prototype` -> CallExpression + reportNode(prototypeNode.parent, identifierNode.name); } } diff --git a/lib/rules/no-extra-bind.js b/lib/rules/no-extra-bind.js index df695924ab5..2db440dc1ea 100644 --- a/lib/rules/no-extra-bind.js +++ b/lib/rules/no-extra-bind.js @@ -61,24 +61,62 @@ module.exports = { * @returns {void} */ function report(node) { + const memberNode = node.parent; + const callNode = memberNode.parent.type === "ChainExpression" + ? memberNode.parent.parent + : memberNode.parent; + context.report({ - node: node.parent.parent, + node: callNode, messageId: "unexpected", - loc: node.parent.property.loc, + loc: memberNode.property.loc, + fix(fixer) { - if (node.parent.parent.arguments.length && !isSideEffectFree(node.parent.parent.arguments[0])) { + if (!isSideEffectFree(callNode.arguments[0])) { return null; } - const firstTokenToRemove = sourceCode - .getFirstTokenBetween(node.parent.object, node.parent.property, astUtils.isNotClosingParenToken); - const lastTokenToRemove = sourceCode.getLastToken(node.parent.parent); + /* + * The list of the first/last token pair of a removal range. + * This is two parts because closing parentheses may exist between the method name and arguments. + * E.g. `(function(){}.bind ) (obj)` + * ^^^^^ ^^^^^ < removal ranges + * E.g. `(function(){}?.['bind'] ) ?.(obj)` + * ^^^^^^^^^^ ^^^^^^^ < removal ranges + */ + const tokenPairs = [ + [ + + // `.`, `?.`, or `[` token. + sourceCode.getTokenAfter( + memberNode.object, + astUtils.isNotClosingParenToken + ), + + // property name or `]` token. + sourceCode.getLastToken(memberNode) + ], + [ + + // `?.` or `(` token of arguments. + sourceCode.getTokenAfter( + memberNode, + astUtils.isNotClosingParenToken + ), + + // `)` token of arguments. + sourceCode.getLastToken(callNode) + ] + ]; + const firstTokenToRemove = tokenPairs[0][0]; + const lastTokenToRemove = tokenPairs[1][1]; if (sourceCode.commentsExistBetween(firstTokenToRemove, lastTokenToRemove)) { return null; } - return fixer.removeRange([firstTokenToRemove.range[0], node.parent.parent.range[1]]); + return tokenPairs.map(([start, end]) => + fixer.removeRange([start.range[0], end.range[1]])); } }); } @@ -93,18 +131,20 @@ module.exports = { * @returns {boolean} `true` if the node is the callee of `.bind()` method. */ function isCalleeOfBindMethod(node) { - const parent = node.parent; - const grandparent = parent.parent; + if (!astUtils.isSpecificMemberAccess(node.parent, null, "bind")) { + return false; + } + + // The node of `*.bind` member access. + const bindNode = node.parent.parent.type === "ChainExpression" + ? node.parent.parent + : node.parent; return ( - grandparent && - grandparent.type === "CallExpression" && - grandparent.callee === parent && - grandparent.arguments.length === 1 && - grandparent.arguments[0].type !== "SpreadElement" && - parent.type === "MemberExpression" && - parent.object === node && - astUtils.getStaticPropertyName(parent) === "bind" + bindNode.parent.type === "CallExpression" && + bindNode.parent.callee === bindNode && + bindNode.parent.arguments.length === 1 && + bindNode.parent.arguments[0].type !== "SpreadElement" ); } diff --git a/lib/rules/no-extra-boolean-cast.js b/lib/rules/no-extra-boolean-cast.js index b90757b1126..6ae3ea62ca7 100644 --- a/lib/rules/no-extra-boolean-cast.js +++ b/lib/rules/no-extra-boolean-cast.js @@ -111,6 +111,10 @@ module.exports = { * @returns {boolean} If the node is in one of the flagged contexts */ function isInFlaggedContext(node) { + if (node.parent.type === "ChainExpression") { + return isInFlaggedContext(node.parent); + } + return isInBooleanContext(node) || (isLogicalContext(node.parent) && @@ -149,6 +153,9 @@ module.exports = { * @returns {boolean} `true` if the node needs to be parenthesized. */ function needsParens(previousNode, node) { + if (previousNode.parent.type === "ChainExpression") { + return needsParens(previousNode.parent, node); + } if (isParenthesized(previousNode)) { // parentheses around the previous node will stay, so there is no need for an additional pair diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js index 1ece81eee81..e9d394c616a 100644 --- a/lib/rules/no-extra-parens.js +++ b/lib/rules/no-extra-parens.js @@ -100,10 +100,18 @@ module.exports = { * @private */ function isImmediateFunctionPrototypeMethodCall(node) { - return node.type === "CallExpression" && - node.callee.type === "MemberExpression" && - node.callee.object.type === "FunctionExpression" && - ["call", "apply"].includes(astUtils.getStaticPropertyName(node.callee)); + const callNode = astUtils.skipChainExpression(node); + + if (callNode.type !== "CallExpression") { + return false; + } + const callee = astUtils.skipChainExpression(callNode.callee); + + return ( + callee.type === "MemberExpression" && + callee.object.type === "FunctionExpression" && + ["call", "apply"].includes(astUtils.getStaticPropertyName(callee)) + ); } /** @@ -360,7 +368,9 @@ module.exports = { * @returns {boolean} `true` if the given node is an IIFE */ function isIIFE(node) { - return node.type === "CallExpression" && node.callee.type === "FunctionExpression"; + const maybeCallNode = astUtils.skipChainExpression(node); + + return maybeCallNode.type === "CallExpression" && maybeCallNode.callee.type === "FunctionExpression"; } /** @@ -466,13 +476,16 @@ module.exports = { if ( hasDoubleExcessParens(callee) || - !isIIFE(node) && !hasNewParensException && !( + !isIIFE(node) && + !hasNewParensException && + !( // Allow extra parens around a new expression if they are intervening parentheses. node.type === "NewExpression" && callee.type === "MemberExpression" && doesMemberExpressionContainCallExpression(callee) - ) + ) && + !(!node.optional && callee.type === "ChainExpression") ) { report(node.callee); } @@ -1004,6 +1017,13 @@ module.exports = { report(node.object); } + if (nodeObjHasExcessParens && + node.optional && + node.object.type === "ChainExpression" + ) { + report(node.object); + } + if (node.computed && hasExcessParens(node.property)) { report(node.property); } diff --git a/lib/rules/no-implicit-coercion.js b/lib/rules/no-implicit-coercion.js index 6d5ee61e96b..a639711ecea 100644 --- a/lib/rules/no-implicit-coercion.js +++ b/lib/rules/no-implicit-coercion.js @@ -47,12 +47,14 @@ function isDoubleLogicalNegating(node) { * @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling. */ function isBinaryNegatingOfIndexOf(node) { + if (node.operator !== "~") { + return false; + } + const callNode = astUtils.skipChainExpression(node.argument); + return ( - node.operator === "~" && - node.argument.type === "CallExpression" && - node.argument.callee.type === "MemberExpression" && - node.argument.callee.property.type === "Identifier" && - INDEX_OF_PATTERN.test(node.argument.callee.property.name) + callNode.type === "CallExpression" && + astUtils.isSpecificMemberAccess(callNode.callee, null, INDEX_OF_PATTERN) ); } @@ -246,7 +248,10 @@ module.exports = { // ~foo.indexOf(bar) operatorAllowed = options.allow.indexOf("~") >= 0; if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) { - const recommendation = `${sourceCode.getText(node.argument)} !== -1`; + + // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case. + const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1"; + const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`; report(node, recommendation, false); } diff --git a/lib/rules/no-implied-eval.js b/lib/rules/no-implied-eval.js index 1668a0432a5..b8120a64887 100644 --- a/lib/rules/no-implied-eval.js +++ b/lib/rules/no-implied-eval.js @@ -35,8 +35,8 @@ module.exports = { }, create(context) { - const EVAL_LIKE_FUNCS = Object.freeze(["setTimeout", "execScript", "setInterval"]); const GLOBAL_CANDIDATES = Object.freeze(["global", "window", "globalThis"]); + const EVAL_LIKE_FUNC_PATTERN = /^(?:set(?:Interval|Timeout)|execScript)$/u; /** * Checks whether a node is evaluated as a string or not. @@ -56,28 +56,6 @@ module.exports = { return false; } - /** - * Checks whether a node is an Identifier node named one of the specified names. - * @param {ASTNode} node A node to check. - * @param {string[]} specifiers Array of specified name. - * @returns {boolean} True if the node is a Identifier node which has specified name. - */ - function isSpecifiedIdentifier(node, specifiers) { - return node.type === "Identifier" && specifiers.includes(node.name); - } - - /** - * Checks a given node is a MemberExpression node which has the specified name's - * property. - * @param {ASTNode} node A node to check. - * @param {string[]} specifiers Array of specified name. - * @returns {boolean} `true` if the node is a MemberExpression node which has - * the specified name's property - */ - function isSpecifiedMember(node, specifiers) { - return node.type === "MemberExpression" && specifiers.includes(astUtils.getStaticPropertyName(node)); - } - /** * Reports if the `CallExpression` node has evaluated argument. * @param {ASTNode} node A CallExpression to check. @@ -114,14 +92,15 @@ module.exports = { const identifier = ref.identifier; let node = identifier.parent; - while (isSpecifiedMember(node, [name])) { + while (astUtils.isSpecificMemberAccess(node, null, name)) { node = node.parent; } - if (isSpecifiedMember(node, EVAL_LIKE_FUNCS)) { - const parent = node.parent; + if (astUtils.isSpecificMemberAccess(node, null, EVAL_LIKE_FUNC_PATTERN)) { + const calleeNode = node.parent.type === "ChainExpression" ? node.parent : node; + const parent = calleeNode.parent; - if (parent.type === "CallExpression" && parent.callee === node) { + if (parent.type === "CallExpression" && parent.callee === calleeNode) { reportImpliedEvalCallExpression(parent); } } @@ -134,7 +113,7 @@ module.exports = { return { CallExpression(node) { - if (isSpecifiedIdentifier(node.callee, EVAL_LIKE_FUNCS)) { + if (astUtils.isSpecificId(node.callee, EVAL_LIKE_FUNC_PATTERN)) { reportImpliedEvalCallExpression(node); } }, diff --git a/lib/rules/no-import-assign.js b/lib/rules/no-import-assign.js index 32e445ff68b..7a349bb730b 100644 --- a/lib/rules/no-import-assign.js +++ b/lib/rules/no-import-assign.js @@ -9,16 +9,12 @@ // Helpers //------------------------------------------------------------------------------ -const { findVariable, getPropertyName } = require("eslint-utils"); - -const MutationMethods = { - Object: new Set([ - "assign", "defineProperties", "defineProperty", "freeze", - "setPrototypeOf" - ]), - Reflect: new Set([ - "defineProperty", "deleteProperty", "set", "setPrototypeOf" - ]) +const { findVariable } = require("eslint-utils"); +const astUtils = require("./utils/ast-utils"); + +const WellKnownMutationFunctions = { + Object: /^(?:assign|definePropert(?:y|ies)|freeze|setPrototypeOf)$/u, + Reflect: /^(?:(?:define|delete)Property|set(?:PrototypeOf)?)$/u }; /** @@ -56,17 +52,20 @@ function isAssignmentLeft(node) { * @returns {boolean} `true` if the node is the operand of mutation unary operator. */ function isOperandOfMutationUnaryOperator(node) { - const { parent } = node; + const argumentNode = node.parent.type === "ChainExpression" + ? node.parent + : node; + const { parent } = argumentNode; return ( ( parent.type === "UpdateExpression" && - parent.argument === node + parent.argument === argumentNode ) || ( parent.type === "UnaryExpression" && parent.operator === "delete" && - parent.argument === node + parent.argument === argumentNode ) ); } @@ -92,35 +91,37 @@ function isIterationVariable(node) { } /** - * Check if a given node is the iteration variable of `for-in`/`for-of` syntax. + * Check if a given node is at the first argument of a well-known mutation function. + * - `Object.assign` + * - `Object.defineProperty` + * - `Object.defineProperties` + * - `Object.freeze` + * - `Object.setPrototypeOf` + * - `Refrect.defineProperty` + * - `Refrect.deleteProperty` + * - `Refrect.set` + * - `Refrect.setPrototypeOf` * @param {ASTNode} node The node to check. * @param {Scope} scope A `escope.Scope` object to find variable (whichever). - * @returns {boolean} `true` if the node is the iteration variable. + * @returns {boolean} `true` if the node is at the first argument of a well-known mutation function. */ function isArgumentOfWellKnownMutationFunction(node, scope) { const { parent } = node; + if (parent.type !== "CallExpression" || parent.arguments[0] !== node) { + return false; + } + const callee = astUtils.skipChainExpression(parent.callee); + if ( - parent.type === "CallExpression" && - parent.arguments[0] === node && - parent.callee.type === "MemberExpression" && - parent.callee.object.type === "Identifier" + !astUtils.isSpecificMemberAccess(callee, "Object", WellKnownMutationFunctions.Object) && + !astUtils.isSpecificMemberAccess(callee, "Reflect", WellKnownMutationFunctions.Reflect) ) { - const { callee } = parent; - const { object } = callee; - - if (Object.keys(MutationMethods).includes(object.name)) { - const variable = findVariable(scope, object); - - return ( - variable !== null && - variable.scope.type === "global" && - MutationMethods[object.name].has(getPropertyName(callee, scope)) - ); - } + return false; } + const variable = findVariable(scope, callee.object); - return false; + return variable !== null && variable.scope.type === "global"; } /** diff --git a/lib/rules/no-magic-numbers.js b/lib/rules/no-magic-numbers.js index cd07f5c3bda..6f6a156eb75 100644 --- a/lib/rules/no-magic-numbers.js +++ b/lib/rules/no-magic-numbers.js @@ -5,7 +5,7 @@ "use strict"; -const { isNumericLiteral } = require("./utils/ast-utils"); +const astUtils = require("./utils/ast-utils"); // Maximum array length by the ECMAScript Specification. const MAX_ARRAY_LENGTH = 2 ** 32 - 1; @@ -100,12 +100,8 @@ module.exports = { return parent.type === "CallExpression" && fullNumberNode === parent.arguments[1] && ( - parent.callee.name === "parseInt" || - ( - parent.callee.type === "MemberExpression" && - parent.callee.object.name === "Number" && - parent.callee.property.name === "parseInt" - ) + astUtils.isSpecificId(parent.callee, "parseInt") || + astUtils.isSpecificMemberAccess(parent.callee, "Number", "parseInt") ); } @@ -157,7 +153,7 @@ module.exports = { return { Literal(node) { - if (!isNumericLiteral(node)) { + if (!astUtils.isNumericLiteral(node)) { return; } diff --git a/lib/rules/no-obj-calls.js b/lib/rules/no-obj-calls.js index 6139ba2c182..6eb200c9b87 100644 --- a/lib/rules/no-obj-calls.js +++ b/lib/rules/no-obj-calls.js @@ -24,10 +24,13 @@ const nonCallableGlobals = ["Atomics", "JSON", "Math", "Reflect"]; * @returns {string} name to report */ function getReportNodeName(node) { - if (node.callee.type === "MemberExpression") { - return getPropertyName(node.callee); + if (node.type === "ChainExpression") { + return getReportNodeName(node.expression); } - return node.callee.name; + if (node.type === "MemberExpression") { + return getPropertyName(node); + } + return node.name; } //------------------------------------------------------------------------------ @@ -69,7 +72,7 @@ module.exports = { } for (const { node, path } of tracker.iterateGlobalReferences(traceMap)) { - const name = getReportNodeName(node); + const name = getReportNodeName(node.callee); const ref = path[0]; const messageId = name === ref ? "unexpectedCall" : "unexpectedRefCall"; diff --git a/lib/rules/no-prototype-builtins.js b/lib/rules/no-prototype-builtins.js index a00d3707204..ccec86c30da 100644 --- a/lib/rules/no-prototype-builtins.js +++ b/lib/rules/no-prototype-builtins.js @@ -4,6 +4,12 @@ */ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -39,15 +45,19 @@ module.exports = { * @returns {void} */ function disallowBuiltIns(node) { - if (node.callee.type !== "MemberExpression" || node.callee.computed) { + + // TODO: just use `astUtils.getStaticPropertyName(node.callee)` + const callee = astUtils.skipChainExpression(node.callee); + + if (callee.type !== "MemberExpression" || callee.computed) { return; } - const propName = node.callee.property.name; + const propName = callee.property.name; if (DISALLOWED_PROPS.indexOf(propName) > -1) { context.report({ messageId: "prototypeBuildIn", - loc: node.callee.property.loc, + loc: callee.property.loc, data: { prop: propName }, node }); diff --git a/lib/rules/no-self-assign.js b/lib/rules/no-self-assign.js index 170e46b0593..705be324cf0 100644 --- a/lib/rules/no-self-assign.js +++ b/lib/rules/no-self-assign.js @@ -17,56 +17,6 @@ const astUtils = require("./utils/ast-utils"); const SPACES = /\s+/gu; -/** - * Checks whether the property of 2 given member expression nodes are the same - * property or not. - * @param {ASTNode} left A member expression node to check. - * @param {ASTNode} right Another member expression node to check. - * @returns {boolean} `true` if the member expressions have the same property. - */ -function isSameProperty(left, right) { - if (left.property.type === "Identifier" && - left.property.type === right.property.type && - left.property.name === right.property.name && - left.computed === right.computed - ) { - return true; - } - - const lname = astUtils.getStaticPropertyName(left); - const rname = astUtils.getStaticPropertyName(right); - - return lname !== null && lname === rname; -} - -/** - * Checks whether 2 given member expression nodes are the reference to the same - * property or not. - * @param {ASTNode} left A member expression node to check. - * @param {ASTNode} right Another member expression node to check. - * @returns {boolean} `true` if the member expressions are the reference to the - * same property or not. - */ -function isSameMember(left, right) { - if (!isSameProperty(left, right)) { - return false; - } - - const lobj = left.object; - const robj = right.object; - - if (lobj.type !== robj.type) { - return false; - } - if (lobj.type === "MemberExpression") { - return isSameMember(lobj, robj); - } - if (lobj.type === "ThisExpression") { - return true; - } - return lobj.type === "Identifier" && lobj.name === robj.name; -} - /** * Traverses 2 Pattern nodes in parallel, then reports self-assignments. * @param {ASTNode|null} left A left node to traverse. This is a Pattern or @@ -162,9 +112,9 @@ function eachSelfAssignment(left, right, props, report) { } } else if ( props && - left.type === "MemberExpression" && - right.type === "MemberExpression" && - isSameMember(left, right) + astUtils.skipChainExpression(left).type === "MemberExpression" && + astUtils.skipChainExpression(right).type === "MemberExpression" && + astUtils.isSameReference(left, right) ) { report(right); } diff --git a/lib/rules/no-setter-return.js b/lib/rules/no-setter-return.js index a558640c357..9c79240dda1 100644 --- a/lib/rules/no-setter-return.js +++ b/lib/rules/no-setter-return.js @@ -39,15 +39,12 @@ function isGlobalReference(node, scope) { * @returns {boolean} `true` if the node is argument at the given position. */ function isArgumentOfGlobalMethodCall(node, scope, objectName, methodName, index) { - const parent = node.parent; + const callNode = node.parent; - return parent.type === "CallExpression" && - parent.arguments[index] === node && - parent.callee.type === "MemberExpression" && - astUtils.getStaticPropertyName(parent.callee) === methodName && - parent.callee.object.type === "Identifier" && - parent.callee.object.name === objectName && - isGlobalReference(parent.callee.object, scope); + return callNode.type === "CallExpression" && + callNode.arguments[index] === node && + astUtils.isSpecificMemberAccess(callNode.callee, objectName, methodName) && + isGlobalReference(astUtils.skipChainExpression(callNode.callee).object, scope); } /** diff --git a/lib/rules/no-unexpected-multiline.js b/lib/rules/no-unexpected-multiline.js index b5ec20de4b2..7af3fe67090 100644 --- a/lib/rules/no-unexpected-multiline.js +++ b/lib/rules/no-unexpected-multiline.js @@ -68,7 +68,7 @@ module.exports = { return { MemberExpression(node) { - if (!node.computed) { + if (!node.computed || node.optional) { return; } checkForBreakAfter(node.object, "property"); @@ -96,7 +96,7 @@ module.exports = { }, CallExpression(node) { - if (node.arguments.length === 0) { + if (node.arguments.length === 0 || node.optional) { return; } checkForBreakAfter(node.callee, "function"); diff --git a/lib/rules/no-unused-expressions.js b/lib/rules/no-unused-expressions.js index 8c049f556ff..882a0fd1c11 100644 --- a/lib/rules/no-unused-expressions.js +++ b/lib/rules/no-unused-expressions.js @@ -8,6 +8,22 @@ // Rule Definition //------------------------------------------------------------------------------ +/** + * Returns `true`. + * @returns {boolean} `true`. + */ +function alwaysTrue() { + return true; +} + +/** + * Returns `false`. + * @returns {boolean} `false`. + */ +function alwaysFalse() { + return false; +} + module.exports = { meta: { type: "suggestion", @@ -101,40 +117,56 @@ module.exports = { } /** - * Determines whether or not a given node is a valid expression. Recurses on short circuit eval and ternary nodes if enabled by flags. - * @param {ASTNode} node any node - * @returns {boolean} whether the given node is a valid expression + * The member functions return `true` if the type has no side-effects. + * Unknown nodes are handled as `false`, then this rule ignores those. */ - function isValidExpression(node) { - if (allowTernary) { - - // Recursive check for ternary and logical expressions - if (node.type === "ConditionalExpression") { - return isValidExpression(node.consequent) && isValidExpression(node.alternate); + const Checker = Object.assign(Object.create(null), { + isDisallowed(node) { + return (Checker[node.type] || alwaysFalse)(node); + }, + + ArrayExpression: alwaysTrue, + ArrowFunctionExpression: alwaysTrue, + BinaryExpression: alwaysTrue, + ChainExpression(node) { + return Checker.isDisallowed(node.expression); + }, + ClassExpression: alwaysTrue, + ConditionalExpression(node) { + if (allowTernary) { + return Checker.isDisallowed(node.consequent) || Checker.isDisallowed(node.alternate); } - } - - if (allowShortCircuit) { - if (node.type === "LogicalExpression") { - return isValidExpression(node.right); + return true; + }, + FunctionExpression: alwaysTrue, + Identifier: alwaysTrue, + Literal: alwaysTrue, + LogicalExpression(node) { + if (allowShortCircuit) { + return Checker.isDisallowed(node.right); } - } - - if (allowTaggedTemplates && node.type === "TaggedTemplateExpression") { return true; + }, + MemberExpression: alwaysTrue, + MetaProperty: alwaysTrue, + ObjectExpression: alwaysTrue, + SequenceExpression: alwaysTrue, + TaggedTemplateExpression() { + return !allowTaggedTemplates; + }, + TemplateLiteral: alwaysTrue, + ThisExpression: alwaysTrue, + UnaryExpression(node) { + return node.operator !== "void" && node.operator !== "delete"; } - - return /^(?:Assignment|Call|New|Update|Yield|Await|Import)Expression$/u.test(node.type) || - (node.type === "UnaryExpression" && ["delete", "void"].indexOf(node.operator) >= 0); - } + }); return { ExpressionStatement(node) { - if (!isValidExpression(node.expression) && !isDirective(node, context.getAncestors())) { + if (Checker.isDisallowed(node.expression) && !isDirective(node, context.getAncestors())) { context.report({ node, messageId: "unusedExpression" }); } } }; - } }; diff --git a/lib/rules/no-useless-call.js b/lib/rules/no-useless-call.js index afc729d5de0..b1382a2fa28 100644 --- a/lib/rules/no-useless-call.js +++ b/lib/rules/no-useless-call.js @@ -17,13 +17,15 @@ const astUtils = require("./utils/ast-utils"); * @returns {boolean} Whether or not the node is a `.call()`/`.apply()`. */ function isCallOrNonVariadicApply(node) { + const callee = astUtils.skipChainExpression(node.callee); + return ( - node.callee.type === "MemberExpression" && - node.callee.property.type === "Identifier" && - node.callee.computed === false && + callee.type === "MemberExpression" && + callee.property.type === "Identifier" && + callee.computed === false && ( - (node.callee.property.name === "call" && node.arguments.length >= 1) || - (node.callee.property.name === "apply" && node.arguments.length === 2 && node.arguments[1].type === "ArrayExpression") + (callee.property.name === "call" && node.arguments.length >= 1) || + (callee.property.name === "apply" && node.arguments.length === 2 && node.arguments[1].type === "ArrayExpression") ) ); } @@ -74,12 +76,13 @@ module.exports = { return; } - const applied = node.callee.object; + const callee = astUtils.skipChainExpression(node.callee); + const applied = astUtils.skipChainExpression(callee.object); const expectedThis = (applied.type === "MemberExpression") ? applied.object : null; const thisArg = node.arguments[0]; if (isValidThisArg(expectedThis, thisArg, sourceCode)) { - context.report({ node, messageId: "unnecessaryCall", data: { name: node.callee.property.name } }); + context.report({ node, messageId: "unnecessaryCall", data: { name: callee.property.name } }); } } }; diff --git a/lib/rules/no-whitespace-before-property.js b/lib/rules/no-whitespace-before-property.js index ccd0b091b74..226f873c5f6 100644 --- a/lib/rules/no-whitespace-before-property.js +++ b/lib/rules/no-whitespace-before-property.js @@ -49,8 +49,6 @@ module.exports = { * @private */ function reportError(node, leftToken, rightToken) { - const replacementText = node.computed ? "" : "."; - context.report({ node, messageId: "unexpectedWhitespace", @@ -58,7 +56,9 @@ module.exports = { propName: sourceCode.getText(node.property) }, fix(fixer) { - if (!node.computed && astUtils.isDecimalInteger(node.object)) { + let replacementText = ""; + + if (!node.computed && !node.optional && astUtils.isDecimalInteger(node.object)) { /* * If the object is a number literal, fixing it to something like 5.toString() would cause a SyntaxError. @@ -66,6 +66,18 @@ module.exports = { */ return null; } + + // Don't fix if comments exist. + if (sourceCode.commentsExistBetween(leftToken, rightToken)) { + return null; + } + + if (node.optional) { + replacementText = "?."; + } else if (!node.computed) { + replacementText = "."; + } + return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], replacementText); } }); @@ -86,7 +98,7 @@ module.exports = { if (node.computed) { rightToken = sourceCode.getTokenBefore(node.property, astUtils.isOpeningBracketToken); - leftToken = sourceCode.getTokenBefore(rightToken); + leftToken = sourceCode.getTokenBefore(rightToken, node.optional ? 1 : 0); } else { rightToken = sourceCode.getFirstToken(node.property); leftToken = sourceCode.getTokenBefore(rightToken, 1); diff --git a/lib/rules/operator-assignment.js b/lib/rules/operator-assignment.js index 6820793439c..aee79077f44 100644 --- a/lib/rules/operator-assignment.js +++ b/lib/rules/operator-assignment.js @@ -40,45 +40,6 @@ function isNonCommutativeOperatorWithShorthand(operator) { // Rule Definition //------------------------------------------------------------------------------ -/** - * Checks whether two expressions reference the same value. For example: - * a = a - * a.b = a.b - * a[0] = a[0] - * a['b'] = a['b'] - * @param {ASTNode} a Left side of the comparison. - * @param {ASTNode} b Right side of the comparison. - * @returns {boolean} True if both sides match and reference the same value. - */ -function same(a, b) { - if (a.type !== b.type) { - return false; - } - - switch (a.type) { - case "Identifier": - return a.name === b.name; - - case "Literal": - return a.value === b.value; - - case "MemberExpression": - - /* - * x[0] = x[0] - * x[y] = x[y] - * x.y = x.y - */ - return same(a.object, b.object) && same(a.property, b.property); - - case "ThisExpression": - return true; - - default: - return false; - } -} - /** * Determines if the left side of a node can be safely fixed (i.e. if it activates the same getters/setters and) * toString calls regardless of whether assignment shorthand is used) @@ -148,12 +109,12 @@ module.exports = { const operator = expr.operator; if (isCommutativeOperatorWithShorthand(operator) || isNonCommutativeOperatorWithShorthand(operator)) { - if (same(left, expr.left)) { + if (astUtils.isSameReference(left, expr.left, true)) { context.report({ node, messageId: "replaced", fix(fixer) { - if (canBeFixed(left)) { + if (canBeFixed(left) && canBeFixed(expr.left)) { const equalsToken = getOperatorToken(node); const operatorToken = getOperatorToken(expr); const leftText = sourceCode.getText().slice(node.range[0], equalsToken.range[0]); @@ -169,7 +130,7 @@ module.exports = { return null; } }); - } else if (same(left, expr.right) && isCommutativeOperatorWithShorthand(operator)) { + } else if (astUtils.isSameReference(left, expr.right, true) && isCommutativeOperatorWithShorthand(operator)) { /* * This case can't be fixed safely. diff --git a/lib/rules/padding-line-between-statements.js b/lib/rules/padding-line-between-statements.js index eea19f5ce58..c97b9956b71 100644 --- a/lib/rules/padding-line-between-statements.js +++ b/lib/rules/padding-line-between-statements.js @@ -85,10 +85,10 @@ function newNodeTypeTester(type) { */ function isIIFEStatement(node) { if (node.type === "ExpressionStatement") { - let call = node.expression; + let call = astUtils.skipChainExpression(node.expression); if (call.type === "UnaryExpression") { - call = call.argument; + call = astUtils.skipChainExpression(call.argument); } return call.type === "CallExpression" && astUtils.isFunction(call.callee); } diff --git a/lib/rules/prefer-arrow-callback.js b/lib/rules/prefer-arrow-callback.js index d4e0251940c..ee5cfe3c8c7 100644 --- a/lib/rules/prefer-arrow-callback.js +++ b/lib/rules/prefer-arrow-callback.js @@ -5,6 +5,8 @@ "use strict"; +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ @@ -66,6 +68,7 @@ function getCallbackInfo(node) { const retv = { isCallback: false, isLexicalThis: false }; let currentNode = node; let parent = node.parent; + let bound = false; while (currentNode) { switch (parent.type) { @@ -73,23 +76,34 @@ function getCallbackInfo(node) { // Checks parents recursively. case "LogicalExpression": + case "ChainExpression": case "ConditionalExpression": break; // Checks whether the parent node is `.bind(this)` call. case "MemberExpression": - if (parent.object === currentNode && + if ( + parent.object === currentNode && !parent.property.computed && parent.property.type === "Identifier" && - parent.property.name === "bind" && - parent.parent.type === "CallExpression" && - parent.parent.callee === parent + parent.property.name === "bind" ) { - retv.isLexicalThis = ( - parent.parent.arguments.length === 1 && - parent.parent.arguments[0].type === "ThisExpression" - ); - parent = parent.parent; + const maybeCallee = parent.parent.type === "ChainExpression" + ? parent.parent + : parent; + + if (astUtils.isCallee(maybeCallee)) { + if (!bound) { + bound = true; // Use only the first `.bind()` to make `isLexicalThis` value. + retv.isLexicalThis = ( + maybeCallee.parent.arguments.length === 1 && + maybeCallee.parent.arguments[0].type === "ThisExpression" + ); + } + parent = maybeCallee.parent; + } else { + return retv; + } } else { return retv; } @@ -272,7 +286,7 @@ module.exports = { context.report({ node, messageId: "preferArrowCallback", - fix(fixer) { + *fix(fixer) { if ((!callbackInfo.isLexicalThis && scopeInfo.this) || hasDuplicateParams(node.params)) { /* @@ -281,30 +295,81 @@ 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 null; + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive } - const paramsLeftParen = node.params.length ? sourceCode.getTokenBefore(node.params[0]) : sourceCode.getTokenBefore(node.body, 1); - const paramsRightParen = sourceCode.getTokenBefore(node.body); - const asyncKeyword = node.async ? "async " : ""; - const paramsFullText = sourceCode.text.slice(paramsLeftParen.range[0], paramsRightParen.range[1]); - const arrowFunctionText = `${asyncKeyword}${paramsFullText} => ${sourceCode.getText(node.body)}`; + // Remove `.bind(this)` if exists. + if (callbackInfo.isLexicalThis) { + const memberNode = node.parent; - /* - * If the callback function has `.bind(this)`, replace it with an arrow function and remove the binding. - * Otherwise, just replace the arrow function itself. - */ - const replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node; + /* + * If `.bind(this)` exists but the parent is not `.bind(this)`, don't remove it automatically. + * E.g. `(foo || function(){}).bind(this)` + */ + if (memberNode.type !== "MemberExpression") { + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + } + + const callNode = memberNode.parent; + const firstTokenToRemove = sourceCode.getTokenAfter(memberNode.object, astUtils.isNotClosingParenToken); + const lastTokenToRemove = sourceCode.getLastToken(callNode); + + /* + * If the member expression is parenthesized, don't remove the right paren. + * E.g. `(function(){}.bind)(this)` + * ^^^^^^^^^^^^ + */ + if (astUtils.isParenthesised(sourceCode, memberNode)) { + return; // eslint-disable-line eslint-plugin/fixer-return -- false positive + } + + // 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 + } + + yield fixer.removeRange([firstTokenToRemove.range[0], lastTokenToRemove.range[1]]); + } + + // Convert the function expression to an arrow function. + const functionToken = sourceCode.getFirstToken(node, node.async ? 1 : 0); + const leftParenToken = sourceCode.getTokenAfter(functionToken, astUtils.isOpeningParenToken); + + if (sourceCode.commentsExistBetween(functionToken, leftParenToken)) { + + // Remove only extra tokens to keep comments. + yield fixer.remove(functionToken); + if (node.id) { + yield fixer.remove(node.id); + } + } else { + + // Remove extra tokens and spaces. + yield fixer.removeRange([functionToken.range[0], leftParenToken.range[0]]); + } + yield fixer.insertTextBefore(node.body, "=> "); + + // Get the node that will become the new arrow function. + let replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node; + + if (replacedNode.type === "ChainExpression") { + replacedNode = replacedNode.parent; + } /* * If the replaced node is part of a BinaryExpression, LogicalExpression, or MemberExpression, then * the arrow function needs to be parenthesized, because `foo || () => {}` is invalid syntax even * though `foo || function() {}` is valid. */ - const needsParens = replacedNode.parent.type !== "CallExpression" && replacedNode.parent.type !== "ConditionalExpression"; - const replacementText = needsParens ? `(${arrowFunctionText})` : arrowFunctionText; - - return fixer.replaceText(replacedNode, replacementText); + if ( + replacedNode.parent.type !== "CallExpression" && + replacedNode.parent.type !== "ConditionalExpression" && + !astUtils.isParenthesised(sourceCode, replacedNode) && + !astUtils.isParenthesised(sourceCode, node) + ) { + yield fixer.insertTextBefore(replacedNode, "("); + yield fixer.insertTextAfter(replacedNode, ")"); + } } }); } diff --git a/lib/rules/prefer-exponentiation-operator.js b/lib/rules/prefer-exponentiation-operator.js index 5e75ef4724f..d1a00d6209e 100644 --- a/lib/rules/prefer-exponentiation-operator.js +++ b/lib/rules/prefer-exponentiation-operator.js @@ -52,7 +52,7 @@ function doesExponentNeedParens(exponent) { * @returns {boolean} `true` if the expression needs to be parenthesised. */ function doesExponentiationExpressionNeedParens(node, sourceCode) { - const parent = node.parent; + const parent = node.parent.type === "ChainExpression" ? node.parent.parent : node.parent; const needsParens = ( parent.type === "ClassDeclaration" || diff --git a/lib/rules/prefer-numeric-literals.js b/lib/rules/prefer-numeric-literals.js index 2a4fb5d954a..662136c4aad 100644 --- a/lib/rules/prefer-numeric-literals.js +++ b/lib/rules/prefer-numeric-literals.js @@ -29,19 +29,10 @@ const radixMap = new Map([ * false otherwise. */ function isParseInt(calleeNode) { - switch (calleeNode.type) { - case "Identifier": - return calleeNode.name === "parseInt"; - case "MemberExpression": - return calleeNode.object.type === "Identifier" && - calleeNode.object.name === "Number" && - calleeNode.property.type === "Identifier" && - calleeNode.property.name === "parseInt"; - - // no default - } - - return false; + return ( + astUtils.isSpecificId(calleeNode, "parseInt") || + astUtils.isSpecificMemberAccess(calleeNode, "Number", "parseInt") + ); } //------------------------------------------------------------------------------ diff --git a/lib/rules/prefer-promise-reject-errors.js b/lib/rules/prefer-promise-reject-errors.js index 56911b67adc..ec16e445555 100644 --- a/lib/rules/prefer-promise-reject-errors.js +++ b/lib/rules/prefer-promise-reject-errors.js @@ -73,9 +73,7 @@ module.exports = { * @returns {boolean} `true` if the call is a Promise.reject() call */ function isPromiseRejectCall(node) { - return node.callee.type === "MemberExpression" && - node.callee.object.type === "Identifier" && node.callee.object.name === "Promise" && - node.callee.property.type === "Identifier" && node.callee.property.name === "reject"; + return astUtils.isSpecificMemberAccess(node.callee, "Promise", "reject"); } //---------------------------------------------------------------------- diff --git a/lib/rules/prefer-regex-literals.js b/lib/rules/prefer-regex-literals.js index 8a5d209c1e2..9e8ce023547 100644 --- a/lib/rules/prefer-regex-literals.js +++ b/lib/rules/prefer-regex-literals.js @@ -102,11 +102,8 @@ module.exports = { */ function isStringRawTaggedStaticTemplateLiteral(node) { return node.type === "TaggedTemplateExpression" && - node.tag.type === "MemberExpression" && - node.tag.object.type === "Identifier" && - node.tag.object.name === "String" && - isGlobalReference(node.tag.object) && - astUtils.getStaticPropertyName(node.tag) === "raw" && + astUtils.isSpecificMemberAccess(node.tag, "String", "raw") && + isGlobalReference(astUtils.skipChainExpression(node.tag).object) && isStaticTemplateLiteral(node.quasi); } diff --git a/lib/rules/prefer-spread.js b/lib/rules/prefer-spread.js index bcb0dc0dd4c..d3c3c4d2297 100644 --- a/lib/rules/prefer-spread.js +++ b/lib/rules/prefer-spread.js @@ -18,17 +18,13 @@ const astUtils = require("./utils/ast-utils"); */ function isVariadicApplyCalling(node) { return ( - node.callee.type === "MemberExpression" && - node.callee.property.type === "Identifier" && - node.callee.property.name === "apply" && - node.callee.computed === false && + astUtils.isSpecificMemberAccess(node.callee, null, "apply") && node.arguments.length === 2 && node.arguments[1].type !== "ArrayExpression" && node.arguments[1].type !== "SpreadElement" ); } - /** * Checks whether or not `thisArg` is not changed by `.apply()`. * @param {ASTNode|null} expectedThis The node that is the owner of the applied function. @@ -75,7 +71,7 @@ module.exports = { return; } - const applied = node.callee.object; + const applied = astUtils.skipChainExpression(astUtils.skipChainExpression(node.callee).object); const expectedThis = (applied.type === "MemberExpression") ? applied.object : null; const thisArg = node.arguments[0]; diff --git a/lib/rules/radix.js b/lib/rules/radix.js index 3903cb2a6a2..e3225662388 100644 --- a/lib/rules/radix.js +++ b/lib/rules/radix.js @@ -166,9 +166,12 @@ module.exports = { if (variable && !isShadowed(variable)) { variable.references.forEach(reference => { const node = reference.identifier.parent; + const maybeCallee = node.parent.type === "ChainExpression" + ? node.parent + : node; - if (isParseIntMethod(node) && astUtils.isCallee(node)) { - checkArguments(node.parent); + if (isParseIntMethod(node) && astUtils.isCallee(maybeCallee)) { + checkArguments(maybeCallee.parent); } }); } diff --git a/lib/rules/use-isnan.js b/lib/rules/use-isnan.js index 7b466be75f2..53ffeb7e6d1 100644 --- a/lib/rules/use-isnan.js +++ b/lib/rules/use-isnan.js @@ -106,7 +106,7 @@ module.exports = { * @returns {void} */ function checkCallExpression(node) { - const callee = node.callee; + const callee = astUtils.skipChainExpression(node.callee); if (callee.type === "MemberExpression") { const methodName = astUtils.getStaticPropertyName(callee); diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js index ecea6948da2..d0dd770d199 100644 --- a/lib/rules/utils/ast-utils.js +++ b/lib/rules/utils/ast-utils.js @@ -143,6 +143,23 @@ function isInLoop(node) { return false; } +/** + * Determines whether the given node is a `null` literal. + * @param {ASTNode} node The node to check + * @returns {boolean} `true` if the node is a `null` literal + */ +function isNullLiteral(node) { + + /* + * Checking `node.value === null` does not guarantee that a literal is a null literal. + * When parsing values that cannot be represented in the current environment (e.g. unicode + * regexes in Node 4), `node.value` is set to `null` because it wouldn't be possible to + * set `node.value` to a unicode regex. To make sure a literal is actually `null`, check + * `node.regex` instead. Also see: https://github.com/eslint/eslint/issues/8020 + */ + return node.type === "Literal" && node.value === null && !node.regex && !node.bigint; +} + /** * Checks whether or not a node is `null` or `undefined`. * @param {ASTNode} node A node to check. @@ -151,7 +168,7 @@ function isInLoop(node) { */ function isNullOrUndefined(node) { return ( - module.exports.isNullLiteral(node) || + isNullLiteral(node) || (node.type === "Identifier" && node.name === "undefined") || (node.type === "UnaryExpression" && node.operator === "void") ); @@ -166,20 +183,270 @@ function isCallee(node) { return node.parent.type === "CallExpression" && node.parent.callee === node; } +/** + * Returns the result of the string conversion applied to the evaluated value of the given expression node, + * if it can be determined statically. + * + * This function returns a `string` value for all `Literal` nodes and simple `TemplateLiteral` nodes only. + * In all other cases, this function returns `null`. + * @param {ASTNode} node Expression node. + * @returns {string|null} String value if it can be determined. Otherwise, `null`. + */ +function getStaticStringValue(node) { + switch (node.type) { + case "Literal": + if (node.value === null) { + if (isNullLiteral(node)) { + return String(node.value); // "null" + } + if (node.regex) { + return `/${node.regex.pattern}/${node.regex.flags}`; + } + if (node.bigint) { + return node.bigint; + } + + // Otherwise, this is an unknown literal. The function will return null. + + } else { + return String(node.value); + } + break; + case "TemplateLiteral": + if (node.expressions.length === 0 && node.quasis.length === 1) { + return node.quasis[0].value.cooked; + } + break; + + // no default + } + + return null; +} + +/** + * Gets the property name of a given node. + * The node can be a MemberExpression, a Property, or a MethodDefinition. + * + * If the name is dynamic, this returns `null`. + * + * For examples: + * + * a.b // => "b" + * a["b"] // => "b" + * a['b'] // => "b" + * a[`b`] // => "b" + * a[100] // => "100" + * a[b] // => null + * a["a" + "b"] // => null + * a[tag`b`] // => null + * a[`${b}`] // => null + * + * let a = {b: 1} // => "b" + * let a = {["b"]: 1} // => "b" + * let a = {['b']: 1} // => "b" + * let a = {[`b`]: 1} // => "b" + * let a = {[100]: 1} // => "100" + * let a = {[b]: 1} // => null + * let a = {["a" + "b"]: 1} // => null + * let a = {[tag`b`]: 1} // => null + * let a = {[`${b}`]: 1} // => null + * @param {ASTNode} node The node to get. + * @returns {string|null} The property name if static. Otherwise, null. + */ +function getStaticPropertyName(node) { + let prop; + + switch (node && node.type) { + case "ChainExpression": + return getStaticPropertyName(node.expression); + + case "Property": + case "MethodDefinition": + prop = node.key; + break; + + case "MemberExpression": + prop = node.property; + break; + + // no default + } + + if (prop) { + if (prop.type === "Identifier" && !node.computed) { + return prop.name; + } + + return getStaticStringValue(prop); + } + + return null; +} + +/** + * Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it. + * @param {ASTNode} node The node to address. + * @returns {ASTNode} The `ChainExpression#expression` value if the node is a `ChainExpression` node. Otherwise, the node. + */ +function skipChainExpression(node) { + return node && node.type === "ChainExpression" ? node.expression : node; +} + +/** + * Check if the `actual` is an expected value. + * @param {string} actual The string value to check. + * @param {string | RegExp} expected The expected string value or pattern. + * @returns {boolean} `true` if the `actual` is an expected value. + */ +function checkText(actual, expected) { + return typeof expected === "string" + ? actual === expected + : expected.test(actual); +} + +/** + * Check if a given node is an Identifier node with a given name. + * @param {ASTNode} node The node to check. + * @param {string | RegExp} name The expected name or the expected pattern of the object name. + * @returns {boolean} `true` if the node is an Identifier node with the name. + */ +function isSpecificId(node, name) { + return node.type === "Identifier" && checkText(node.name, name); +} + +/** + * Check if a given node is member access with a given object name and property name pair. + * This is regardless of optional or not. + * @param {ASTNode} node The node to check. + * @param {string | RegExp | null} objectName The expected name or the expected pattern of the object name. If this is nullish, this method doesn't check object. + * @param {string | RegExp | null} propertyName The expected name or the expected pattern of the property name. If this is nullish, this method doesn't check property. + * @returns {boolean} `true` if the node is member access with the object name and property name pair. + * The node is a `MemberExpression` or `ChainExpression`. + */ +function isSpecificMemberAccess(node, objectName, propertyName) { + const checkNode = skipChainExpression(node); + + if (checkNode.type !== "MemberExpression") { + return false; + } + + if (objectName && !isSpecificId(checkNode.object, objectName)) { + return false; + } + + if (propertyName) { + const actualPropertyName = getStaticPropertyName(checkNode); + + if (typeof actualPropertyName !== "string" || !checkText(actualPropertyName, propertyName)) { + return false; + } + } + + return true; +} + +/** + * Check if two literal nodes are the same value. + * @param {ASTNode} left The Literal node to compare. + * @param {ASTNode} right The other Literal node to compare. + * @returns {boolean} `true` if the two literal nodes are the same value. + */ +function equalLiteralValue(left, right) { + + // RegExp literal. + if (left.regex || right.regex) { + return Boolean( + left.regex && + right.regex && + left.regex.pattern === right.regex.pattern && + left.regex.flags === right.regex.flags + ); + } + + // BigInt literal. + if (left.bigint || right.bigint) { + return left.bigint === right.bigint; + } + + return left.value === right.value; +} + +/** + * Check if two expressions reference the same value. For example: + * a = a + * a.b = a.b + * a[0] = a[0] + * a['b'] = a['b'] + * @param {ASTNode} left The left side of the comparison. + * @param {ASTNode} right The right side of the comparison. + * @param {boolean} [disableStaticComputedKey] Don't address `a.b` and `a["b"]` are the same if `true`. For backward compatibility. + * @returns {boolean} `true` if both sides match and reference the same value. + */ +function isSameReference(left, right, disableStaticComputedKey = false) { + if (left.type !== right.type) { + + // Handle `a.b` and `a?.b` are samely. + if (left.type === "ChainExpression") { + return isSameReference(left.expression, right, disableStaticComputedKey); + } + if (right.type === "ChainExpression") { + return isSameReference(left, right.expression, disableStaticComputedKey); + } + + return false; + } + + switch (left.type) { + case "Super": + case "ThisExpression": + return true; + + case "Identifier": + return left.name === right.name; + case "Literal": + return equalLiteralValue(left, right); + + case "ChainExpression": + return isSameReference(left.expression, right.expression, disableStaticComputedKey); + + case "MemberExpression": { + if (!disableStaticComputedKey) { + const nameA = getStaticPropertyName(left); + + // x.y = x["y"] + if (nameA !== null) { + return ( + isSameReference(left.object, right.object, disableStaticComputedKey) && + nameA === getStaticPropertyName(right) + ); + } + } + + /* + * x[0] = x[0] + * x[y] = x[y] + * x.y = x.y + */ + return ( + left.computed === right.computed && + isSameReference(left.object, right.object, disableStaticComputedKey) && + isSameReference(left.property, right.property, disableStaticComputedKey) + ); + } + + default: + return false; + } +} + /** * Checks whether or not a node is `Reflect.apply`. * @param {ASTNode} node A node to check. * @returns {boolean} Whether or not the node is a `Reflect.apply`. */ function isReflectApply(node) { - return ( - node.type === "MemberExpression" && - node.object.type === "Identifier" && - node.object.name === "Reflect" && - node.property.type === "Identifier" && - node.property.name === "apply" && - node.computed === false - ); + return isSpecificMemberAccess(node, "Reflect", "apply"); } /** @@ -188,14 +455,7 @@ function isReflectApply(node) { * @returns {boolean} Whether or not the node is a `Array.from`. */ function isArrayFromMethod(node) { - return ( - node.type === "MemberExpression" && - node.object.type === "Identifier" && - arrayOrTypedArrayPattern.test(node.object.name) && - node.property.type === "Identifier" && - node.property.name === "from" && - node.computed === false - ); + return isSpecificMemberAccess(node, arrayOrTypedArrayPattern, "from"); } /** @@ -204,17 +464,7 @@ function isArrayFromMethod(node) { * @returns {boolean} Whether or not the node is a method which has `thisArg`. */ function isMethodWhichHasThisArg(node) { - for ( - let currentNode = node; - currentNode.type === "MemberExpression" && !currentNode.computed; - currentNode = currentNode.property - ) { - if (currentNode.property.type === "Identifier") { - return arrayMethodPattern.test(currentNode.property.name); - } - } - - return false; + return isSpecificMemberAccess(node, null, arrayMethodPattern); } /** @@ -289,6 +539,15 @@ function isDotToken(token) { return token.value === "." && token.type === "Punctuator"; } +/** + * Checks if the given token is a `?.` token or not. + * @param {Token} token The token to check. + * @returns {boolean} `true` if the token is a `?.` token. + */ +function isQuestionDotToken(token) { + return token.value === "?." && token.type === "Punctuator"; +} + /** * Checks if the given token is a semicolon token or not. * @param {Token} token The token to check. @@ -505,6 +764,7 @@ module.exports = { isCommaToken, isCommentToken, isDotToken, + isQuestionDotToken, isKeywordToken, isNotClosingBraceToken: negate(isClosingBraceToken), isNotClosingBracketToken: negate(isClosingBracketToken), @@ -512,6 +772,7 @@ module.exports = { isNotColonToken: negate(isColonToken), isNotCommaToken: negate(isCommaToken), isNotDotToken: negate(isDotToken), + isNotQuestionDotToken: negate(isQuestionDotToken), isNotOpeningBraceToken: negate(isOpeningBraceToken), isNotOpeningBracketToken: negate(isOpeningBracketToken), isNotOpeningParenToken: negate(isOpeningParenToken), @@ -669,6 +930,7 @@ module.exports = { */ case "LogicalExpression": case "ConditionalExpression": + case "ChainExpression": currentNode = parent; break; @@ -755,14 +1017,21 @@ module.exports = { * (function foo() { ... }).apply(obj, []); */ case "MemberExpression": - return ( - parent.object !== currentNode || - parent.property.type !== "Identifier" || - !bindOrCallOrApplyPattern.test(parent.property.name) || - !isCallee(parent) || - parent.parent.arguments.length === 0 || - isNullOrUndefined(parent.parent.arguments[0]) - ); + if ( + parent.object === currentNode && + isSpecificMemberAccess(parent, null, bindOrCallOrApplyPattern) + ) { + const maybeCalleeNode = parent.parent.type === "ChainExpression" + ? parent.parent + : parent; + + return !( + isCallee(maybeCalleeNode) && + maybeCalleeNode.parent.arguments.length >= 1 && + !isNullOrUndefined(maybeCalleeNode.parent.arguments[0]) + ); + } + return true; /* * e.g. @@ -884,6 +1153,7 @@ module.exports = { return 17; case "CallExpression": + case "ChainExpression": case "ImportExpression": return 18; @@ -913,104 +1183,6 @@ module.exports = { return isFunction(node) && module.exports.isEmptyBlock(node.body); }, - /** - * Returns the result of the string conversion applied to the evaluated value of the given expression node, - * if it can be determined statically. - * - * This function returns a `string` value for all `Literal` nodes and simple `TemplateLiteral` nodes only. - * In all other cases, this function returns `null`. - * @param {ASTNode} node Expression node. - * @returns {string|null} String value if it can be determined. Otherwise, `null`. - */ - getStaticStringValue(node) { - switch (node.type) { - case "Literal": - if (node.value === null) { - if (module.exports.isNullLiteral(node)) { - return String(node.value); // "null" - } - if (node.regex) { - return `/${node.regex.pattern}/${node.regex.flags}`; - } - if (node.bigint) { - return node.bigint; - } - - // Otherwise, this is an unknown literal. The function will return null. - - } else { - return String(node.value); - } - break; - case "TemplateLiteral": - if (node.expressions.length === 0 && node.quasis.length === 1) { - return node.quasis[0].value.cooked; - } - break; - - // no default - } - - return null; - }, - - /** - * Gets the property name of a given node. - * The node can be a MemberExpression, a Property, or a MethodDefinition. - * - * If the name is dynamic, this returns `null`. - * - * For examples: - * - * a.b // => "b" - * a["b"] // => "b" - * a['b'] // => "b" - * a[`b`] // => "b" - * a[100] // => "100" - * a[b] // => null - * a["a" + "b"] // => null - * a[tag`b`] // => null - * a[`${b}`] // => null - * - * let a = {b: 1} // => "b" - * let a = {["b"]: 1} // => "b" - * let a = {['b']: 1} // => "b" - * let a = {[`b`]: 1} // => "b" - * let a = {[100]: 1} // => "100" - * let a = {[b]: 1} // => null - * let a = {["a" + "b"]: 1} // => null - * let a = {[tag`b`]: 1} // => null - * let a = {[`${b}`]: 1} // => null - * @param {ASTNode} node The node to get. - * @returns {string|null} The property name if static. Otherwise, null. - */ - getStaticPropertyName(node) { - let prop; - - switch (node && node.type) { - case "Property": - case "MethodDefinition": - prop = node.key; - break; - - case "MemberExpression": - prop = node.property; - break; - - // no default - } - - if (prop) { - if (prop.type === "Identifier" && !node.computed) { - return prop.name; - } - - return module.exports.getStaticStringValue(prop); - } - - return null; - }, - /** * Get directives from directive prologue of a Program or Function node. * @param {ASTNode} node The node to check. @@ -1164,7 +1336,7 @@ module.exports = { if (node.id) { tokens.push(`'${node.id.name}'`); } else { - const name = module.exports.getStaticPropertyName(parent); + const name = getStaticPropertyName(parent); if (name !== null) { tokens.push(`'${name}'`); @@ -1391,6 +1563,7 @@ module.exports = { case "TaggedTemplateExpression": case "YieldExpression": case "AwaitExpression": + case "ChainExpression": return true; // possibly an error object. case "AssignmentExpression": @@ -1413,23 +1586,6 @@ module.exports = { } }, - /** - * Determines whether the given node is a `null` literal. - * @param {ASTNode} node The node to check - * @returns {boolean} `true` if the node is a `null` literal - */ - isNullLiteral(node) { - - /* - * Checking `node.value === null` does not guarantee that a literal is a null literal. - * When parsing values that cannot be represented in the current environment (e.g. unicode - * regexes in Node 4), `node.value` is set to `null` because it wouldn't be possible to - * set `node.value` to a unicode regex. To make sure a literal is actually `null`, check - * `node.regex` instead. Also see: https://github.com/eslint/eslint/issues/8020 - */ - return node.type === "Literal" && node.value === null && !node.regex && !node.bigint; - }, - /** * Check if a given node is a numeric literal or not. * @param {ASTNode} node The node to check. @@ -1590,5 +1746,13 @@ module.exports = { isLogicalExpression, isCoalesceExpression, - isMixedLogicalAndCoalesceExpressions + isMixedLogicalAndCoalesceExpressions, + isNullLiteral, + getStaticStringValue, + getStaticPropertyName, + skipChainExpression, + isSpecificId, + isSpecificMemberAccess, + equalLiteralValue, + isSameReference }; diff --git a/lib/rules/wrap-iife.js b/lib/rules/wrap-iife.js index 896aed63de5..07e5b84a4a5 100644 --- a/lib/rules/wrap-iife.js +++ b/lib/rules/wrap-iife.js @@ -23,7 +23,14 @@ const eslintUtils = require("eslint-utils"); * @private */ function isCalleeOfNewExpression(node) { - return node.parent.type === "NewExpression" && node.parent.callee === node; + const maybeCallee = node.parent.type === "ChainExpression" + ? node.parent + : node; + + return ( + maybeCallee.parent.type === "NewExpression" && + maybeCallee.parent.callee === maybeCallee + ); } //------------------------------------------------------------------------------ @@ -98,7 +105,7 @@ module.exports = { * @returns {ASTNode} node that is the function expression of the given IIFE, or null if none exist */ function getFunctionNodeFromIIFE(node) { - const callee = node.callee; + const callee = astUtils.skipChainExpression(node.callee); if (callee.type === "FunctionExpression") { return callee; diff --git a/lib/rules/yoda.js b/lib/rules/yoda.js index f1159e5255d..87dd5ed6c57 100644 --- a/lib/rules/yoda.js +++ b/lib/rules/yoda.js @@ -111,59 +111,6 @@ function getNormalizedLiteral(node) { return null; } -/** - * Checks whether two expressions reference the same value. For example: - * a = a - * a.b = a.b - * a[0] = a[0] - * a['b'] = a['b'] - * @param {ASTNode} a Left side of the comparison. - * @param {ASTNode} b Right side of the comparison. - * @returns {boolean} True if both sides match and reference the same value. - */ -function same(a, b) { - if (a.type !== b.type) { - return false; - } - - switch (a.type) { - case "Identifier": - return a.name === b.name; - - case "Literal": - return a.value === b.value; - - case "MemberExpression": { - const nameA = astUtils.getStaticPropertyName(a); - - // x.y = x["y"] - if (nameA !== null) { - return ( - same(a.object, b.object) && - nameA === astUtils.getStaticPropertyName(b) - ); - } - - /* - * x[0] = x[0] - * x[y] = x[y] - * x.y = x.y - */ - return ( - a.computed === b.computed && - same(a.object, b.object) && - same(a.property, b.property) - ); - } - - case "ThisExpression": - return true; - - default: - return false; - } -} - //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -236,7 +183,7 @@ module.exports = { * @returns {boolean} Whether node is a "between" range test. */ function isBetweenTest() { - if (node.operator === "&&" && same(left.right, right.left)) { + if (node.operator === "&&" && astUtils.isSameReference(left.right, right.left)) { const leftLiteral = getNormalizedLiteral(left.left); const rightLiteral = getNormalizedLiteral(right.right); @@ -260,7 +207,7 @@ module.exports = { * @returns {boolean} Whether node is an "outside" range test. */ function isOutsideTest() { - if (node.operator === "||" && same(left.left, right.right)) { + if (node.operator === "||" && astUtils.isSameReference(left.left, right.right)) { const leftLiteral = getNormalizedLiteral(left.right); const rightLiteral = getNormalizedLiteral(right.left); diff --git a/package.json b/package.json index 442639e6d67..727a7a3a41c 100644 --- a/package.json +++ b/package.json @@ -54,9 +54,9 @@ "doctrine": "^3.0.0", "enquirer": "^2.3.5", "eslint-scope": "^5.1.0", - "eslint-utils": "^2.0.0", - "eslint-visitor-keys": "^1.2.0", - "espree": "^7.1.0", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^1.3.0", + "espree": "^7.2.0", "esquery": "^1.2.0", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", diff --git a/tests/fixtures/code-path-analysis/logical--if-qdot-1.js b/tests/fixtures/code-path-analysis/logical--if-qdot-1.js new file mode 100644 index 00000000000..4e1d7b000c7 --- /dev/null +++ b/tests/fixtures/code-path-analysis/logical--if-qdot-1.js @@ -0,0 +1,38 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_4->s1_5->s1_6->s1_7->s1_9->s1_11; +s1_1->s1_3->s1_10->s1_11; +s1_4->s1_6->s1_8->s1_9; +s1_11->final; +*/ +if (obj?.foo) { + if (obj?.bar) { + foo(); + } else { + bar(); + } +} else { + qiz(); +} + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nIfStatement:enter\nChainExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="Identifier (foo)\nMemberExpression:exit"]; + s1_3[label="ChainExpression:exit"]; + s1_4[label="BlockStatement:enter\nIfStatement:enter\nChainExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_5[label="Identifier (bar)\nMemberExpression:exit"]; + s1_6[label="ChainExpression:exit"]; + s1_7[label="BlockStatement:enter\nExpressionStatement:enter\nCallExpression:enter\nIdentifier (foo)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"]; + s1_9[label="IfStatement:exit\nBlockStatement:exit"]; + s1_11[label="IfStatement:exit\nProgram:exit"]; + s1_10[label="BlockStatement:enter\nExpressionStatement:enter\nCallExpression:enter\nIdentifier (qiz)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"]; + s1_8[label="BlockStatement:enter\nExpressionStatement:enter\nCallExpression:enter\nIdentifier (bar)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"]; + initial->s1_1->s1_2->s1_3->s1_4->s1_5->s1_6->s1_7->s1_9->s1_11; + s1_1->s1_3->s1_10->s1_11; + s1_4->s1_6->s1_8->s1_9; + s1_11->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--complex-1.js b/tests/fixtures/code-path-analysis/optional-chaining--complex-1.js new file mode 100644 index 00000000000..5444269a841 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--complex-1.js @@ -0,0 +1,38 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_9->s1_10->s1_11->s1_12->s1_13->s1_16; +s1_1->s1_16; +s1_2->s1_4->s1_5->s1_16; +s1_6->s1_8->s1_16; +s1_9->s1_11->s1_13; +s1_16->final; +*/ + +obj?.[cond ? k1 : k2]?.[k3 || k4]?.(a1 && a2, b1 ?? b2).foo(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nCallExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="ConditionalExpression:enter\nIdentifier (cond)"]; + s1_3[label="Identifier (k1)"]; + s1_5[label="ConditionalExpression:exit\nMemberExpression:exit"]; + s1_6[label="LogicalExpression:enter\nIdentifier (k3)"]; + s1_7[label="Identifier (k4)"]; + s1_8[label="LogicalExpression:exit\nMemberExpression:exit"]; + s1_9[label="LogicalExpression:enter\nIdentifier (a1)"]; + s1_10[label="Identifier (a2)"]; + s1_11[label="LogicalExpression:exit\nLogicalExpression:enter\nIdentifier (b1)"]; + s1_12[label="Identifier (b2)"]; + s1_13[label="LogicalExpression:exit\nCallExpression:exit\nIdentifier (foo)\nMemberExpression:exit\nIdentifier (arg)\nCallExpression:exit"]; + s1_16[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + s1_4[label="Identifier (k2)"]; + initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_9->s1_10->s1_11->s1_12->s1_13->s1_16; + s1_1->s1_16; + s1_2->s1_4->s1_5->s1_16; + s1_6->s1_8->s1_16; + s1_9->s1_11->s1_13; + s1_16->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--complex-2.js b/tests/fixtures/code-path-analysis/optional-chaining--complex-2.js new file mode 100644 index 00000000000..8d53c07e5f5 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--complex-2.js @@ -0,0 +1,38 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_9->s1_10->s1_11->s1_12->s1_13->s1_16; +s1_1->s1_16; +s1_2->s1_4->s1_5->s1_16; +s1_6->s1_8->s1_16; +s1_9->s1_11->s1_13; +s1_16->final; +*/ + +(obj?.[cond ? k1 : k2]?.[k3 || k4]?.(a1 && a2, b1 ?? b2)).foo(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nCallExpression:enter\nMemberExpression:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="ConditionalExpression:enter\nIdentifier (cond)"]; + s1_3[label="Identifier (k1)"]; + s1_5[label="ConditionalExpression:exit\nMemberExpression:exit"]; + s1_6[label="LogicalExpression:enter\nIdentifier (k3)"]; + s1_7[label="Identifier (k4)"]; + s1_8[label="LogicalExpression:exit\nMemberExpression:exit"]; + s1_9[label="LogicalExpression:enter\nIdentifier (a1)"]; + s1_10[label="Identifier (a2)"]; + s1_11[label="LogicalExpression:exit\nLogicalExpression:enter\nIdentifier (b1)"]; + s1_12[label="Identifier (b2)"]; + s1_13[label="LogicalExpression:exit\nCallExpression:exit"]; + s1_16[label="ChainExpression:exit\nIdentifier (foo)\nMemberExpression:exit\nIdentifier (arg)\nCallExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + s1_4[label="Identifier (k2)"]; + initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_9->s1_10->s1_11->s1_12->s1_13->s1_16; + s1_1->s1_16; + s1_2->s1_4->s1_5->s1_16; + s1_6->s1_8->s1_16; + s1_9->s1_11->s1_13; + s1_16->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--complex-3.js b/tests/fixtures/code-path-analysis/optional-chaining--complex-3.js new file mode 100644 index 00000000000..0afef1cd172 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--complex-3.js @@ -0,0 +1,41 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_10->s1_11->s1_12->s1_13->s1_14->s1_15->s1_16; +s1_1->s1_10; +s1_2->s1_4->s1_5->s1_10; +s1_6->s1_8; +s1_10->s1_16; +s1_11->s1_13->s1_15; +s1_16->final; +*/ + +(obj?.[cond ? k1 : k2]?.[k3 || k4])?.(a1 && a2, b1 ?? b2).foo(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nCallExpression:enter\nChainExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="ConditionalExpression:enter\nIdentifier (cond)"]; + s1_3[label="Identifier (k1)"]; + s1_5[label="ConditionalExpression:exit\nMemberExpression:exit"]; + s1_6[label="LogicalExpression:enter\nIdentifier (k3)"]; + s1_7[label="Identifier (k4)"]; + s1_8[label="LogicalExpression:exit\nMemberExpression:exit"]; + s1_10[label="ChainExpression:exit"]; + s1_11[label="LogicalExpression:enter\nIdentifier (a1)"]; + s1_12[label="Identifier (a2)"]; + s1_13[label="LogicalExpression:exit\nLogicalExpression:enter\nIdentifier (b1)"]; + s1_14[label="Identifier (b2)"]; + s1_15[label="LogicalExpression:exit\nCallExpression:exit\nIdentifier (foo)\nMemberExpression:exit\nIdentifier (arg)\nCallExpression:exit"]; + s1_16[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + s1_4[label="Identifier (k2)"]; + initial->s1_1->s1_2->s1_3->s1_5->s1_6->s1_7->s1_8->s1_10->s1_11->s1_12->s1_13->s1_14->s1_15->s1_16; + s1_1->s1_10; + s1_2->s1_4->s1_5->s1_10; + s1_6->s1_8; + s1_10->s1_16; + s1_11->s1_13->s1_15; + s1_16->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--simple-1.js b/tests/fixtures/code-path-analysis/optional-chaining--simple-1.js new file mode 100644 index 00000000000..d466a4e72d7 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--simple-1.js @@ -0,0 +1,19 @@ +/*expected +initial->s1_1->s1_2->s1_3; +s1_1->s1_3->final; +*/ + +obj?.aaa.bbb(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="Identifier (aaa)\nMemberExpression:exit\nIdentifier (bbb)\nMemberExpression:exit\nIdentifier (arg)\nCallExpression:exit"]; + s1_3[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + initial->s1_1->s1_2->s1_3; + s1_1->s1_3->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--simple-2.js b/tests/fixtures/code-path-analysis/optional-chaining--simple-2.js new file mode 100644 index 00000000000..a5867528678 --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--simple-2.js @@ -0,0 +1,19 @@ +/*expected +initial->s1_1->s1_2->s1_3; +s1_1->s1_3->final; +*/ + +obj.foo?.(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nIdentifier (obj)\nIdentifier (foo)\nMemberExpression:exit"]; + s1_2[label="Identifier (arg)\nCallExpression:exit"]; + s1_3[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + initial->s1_1->s1_2->s1_3; + s1_1->s1_3->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--simple-3.js b/tests/fixtures/code-path-analysis/optional-chaining--simple-3.js new file mode 100644 index 00000000000..fa43af967ed --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--simple-3.js @@ -0,0 +1,25 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_4->s1_7; +s1_1->s1_7; +s1_2->s1_7; +s1_3->s1_7->final; +*/ + +obj?.aaa?.bbb?.(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nMemberExpression:enter\nMemberExpression:enter\nIdentifier (obj)"]; + s1_2[label="Identifier (aaa)\nMemberExpression:exit"]; + s1_3[label="Identifier (bbb)\nMemberExpression:exit"]; + s1_4[label="Identifier (arg)\nCallExpression:exit"]; + s1_7[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + initial->s1_1->s1_2->s1_3->s1_4->s1_7; + s1_1->s1_7; + s1_2->s1_7; + s1_3->s1_7->final; +} +*/ diff --git a/tests/fixtures/code-path-analysis/optional-chaining--simple-4.js b/tests/fixtures/code-path-analysis/optional-chaining--simple-4.js new file mode 100644 index 00000000000..c10174646bd --- /dev/null +++ b/tests/fixtures/code-path-analysis/optional-chaining--simple-4.js @@ -0,0 +1,19 @@ +/*expected +initial->s1_1->s1_2->s1_3; +s1_1->s1_3->final; +*/ + +func?.()(arg) + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nChainExpression:enter\nCallExpression:enter\nCallExpression:enter\nIdentifier (func)\nCallExpression:exit"]; + s1_2[label="Identifier (arg)\nCallExpression:exit"]; + s1_3[label="ChainExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + initial->s1_1->s1_2->s1_3; + s1_1->s1_3->final; +} +*/ diff --git a/tests/lib/rules/accessor-pairs.js b/tests/lib/rules/accessor-pairs.js index e8f143ada4c..c6341326647 100644 --- a/tests/lib/rules/accessor-pairs.js +++ b/tests/lib/rules/accessor-pairs.js @@ -1087,6 +1087,46 @@ ruleTester.run("accessor-pairs", rule, { code: "Object.create(null, {foo: {set: function(value) {}}});", errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] }, + { + code: "var o = {d: 1};\n Object?.defineProperty(o, 'c', \n{set: function(value) {\n val = value; \n} \n});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "Reflect?.defineProperty(obj, 'foo', {set: function(value) {}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "Object?.defineProperties(obj, {foo: {set: function(value) {}}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "Object?.create(null, {foo: {set: function(value) {}}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "var o = {d: 1};\n (Object?.defineProperty)(o, 'c', \n{set: function(value) {\n val = value; \n} \n});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "(Reflect?.defineProperty)(obj, 'foo', {set: function(value) {}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "(Object?.defineProperties)(obj, {foo: {set: function(value) {}}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, + { + code: "(Object?.create)(null, {foo: {set: function(value) {}}});", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ message: "Getter is not present in property descriptor.", type: "ObjectExpression" }] + }, //------------------------------------------------------------------------------ // Classes diff --git a/tests/lib/rules/array-callback-return.js b/tests/lib/rules/array-callback-return.js index 61aabaeb32c..6d6a4d86242 100644 --- a/tests/lib/rules/array-callback-return.js +++ b/tests/lib/rules/array-callback-return.js @@ -443,6 +443,33 @@ ruleTester.run("array-callback-return", rule, { endLine: 2, endColumn: 20 }] + }, + + // Optional chaining + { + code: "foo?.filter(() => { console.log('hello') })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" } }] + }, + { + code: "(foo?.filter)(() => { console.log('hello') })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" } }] + }, + { + code: "Array?.from([], () => { console.log('hello') })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.from" } }] + }, + { + code: "(Array?.from)([], () => { console.log('hello') })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.from" } }] + }, + { + code: "foo?.filter((function() { return () => { console.log('hello') } })?.())", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedInside", data: { name: "arrow function", arrayMethodName: "Array.prototype.filter" } }] } ] }); diff --git a/tests/lib/rules/camelcase.js b/tests/lib/rules/camelcase.js index eab0f354aec..4f9cdca78fe 100644 --- a/tests/lib/rules/camelcase.js +++ b/tests/lib/rules/camelcase.js @@ -1306,6 +1306,20 @@ ruleTester.run("camelcase", rule, { type: "Identifier" } ] + }, + + // Optional chaining. + { + code: "obj.o_k.non_camelcase = 0", + options: [{ properties: "always" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "notCamelCase", data: { name: "non_camelcase" } }] + }, + { + code: "(obj?.o_k).non_camelcase = 0", + options: [{ properties: "always" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "notCamelCase", data: { name: "non_camelcase" } }] } ] }); diff --git a/tests/lib/rules/computed-property-spacing.js b/tests/lib/rules/computed-property-spacing.js index b0ecef6eb81..a1e5834243e 100644 --- a/tests/lib/rules/computed-property-spacing.js +++ b/tests/lib/rules/computed-property-spacing.js @@ -1906,6 +1906,28 @@ ruleTester.run("computed-property-spacing", rule, { endColumn: 19 } ] + }, + + // Optional chaining + { + code: "obj?.[1];", + output: "obj?.[ 1 ];", + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "missingSpaceAfter", data: { tokenValue: "[" } }, + { messageId: "missingSpaceBefore", data: { tokenValue: "]" } } + ] + }, + { + code: "obj?.[ 1 ];", + output: "obj?.[1];", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "unexpectedSpaceAfter", data: { tokenValue: "[" } }, + { messageId: "unexpectedSpaceBefore", data: { tokenValue: "]" } } + ] } ] }); diff --git a/tests/lib/rules/constructor-super.js b/tests/lib/rules/constructor-super.js index b6c223dec7d..e20da576b5e 100644 --- a/tests/lib/rules/constructor-super.js +++ b/tests/lib/rules/constructor-super.js @@ -16,7 +16,7 @@ const { RuleTester } = require("../../../lib/rule-tester"); // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("constructor-super", rule, { valid: [ @@ -88,7 +88,10 @@ ruleTester.run("constructor-super", rule, { } } } - ` + `, + + // Optional chaining + "class A extends obj?.prop { constructor() { super(); } }" ], invalid: [ diff --git a/tests/lib/rules/dot-location.js b/tests/lib/rules/dot-location.js index 1a6ea37a733..e102bd6927d 100644 --- a/tests/lib/rules/dot-location.js +++ b/tests/lib/rules/dot-location.js @@ -136,6 +136,68 @@ ruleTester.run("dot-location", rule, { { code: "(\na &&\nb()\n).toString()", options: ["object"] + }, + + // Optional chaining + { + code: "obj?.prop", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[key]", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\nprop", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n?.[key]", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\n[key]", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[\nkey]", + options: ["object"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.prop", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[key]", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n?.prop", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n?.[key]", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\n[key]", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[\nkey]", + options: ["property"], + parserOptions: { ecmaVersion: 2020 } } ], invalid: [ @@ -255,6 +317,29 @@ ruleTester.run("dot-location", rule, { output: "(5).\ntoExponential()", options: ["object"], errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 2, column: 1 }] + }, + + // Optional chaining + { + code: "obj\n?.prop", + output: "obj?.\nprop", + options: ["object"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedDotAfterObject" }] + }, + { + code: "10\n?.prop", + output: "10?.\nprop", + options: ["object"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedDotAfterObject" }] + }, + { + code: "obj?.\nprop", + output: "obj\n?.prop", + options: ["property"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedDotBeforeProperty" }] } ] }); diff --git a/tests/lib/rules/dot-notation.js b/tests/lib/rules/dot-notation.js index 5532ea35870..341ef25ec67 100644 --- a/tests/lib/rules/dot-notation.js +++ b/tests/lib/rules/dot-notation.js @@ -218,6 +218,34 @@ ruleTester.run("dot-notation", rule, { output: null, // `let["if"]()` is a syntax error because `let[` indicates a destructuring variable declaration options: [{ allowKeywords: false }], errors: [{ messageId: "useBrackets", data: { key: "if" } }] + }, + + // Optional chaining + { + code: "obj?.['prop']", + output: "obj?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "useDot", data: { key: q("prop") } }] + }, + { + code: "0?.['prop']", + output: "0?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "useDot", data: { key: q("prop") } }] + }, + { + code: "obj?.true", + output: "obj?.[\"true\"]", + options: [{ allowKeywords: false }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "useBrackets", data: { key: "true" } }] + }, + { + code: "let?.true", + output: "let?.[\"true\"]", + options: [{ allowKeywords: false }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "useBrackets", data: { key: "true" } }] } ] }); diff --git a/tests/lib/rules/func-call-spacing.js b/tests/lib/rules/func-call-spacing.js index f0199fd0a4e..3520882b019 100644 --- a/tests/lib/rules/func-call-spacing.js +++ b/tests/lib/rules/func-call-spacing.js @@ -218,6 +218,28 @@ ruleTester.run("func-call-spacing", rule, { code: "import\n(source)", options: ["always", { allowNewlines: true }], parserOptions: { ecmaVersion: 2020 } + }, + + // Optional chaining + { + code: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "func ?.()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "func?. ()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "func ?. ()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 } } ], invalid: [ @@ -560,7 +582,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f\n();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, @@ -572,7 +594,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f\n(a, b);", - output: "f (a, b);", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, @@ -593,7 +615,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f.b\n();", - output: "f.b ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [ { @@ -614,7 +636,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f.b\n().c ();", - output: "f.b ().c ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [ { @@ -635,13 +657,13 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f\n() ()", - output: "f () ()", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, { code: "f\n()()", - output: "f () ()", + output: "f\n() ()", // Don't fix the first error to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [ { messageId: "unexpectedNewline", type: "CallExpression" }, @@ -696,25 +718,25 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "f\r();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, { code: "f\u2028();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, { code: "f\u2029();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, { code: "f\r\n();", - output: "f ();", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [{ messageId: "unexpectedNewline", type: "CallExpression" }] }, @@ -841,7 +863,7 @@ ruleTester.run("func-call-spacing", rule, { }, { code: "fnn\n (a, b);", - output: "fnn (a, b);", + output: null, // Don't fix to avoid hiding no-unexpected-multiline (https://github.com/eslint/eslint/issues/7787) options: ["always"], errors: [ { @@ -853,6 +875,96 @@ ruleTester.run("func-call-spacing", rule, { endColumn: 2 } ] + }, + { + code: "f /*comment*/ ()", + output: null, // Don't remove comments + options: ["never"], + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "f /*\n*/ ()", + output: null, // Don't remove comments + options: ["never"], + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "f/*comment*/()", + output: "f/*comment*/ ()", + options: ["always"], + errors: [{ messageId: "missing" }] + }, + + // Optional chaining + { + code: "func ?.()", + output: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func?. ()", + output: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func ?. ()", + output: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func\n?.()", + output: "func?.()", + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func\n//comment\n?.()", + output: null, // Don't remove comments + options: ["never"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace" }] + }, + { + code: "func?.()", + output: null, // Not sure inserting a space into either before/after `?.`. + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missing" }] + }, + { + code: "func\n ?.()", + output: "func ?.()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedNewline" }] + }, + { + code: "func?.\n ()", + output: "func?. ()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedNewline" }] + }, + { + code: "func ?.\n ()", + output: "func ?. ()", + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedNewline" }] + }, + { + code: "func\n /*comment*/ ?.()", + output: null, // Don't remove comments + options: ["always"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedNewline" }] } ] }); diff --git a/tests/lib/rules/func-name-matching.js b/tests/lib/rules/func-name-matching.js index 72ee66b41ed..4add17ea9d0 100644 --- a/tests/lib/rules/func-name-matching.js +++ b/tests/lib/rules/func-name-matching.js @@ -457,6 +457,79 @@ ruleTester.run("func-name-matching", rule, { errors: [ { messageId: "matchProperty", data: { funcName: "bar", name: "value" } } ] + }, + + // Optional chaining + { + code: "(obj?.aaa).foo = function bar() {};", + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "bar", name: "foo" } } + ] + }, + { + code: "Object?.defineProperty(foo, 'bar', { value: function baz() {} })", + options: ["always", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "baz", name: "bar" } } + ] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { value: function baz() {} })", + options: ["always", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "baz", name: "bar" } } + ] + }, + { + code: "Object?.defineProperty(foo, 'bar', { value: function bar() {} })", + options: ["never", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "notMatchProperty", data: { funcName: "bar", name: "bar" } } + ] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { value: function bar() {} })", + options: ["never", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "notMatchProperty", data: { funcName: "bar", name: "bar" } } + ] + }, + { + code: "Object?.defineProperties(foo, { bar: { value: function baz() {} } })", + options: ["always", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "baz", name: "bar" } } + ] + }, + { + code: "(Object?.defineProperties)(foo, { bar: { value: function baz() {} } })", + options: ["always", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "baz", name: "bar" } } + ] + }, + { + code: "Object?.defineProperties(foo, { bar: { value: function bar() {} } })", + options: ["never", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "notMatchProperty", data: { funcName: "bar", name: "bar" } } + ] + }, + { + code: "(Object?.defineProperties)(foo, { bar: { value: function bar() {} } })", + options: ["never", { considerPropertyDescriptor: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "notMatchProperty", data: { funcName: "bar", name: "bar" } } + ] } ] }); diff --git a/tests/lib/rules/getter-return.js b/tests/lib/rules/getter-return.js index ea181650dee..7d7cce078ae 100644 --- a/tests/lib/rules/getter-return.js +++ b/tests/lib/rules/getter-return.js @@ -223,6 +223,30 @@ ruleTester.run("getter-return", rule, { { code: "Object.defineProperties(foo, { bar: { get: function () {~function () { return true; }()}} });", options, errors: [{ messageId: "expected" }] }, // option: {allowImplicit: true} - { code: "Object.defineProperty(foo, \"bar\", { get: function (){}});", options, errors: [{ messageId: "expected" }] } + { code: "Object.defineProperty(foo, \"bar\", { get: function (){}});", options, errors: [{ messageId: "expected" }] }, + + // Optional chaining + { + code: "Object?.defineProperty(foo, 'bar', { get: function (){} });", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expected", data: { name: "method 'get'" } }] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { get: function (){} });", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expected", data: { name: "method 'get'" } }] + }, + { + code: "Object?.defineProperty(foo, 'bar', { get: function (){} });", + options, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expected", data: { name: "method 'get'" } }] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { get: function (){} });", + options, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expected", data: { name: "method 'get'" } }] + } ] }); diff --git a/tests/lib/rules/global-require.js b/tests/lib/rules/global-require.js index 3fb6ccca459..4993962bd05 100644 --- a/tests/lib/rules/global-require.js +++ b/tests/lib/rules/global-require.js @@ -30,7 +30,13 @@ const valid = [ { code: "var logger = require(DEBUG ? 'dev-logger' : 'logger');" }, { code: "var logger = DEBUG ? require('dev-logger') : require('logger');" }, { code: "function localScopedRequire(require) { require('y'); }" }, - { code: "var someFunc = require('./someFunc'); someFunc(function(require) { return('bananas'); });" } + { code: "var someFunc = require('./someFunc'); someFunc(function(require) { return('bananas'); });" }, + + // Optional chaining + { + code: "var x = require('y')?.foo;", + parserOptions: { ecmaVersion: 2020 } + } ]; const error = { messageId: "unexpected", type: "CallExpression" }; diff --git a/tests/lib/rules/id-blacklist.js b/tests/lib/rules/id-blacklist.js index e69de29bb2d..4d13459acf6 100644 --- a/tests/lib/rules/id-blacklist.js +++ b/tests/lib/rules/id-blacklist.js @@ -0,0 +1,1359 @@ +/** + * @fileoverview Tests for id-blacklist rule. + * @author Keith Cirkel + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/id-blacklist"), + { RuleTester } = require("../../../lib/rule-tester"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); +const error = { messageId: "restricted", type: "Identifier" }; + +ruleTester.run("id-blacklist", rule, { + valid: [ + { + code: "foo = \"bar\"", + options: ["bar"] + }, + { + code: "bar = \"bar\"", + options: ["foo"] + }, + { + code: "foo = \"bar\"", + options: ["f", "fo", "fooo", "bar"] + }, + { + code: "function foo(){}", + options: ["bar"] + }, + { + code: "foo()", + options: ["f", "fo", "fooo", "bar"] + }, + { + code: "import { foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" } + }, + { + code: "export { foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" } + }, + { + code: "foo.bar()", + options: ["f", "fo", "fooo", "b", "ba", "baz"] + }, + { + code: "var foo = bar.baz;", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz"] + }, + { + code: "var foo = bar.baz.bing;", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "foo.bar.baz = bing.bong.bash;", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "if (foo.bar) {}", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "var obj = { key: foo.bar };", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "const {foo: bar} = baz", + options: ["foo"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "const {foo: {bar: baz}} = qux", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({ bar: baz }) {}", + options: ["bar"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({ bar: {baz: qux} }) {}", + options: ["bar", "baz"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({baz} = obj.qux) {}", + options: ["qux"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function foo({ foo: {baz} = obj.qux }) {}", + options: ["qux"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "({a: bar = obj.baz});", + options: ["baz"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "({foo: {a: bar = obj.baz}} = qux);", + options: ["baz"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "var arr = [foo.bar];", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "[foo.bar]", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "[foo.bar.nesting]", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "if (foo.bar === bar.baz) { [foo.bar] }", + options: ["f", "fo", "fooo", "b", "ba", "barr", "bazz", "bingg"] + }, + { + code: "var myArray = new Array(); var myDate = new Date();", + options: ["array", "date", "mydate", "myarray", "new", "var"] + }, + { + code: "foo()", + options: ["foo"] + }, + { + code: "foo.bar()", + options: ["bar"] + }, + { + code: "foo.bar", + options: ["bar"] + }, + { + code: "({foo: obj.bar.bar.bar.baz} = {});", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "({[obj.bar]: a = baz} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 } + }, + + // references to global variables + { + code: "Number.parseInt()", + options: ["Number"] + }, + { + code: "x = Number.NaN;", + options: ["Number"] + }, + { + code: "var foo = undefined;", + options: ["undefined"] + }, + { + code: "if (foo === undefined);", + options: ["undefined"] + }, + { + code: "obj[undefined] = 5;", // creates obj["undefined"]. It should be disallowed, but the rule doesn't know values of globals and can't control computed access. + options: ["undefined"] + }, + { + code: "foo = { [myGlobal]: 1 };", + options: ["myGlobal"], + parserOptions: { ecmaVersion: 6 }, + globals: { myGlobal: "readonly" } + }, + { + code: "({ myGlobal } = foo);", // writability doesn't affect the logic, it's always assumed that user doesn't have control over the names of globals. + options: ["myGlobal"], + parserOptions: { ecmaVersion: 6 }, + globals: { myGlobal: "writable" } + }, + { + code: "/* global myGlobal: readonly */ myGlobal = 5;", + options: ["myGlobal"] + }, + { + code: "var foo = [Map];", + options: ["Map"], + env: { es6: true } + }, + { + code: "var foo = { bar: window.baz };", + options: ["window"], + env: { browser: true } + } + ], + invalid: [ + { + code: "foo = \"bar\"", + options: ["foo"], + errors: [ + error + ] + }, + { + code: "bar = \"bar\"", + options: ["bar"], + errors: [ + error + ] + }, + { + code: "foo = \"bar\"", + options: ["f", "fo", "foo", "bar"], + errors: [ + error + ] + }, + { + code: "function foo(){}", + options: ["f", "fo", "foo", "bar"], + errors: [ + error + ] + }, + { + code: "import foo from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "import * as foo from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "export * as foo from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "import { foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "import { foo as bar } from 'mod'", + options: ["bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "import { foo as bar } from 'mod'", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "import { foo as foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "import { foo, foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + }] + }, + { + code: "import { foo as bar, foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 22 + }] + }, + { + code: "import foo, { foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 8 + }] + }, + { + code: "var foo; export { foo as bar };", + options: ["bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 26 + }] + }, + { + code: "var foo; export { foo };", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "var foo; export { foo as bar };", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + + // reports each occurrence of local identifier, although it's renamed in this export specifier + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "var foo; export { foo as foo };", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 26 + } + ] + }, + { + code: "var foo; export { foo as bar };", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 19 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 26 + } + ] + }, + { + code: "export { foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + error + ] + }, + { + code: "export { foo as bar } from 'mod'", + options: ["bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "export { foo as bar } from 'mod'", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "export { foo as foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 17 + }] + }, + { + code: "export { foo, foo as bar } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + }] + }, + { + code: "export { foo as bar, foo } from 'mod'", + options: ["foo"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [{ + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 22 + }] + }, + { + code: "foo.bar()", + options: ["f", "fo", "foo", "b", "ba", "baz"], + errors: [ + error + ] + }, + { + code: "foo[bar] = baz;", + options: ["bar"], + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier" + }] + }, + { + code: "baz = foo[bar];", + options: ["bar"], + errors: [{ + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier" + }] + }, + { + code: "var foo = bar.baz;", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz"], + errors: [ + error + ] + }, + { + code: "var foo = bar.baz;", + options: ["f", "fo", "fooo", "b", "ba", "bar", "bazz"], + errors: [ + error + ] + }, + { + code: "if (foo.bar) {}", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "var obj = { key: foo.bar };", + options: ["obj"], + errors: [ + error + ] + }, + { + code: "var obj = { key: foo.bar };", + options: ["key"], + errors: [ + error + ] + }, + { + code: "var obj = { key: foo.bar };", + options: ["foo"], + errors: [ + error + ] + }, + { + code: "var arr = [foo.bar];", + options: ["arr"], + errors: [ + error + ] + }, + { + code: "var arr = [foo.bar];", + options: ["foo"], + errors: [ + error + ] + }, + { + code: "[foo.bar]", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "if (foo.bar === bar.baz) { [bing.baz] }", + options: ["f", "fo", "foo", "b", "ba", "barr", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "if (foo.bar === bar.baz) { [foo.bar] }", + options: ["f", "fo", "fooo", "b", "ba", "bar", "bazz", "bingg"], + errors: [ + error + ] + }, + { + code: "var myArray = new Array(); var myDate = new Date();", + options: ["array", "date", "myDate", "myarray", "new", "var"], + errors: [ + error + ] + }, + { + code: "var myArray = new Array(); var myDate = new Date();", + options: ["array", "date", "mydate", "myArray", "new", "var"], + errors: [ + error + ] + }, + { + code: "foo.bar = 1", + options: ["bar"], + errors: [ + error + ] + }, + { + code: "foo.bar.baz = 1", + options: ["bar", "baz"], + errors: [ + error + ] + }, + { + code: "const {foo} = baz", + options: ["foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 8 + } + ] + }, + { + code: "const {foo: bar} = baz", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 13 + } + ] + }, + { + code: "const {[foo]: bar} = baz", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 9 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 15 + } + ] + }, + { + code: "const {foo: {bar: baz}} = qux", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "const {foo: {[bar]: baz}} = qux", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 15 + }, + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 21 + } + ] + }, + { + code: "const {[foo]: {[bar]: baz}} = qux", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 9 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + }, + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 23 + } + ] + }, + { + code: "function foo({ bar: baz }) {}", + options: ["bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 21 + } + ] + }, + { + code: "function foo({ bar: {baz: qux} }) {}", + options: ["bar", "baz", "qux"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "qux" }, + type: "Identifier", + column: 27 + } + ] + }, + { + code: "({foo: obj.bar} = baz);", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 12 + } + ] + }, + { + code: "({foo: obj.bar.bar.bar.baz} = {});", + options: ["foo", "bar", "baz"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 24 + } + ] + }, + { + code: "({[foo]: obj.bar} = baz);", + options: ["foo", "bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 4 + }, + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 14 + } + ] + }, + { + code: "({foo: { a: obj.bar }} = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + } + ] + }, + { + code: "({a: obj.bar = baz} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "({a: obj.bar.bar.baz = obj.qux} = obj.qux);", + options: ["a", "bar", "baz", "qux"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "baz" }, + type: "Identifier", + column: 18 + } + ] + }, + { + code: "({a: obj[bar] = obj.qux} = obj.qux);", + options: ["a", "bar", "baz", "qux"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "({a: [obj.bar] = baz} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 11 + } + ] + }, + { + code: "({foo: { a: obj.bar = baz}} = qux);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 17 + } + ] + }, + { + code: "({foo: { [a]: obj.bar }} = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 19 + } + ] + }, + { + code: "({...obj.bar} = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 9 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "([obj.bar] = baz);", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 7 + } + ] + }, + { + code: "const [bar] = baz;", + options: ["bar"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "bar" }, + type: "Identifier", + column: 8 + } + ] + }, + + // not a reference to a global variable, because it isn't a reference to a variable + { + code: "foo.undefined = 1;", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = { undefined: 1 };", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = { undefined: undefined };", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 13 + } + ] + }, + { + code: "var foo = { Number() {} };", + options: ["Number"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier" + } + ] + }, + { + code: "class Foo { Number() {} }", + options: ["Number"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier" + } + ] + }, + { + code: "myGlobal: while(foo) { break myGlobal; } ", + options: ["myGlobal"], + globals: { myGlobal: "readonly" }, + errors: [ + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 1 + }, + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 30 + } + ] + }, + + // globals declared in the given source code are not excluded from consideration + { + code: "const foo = 1; bar = foo;", + options: ["foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 22 + } + ] + }, + { + code: "let foo; foo = bar;", + options: ["foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + } + ] + }, + { + code: "bar = foo; var foo;", + options: ["foo"], + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 16 + } + ] + }, + { + code: "function foo() {} var bar = foo;", + options: ["foo"], + errors: [ + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 10 + }, + { + messageId: "restricted", + data: { name: "foo" }, + type: "Identifier", + column: 29 + } + ] + }, + { + code: "class Foo {} var bar = Foo;", + options: ["Foo"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Foo" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "Foo" }, + type: "Identifier", + column: 24 + } + ] + }, + + // redeclared globals are not excluded from consideration + { + code: "let undefined; undefined = 1;", + options: ["undefined"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 5 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 16 + } + ] + }, + { + code: "foo = undefined; var undefined;", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 22 + } + ] + }, + { + code: "function undefined(){} x = undefined;", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 10 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 28 + } + ] + }, + { + code: "class Number {} x = Number.NaN;", + options: ["Number"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 7 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 21 + } + ] + }, + + /* + * Assignment to a property with a restricted name isn't allowed, in general. + * In this case, that restriction prevents creating a global variable with a restricted name. + */ + { + code: "/* globals myGlobal */ window.myGlobal = 5; foo = myGlobal;", + options: ["myGlobal"], + env: { browser: true }, + errors: [ + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 31 + } + ] + }, + + // disabled global variables + { + code: "var foo = undefined;", + options: ["undefined"], + globals: { undefined: "off" }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + { + code: "/* globals Number: off */ Number.parseInt()", + options: ["Number"], + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier" + } + ] + }, + { + code: "var foo = [Map];", // this actually isn't a disabled global: it was never enabled because es6 environment isn't enabled + options: ["Map"], + errors: [ + { + messageId: "restricted", + data: { name: "Map" }, + type: "Identifier" + } + ] + }, + + // shadowed global variables + { + code: "if (foo) { let undefined; bar = undefined; }", + options: ["undefined"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 16 + }, + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier", + column: 33 + } + ] + }, + { + code: "function foo(Number) { var x = Number.NaN; }", + options: ["Number"], + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 14 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 32 + } + ] + }, + { + code: "function foo() { var myGlobal; x = myGlobal; }", + options: ["myGlobal"], + globals: { myGlobal: "readonly" }, + errors: [ + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 22 + }, + { + messageId: "restricted", + data: { name: "myGlobal" }, + type: "Identifier", + column: 36 + } + ] + }, + { + code: "function foo(bar) { return Number.parseInt(bar); } const Number = 1;", + options: ["Number"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 28 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 58 + } + ] + }, + { + code: "import Number from 'myNumber'; const foo = Number.parseInt(bar);", + options: ["Number"], + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + errors: [ + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 8 + }, + { + messageId: "restricted", + data: { name: "Number" }, + type: "Identifier", + column: 44 + } + ] + }, + { + code: "var foo = function undefined() {};", + options: ["undefined"], + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + }, + + // this is a reference to a global variable, but at the same time creates a property with a restricted name + { + code: "var foo = { undefined }", + options: ["undefined"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "restricted", + data: { name: "undefined" }, + type: "Identifier" + } + ] + } + ] +}); diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js index 56589be5169..b253be22067 100644 --- a/tests/lib/rules/indent.js +++ b/tests/lib/rules/indent.js @@ -11374,6 +11374,105 @@ ruleTester.run("indent", rule, { [5, 4, 0, "Identifier"], [6, 0, 4, "Punctuator"] ]) + }, + + // Optional chaining + { + code: unIndent` + obj + ?.prop + ?.[key] + ?. + [key] + `, + output: unIndent` + obj + ?.prop + ?.[key] + ?. + [key] + `, + options: [4], + parserOptions: { ecmaVersion: 2020 }, + errors: expectedErrors([ + [2, 4, 0, "Punctuator"], + [3, 4, 0, "Punctuator"], + [4, 4, 0, "Punctuator"], + [5, 8, 0, "Punctuator"] + ]) + }, + { + code: unIndent` + ( + longSomething + ?.prop + ?.[key] + ) + ?.prop + ?.[key] + `, + output: unIndent` + ( + longSomething + ?.prop + ?.[key] + ) + ?.prop + ?.[key] + `, + options: [4], + parserOptions: { ecmaVersion: 2020 }, + errors: expectedErrors([ + [6, 4, 0, "Punctuator"], + [7, 4, 0, "Punctuator"] + ]) + }, + { + code: unIndent` + obj + ?.(arg) + ?. + (arg) + `, + output: unIndent` + obj + ?.(arg) + ?. + (arg) + `, + options: [4], + parserOptions: { ecmaVersion: 2020 }, + errors: expectedErrors([ + [2, 4, 0, "Punctuator"], + [3, 4, 0, "Punctuator"], + [4, 4, 0, "Punctuator"] + ]) + }, + { + code: unIndent` + ( + longSomething + ?.(arg) + ?.(arg) + ) + ?.(arg) + ?.(arg) + `, + output: unIndent` + ( + longSomething + ?.(arg) + ?.(arg) + ) + ?.(arg) + ?.(arg) + `, + options: [4], + parserOptions: { ecmaVersion: 2020 }, + errors: expectedErrors([ + [6, 4, 0, "Punctuator"], + [7, 4, 0, "Punctuator"] + ]) } ] }); diff --git a/tests/lib/rules/new-cap.js b/tests/lib/rules/new-cap.js index ec85c4bf6fd..5953c87c24a 100644 --- a/tests/lib/rules/new-cap.js +++ b/tests/lib/rules/new-cap.js @@ -71,7 +71,39 @@ ruleTester.run("new-cap", rule, { { code: "var x = new foo.bar(42);", options: [{ newIsCapExceptionPattern: "^foo\\.." }] }, { code: "var x = new foo.bar(42);", options: [{ properties: false }] }, { code: "var x = Foo.bar(42);", options: [{ properties: false }] }, - { code: "var x = foo.Bar(42);", options: [{ capIsNew: false, properties: false }] } + { code: "var x = foo.Bar(42);", options: [{ capIsNew: false, properties: false }] }, + + // Optional chaining + { + code: "foo?.bar();", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "(foo?.bar)();", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "new (foo?.Bar)();", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "(foo?.Bar)();", + options: [{ properties: false }], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "new (foo?.bar)();", + options: [{ properties: false }], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "Date?.UTC();", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "(Date?.UTC)();", + parserOptions: { ecmaVersion: 2020 } + } ], invalid: [ { @@ -302,6 +334,23 @@ ruleTester.run("new-cap", rule, { options: [{ newIsCapExceptionPattern: "^foo\\.." }], errors: [{ type: "NewExpression", messageId: "lower" }] + }, + + // Optional chaining + { + code: "new (foo?.bar)();", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "lower", column: 11, endColumn: 14 }] + }, + { + code: "foo?.Bar();", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "upper", column: 6, endColumn: 9 }] + }, + { + code: "(foo?.Bar)();", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "upper", column: 7, endColumn: 10 }] } ] }); diff --git a/tests/lib/rules/newline-per-chained-call.js b/tests/lib/rules/newline-per-chained-call.js index 28d45c69b82..2a26937ae62 100644 --- a/tests/lib/rules/newline-per-chained-call.js +++ b/tests/lib/rules/newline-per-chained-call.js @@ -340,5 +340,69 @@ ruleTester.run("newline-per-chained-call", rule, { endLine: 1, endColumn: 35 }] - }] + }, + + // Optional chaining + { + code: "obj?.foo1()?.foo2()?.foo3()", + output: "obj?.foo1()\n?.foo2()\n?.foo3()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.foo2" } }, + { messageId: "expected", data: { callee: "?.foo3" } } + ] + }, + { + code: "(obj?.foo1()?.foo2)()?.foo3()", + output: "(obj?.foo1()\n?.foo2)()\n?.foo3()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.foo2" } }, + { messageId: "expected", data: { callee: "?.foo3" } } + ] + }, + { + code: "(obj?.foo1())?.foo2()?.foo3()", + output: "(obj?.foo1())\n?.foo2()\n?.foo3()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.foo2" } }, + { messageId: "expected", data: { callee: "?.foo3" } } + ] + }, + { + code: "obj?.[foo1]()?.[foo2]()?.[foo3]()", + output: "obj?.[foo1]()\n?.[foo2]()\n?.[foo3]()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.[foo2]" } }, + { messageId: "expected", data: { callee: "?.[foo3]" } } + ] + }, + { + code: "(obj?.[foo1]()?.[foo2])()?.[foo3]()", + output: "(obj?.[foo1]()\n?.[foo2])()\n?.[foo3]()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.[foo2]" } }, + { messageId: "expected", data: { callee: "?.[foo3]" } } + ] + }, + { + code: "(obj?.[foo1]())?.[foo2]()?.[foo3]()", + output: "(obj?.[foo1]())\n?.[foo2]()\n?.[foo3]()", + options: [{ ignoreChainWithDepth: 1 }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "expected", data: { callee: "?.[foo2]" } }, + { messageId: "expected", data: { callee: "?.[foo3]" } } + ] + } + + ] }); diff --git a/tests/lib/rules/no-alert.js b/tests/lib/rules/no-alert.js index 4851d5b0a7a..03fcddb6994 100644 --- a/tests/lib/rules/no-alert.js +++ b/tests/lib/rules/no-alert.js @@ -124,6 +124,18 @@ ruleTester.run("no-alert", rule, { code: "function foo() { var globalThis = bar; globalThis.alert(); }\nglobalThis.alert();", env: { es2020: true }, errors: [{ messageId: "unexpected", data: { name: "alert" }, type: "CallExpression", line: 2, column: 1 }] + }, + + // Optional chaining + { + code: "window?.alert(foo)", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { name: "alert" } }] + }, + { + code: "(window?.alert)(foo)", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { name: "alert" } }] } ] }); diff --git a/tests/lib/rules/no-eval.js b/tests/lib/rules/no-eval.js index ad184bd9370..bc8d38471bb 100644 --- a/tests/lib/rules/no-eval.js +++ b/tests/lib/rules/no-eval.js @@ -67,7 +67,10 @@ ruleTester.run("no-eval", rule, { { code: "(0, globalThis['eval'])('foo')", options: [{ allowIndirect: true }], env: { es2020: true } }, { code: "var EVAL = globalThis.eval; EVAL('foo')", options: [{ allowIndirect: true }] }, { code: "function foo() { globalThis.eval('foo') }", options: [{ allowIndirect: true }], env: { es2020: true } }, - { code: "globalThis.globalThis.eval('foo');", options: [{ allowIndirect: true }], env: { es2020: true } } + { code: "globalThis.globalThis.eval('foo');", options: [{ allowIndirect: true }], env: { es2020: true } }, + { code: "eval?.('foo')", options: [{ allowIndirect: true }], parserOptions: { ecmaVersion: 2020 } }, + { code: "window?.eval('foo')", options: [{ allowIndirect: true }], parserOptions: { ecmaVersion: 2020 }, env: { browser: true } }, + { code: "(window?.eval)('foo')", options: [{ allowIndirect: true }], parserOptions: { ecmaVersion: 2020 }, env: { browser: true } } ], invalid: [ @@ -100,6 +103,26 @@ ruleTester.run("no-eval", rule, { { code: "globalThis.globalThis.eval('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "CallExpression", column: 23, endColumn: 27 }] }, { code: "globalThis.globalThis['eval']('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "CallExpression", column: 23, endColumn: 29 }] }, { code: "(0, globalThis.eval)('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "MemberExpression", column: 16, endColumn: 20 }] }, - { code: "(0, globalThis['eval'])('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "MemberExpression", column: 16, endColumn: 22 }] } + { code: "(0, globalThis['eval'])('foo')", env: { es2020: true }, errors: [{ messageId: "unexpected", type: "MemberExpression", column: 16, endColumn: 22 }] }, + + // Optional chaining + { + code: "window?.eval('foo')", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "(window?.eval)('foo')", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "(window?.window).eval('foo')", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "unexpected" }] + } ] }); diff --git a/tests/lib/rules/no-extend-native.js b/tests/lib/rules/no-extend-native.js index a38177431d2..db07c123314 100644 --- a/tests/lib/rules/no-extend-native.js +++ b/tests/lib/rules/no-extend-native.js @@ -37,6 +37,7 @@ ruleTester.run("no-extend-native", rule, { code: "Object.prototype.g = 0", options: [{ exceptions: ["Object"] }] }, + "obj[Object.prototype] = 0", // https://github.com/eslint/eslint/issues/4438 "Object.defineProperty()", @@ -137,5 +138,29 @@ ruleTester.run("no-extend-native", rule, { data: { builtin: "Object" }, type: "AssignmentExpression" }] - }] + }, + + // Optional chaining + { + code: "(Object?.prototype).p = 0", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { builtin: "Object" } }] + }, + { + code: "Object.defineProperty(Object?.prototype, 'p', { value: 0 })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { builtin: "Object" } }] + }, + { + code: "Object?.defineProperty(Object.prototype, 'p', { value: 0 })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { builtin: "Object" } }] + }, + { + code: "(Object?.defineProperty)(Object.prototype, 'p', { value: 0 })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected", data: { builtin: "Object" } }] + } + + ] }); diff --git a/tests/lib/rules/no-extra-bind.js b/tests/lib/rules/no-extra-bind.js index 12197967176..8422f3de771 100644 --- a/tests/lib/rules/no-extra-bind.js +++ b/tests/lib/rules/no-extra-bind.js @@ -102,6 +102,16 @@ ruleTester.run("no-extra-bind", rule, { output: "var a = function() { (function(){ (function(){ this.d }.bind(c)) }) }", errors: [{ messageId: "unexpected", type: "CallExpression", column: 71 }] }, + { + code: "var a = (function() { return 1; }).bind(this)", + output: "var a = (function() { return 1; })", + errors + }, + { + code: "var a = (function() { return 1; }.bind)(this)", + output: "var a = (function() { return 1; })", + errors + }, // Should not autofix if bind expression args have side effects { @@ -180,6 +190,44 @@ ruleTester.run("no-extra-bind", rule, { code: "var a = function() {}.bind(b)/**/", output: "var a = function() {}/**/", errors + }, + + // Optional chaining + { + code: "var a = function() { return 1; }.bind?.(b)", + output: "var a = function() { return 1; }", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = function() { return 1; }?.bind(b)", + output: "var a = function() { return 1; }", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = (function() { return 1; }?.bind)(b)", + output: "var a = (function() { return 1; })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = function() { return 1; }['bind']?.(b)", + output: "var a = function() { return 1; }", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = function() { return 1; }?.['bind'](b)", + output: "var a = function() { return 1; }", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var a = (function() { return 1; }?.['bind'])(b)", + output: "var a = (function() { return 1; })", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] } ] }); diff --git a/tests/lib/rules/no-extra-boolean-cast.js b/tests/lib/rules/no-extra-boolean-cast.js index 70ec8bd4f61..8dda6992d21 100644 --- a/tests/lib/rules/no-extra-boolean-cast.js +++ b/tests/lib/rules/no-extra-boolean-cast.js @@ -2408,6 +2408,21 @@ ruleTester.run("no-extra-boolean-cast", rule, { options: [{ enforceForLogicalOperands: true }], parserOptions: { ecmaVersion: 2020 }, errors: [{ messageId: "unexpectedCall", type: "CallExpression" }] + }, + + // Optional chaining + { + code: "if (Boolean?.(foo)) ;", + output: "if (foo) ;", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedCall" }] + }, + { + code: "if (Boolean?.(a ?? b) || c) {}", + output: "if ((a ?? b) || c) {}", + options: [{ enforceForLogicalOperands: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedCall" }] } ] }); diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js index e2501bb45a0..6d7f8ef7da0 100644 --- a/tests/lib/rules/no-extra-parens.js +++ b/tests/lib/rules/no-extra-parens.js @@ -628,7 +628,32 @@ ruleTester.run("no-extra-parens", rule, { { code: "var v = (a || b) ?? c", parserOptions: { ecmaVersion: 2020 } }, { code: "var v = a || (b ?? c)", parserOptions: { ecmaVersion: 2020 } }, { code: "var v = (a && b) ?? c", parserOptions: { ecmaVersion: 2020 } }, - { code: "var v = a && (b ?? c)", parserOptions: { ecmaVersion: 2020 } } + { code: "var v = a && (b ?? c)", parserOptions: { ecmaVersion: 2020 } }, + + // Optional chaining + { code: "var v = (obj?.aaa).bbb", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.aaa)()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = new (obj?.aaa)()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = new (obj?.aaa)", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.aaa)`template`", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.()).bbb", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.())()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = new (obj?.())()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = new (obj?.())", parserOptions: { ecmaVersion: 2020 } }, + { code: "var v = (obj?.())`template`", parserOptions: { ecmaVersion: 2020 } }, + { code: "(obj?.aaa).bbb = 0", parserOptions: { ecmaVersion: 2020 } }, + { code: "var foo = (function(){})?.()", parserOptions: { ecmaVersion: 2020 } }, + { code: "var foo = (function(){}?.())", parserOptions: { ecmaVersion: 2020 } }, + { + code: "var foo = (function(){})?.call()", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var foo = (function(){}?.call())", + options: ["all", { enforceForFunctionPrototypeMethods: false }], + parserOptions: { ecmaVersion: 2020 } + } ], invalid: [ @@ -2715,6 +2740,34 @@ ruleTester.run("no-extra-parens", rule, { output: "var v = a | b ?? c | d", parserOptions: { ecmaVersion: 2020 }, errors: [{ messageId: "unexpected" }] + }, + + // Optional chaining + { + code: "var v = (obj?.aaa)?.aaa", + output: "var v = obj?.aaa?.aaa", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var v = (obj.aaa)?.aaa", + output: "var v = obj.aaa?.aaa", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var foo = (function(){})?.call()", + output: "var foo = function(){}?.call()", + options: ["all", { enforceForFunctionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "var foo = (function(){}?.call())", + output: "var foo = function(){}?.call()", + options: ["all", { enforceForFunctionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpected" }] } ] }); diff --git a/tests/lib/rules/no-implicit-coercion.js b/tests/lib/rules/no-implicit-coercion.js index 73bfe7df6d8..fa2b68b4975 100644 --- a/tests/lib/rules/no-implicit-coercion.js +++ b/tests/lib/rules/no-implicit-coercion.js @@ -355,6 +355,28 @@ ruleTester.run("no-implicit-coercion", rule, { data: { recommendation: "String(1n)" }, type: "BinaryExpression" }] + }, + + // Optional chaining + { + code: "~foo?.indexOf(1)", + output: null, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "useRecommendation", + data: { recommendation: "foo?.indexOf(1) >= 0" }, + type: "UnaryExpression" + }] + }, + { + code: "~(foo?.indexOf)(1)", + output: null, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "useRecommendation", + data: { recommendation: "(foo?.indexOf)(1) !== -1" }, + type: "UnaryExpression" + }] } ] }); diff --git a/tests/lib/rules/no-implied-eval.js b/tests/lib/rules/no-implied-eval.js index 1c712f450f0..ce06f662043 100644 --- a/tests/lib/rules/no-implied-eval.js +++ b/tests/lib/rules/no-implied-eval.js @@ -235,6 +235,20 @@ ruleTester.run("no-implied-eval", rule, { line: 3 } ] + }, + + // Optional chaining + { + code: "window?.setTimeout('code', 0)", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "impliedEval" }] + }, + { + code: "(window?.setTimeout)('code', 0)", + parserOptions: { ecmaVersion: 2020 }, + globals: { window: "readonly" }, + errors: [{ messageId: "impliedEval" }] } ] }); diff --git a/tests/lib/rules/no-import-assign.js b/tests/lib/rules/no-import-assign.js index ab65451e33a..babfdfc3445 100644 --- a/tests/lib/rules/no-import-assign.js +++ b/tests/lib/rules/no-import-assign.js @@ -310,6 +310,23 @@ ruleTester.run("no-import-assign", rule, { { code: "import mod, * as mod_ns from 'mod'; mod.prop = 0; mod_ns.prop = 0", errors: [{ messageId: "readonlyMember", data: { name: "mod_ns" }, column: 51 }] + }, + + // Optional chaining + { + code: "import * as mod from 'mod'; Object?.defineProperty(mod, key, d)", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "readonlyMember", data: { name: "mod" }, column: 29 }] + }, + { + code: "import * as mod from 'mod'; (Object?.defineProperty)(mod, key, d)", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "readonlyMember", data: { name: "mod" }, column: 29 }] + }, + { + code: "import * as mod from 'mod'; delete mod?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "readonlyMember", data: { name: "mod" }, column: 29 }] } ] }); diff --git a/tests/lib/rules/no-invalid-this.js b/tests/lib/rules/no-invalid-this.js index 3662a836766..3eb4e7b0960 100644 --- a/tests/lib/rules/no-invalid-this.js +++ b/tests/lib/rules/no-invalid-this.js @@ -366,6 +366,12 @@ const patterns = [ invalid: [USE_STRICT, IMPLIED_STRICT, MODULES], errors }, + { + code: "obj.foo = (function() { return function() { console.log(this); z(x => console.log(x, this)); }; })?.();", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, // Class Instance Methods. { @@ -421,6 +427,24 @@ const patterns = [ valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], invalid: [] }, + { + code: "var foo = function() { console.log(this); z(x => console.log(x, this)); }?.bind(obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "var foo = (function() { console.log(this); z(x => console.log(x, this)); }?.bind)(obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "var foo = function() { console.log(this); z(x => console.log(x, this)); }.bind?.(obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, // Array methods. { @@ -534,6 +558,30 @@ const patterns = [ valid: [NORMAL], invalid: [USE_STRICT, IMPLIED_STRICT, MODULES] }, + { + code: "Array?.from([], function() { console.log(this); z(x => console.log(x, this)); }, obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "foo?.every(function() { console.log(this); z(x => console.log(x, this)); }, obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "(Array?.from)([], function() { console.log(this); z(x => console.log(x, this)); }, obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "(foo?.every)(function() { console.log(this); z(x => console.log(x, this)); }, obj);", + parserOptions: { ecmaVersion: 2020 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, // @this tag. { diff --git a/tests/lib/rules/no-magic-numbers.js b/tests/lib/rules/no-magic-numbers.js index da75571ec28..a47b2a10555 100644 --- a/tests/lib/rules/no-magic-numbers.js +++ b/tests/lib/rules/no-magic-numbers.js @@ -213,6 +213,25 @@ ruleTester.run("no-magic-numbers", rule, { code: "f(-100n)", options: [{ ignore: ["-100n"] }], parserOptions: { ecmaVersion: 2020 } + }, + + // Optional chaining + { + code: "var x = parseInt?.(y, 10);", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var x = Number?.parseInt(y, 10);", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var x = (Number?.parseInt)(y, 10);", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "foo?.[777]", + options: [{ ignoreArrayIndexes: true }], + parserOptions: { ecmaVersion: 2020 } } ], invalid: [ diff --git a/tests/lib/rules/no-obj-calls.js b/tests/lib/rules/no-obj-calls.js index 4c21d9be4c8..6270e7a7280 100644 --- a/tests/lib/rules/no-obj-calls.js +++ b/tests/lib/rules/no-obj-calls.js @@ -315,6 +315,18 @@ ruleTester.run("no-obj-calls", rule, { code: "var foo = window.Atomics; new foo;", env: { es2020: true, browser: true }, errors: [{ messageId: "unexpectedRefCall", data: { name: "foo", ref: "Atomics" }, type: "NewExpression" }] + }, + + // Optional chaining + { + code: "var x = globalThis?.Reflect();", + env: { es2020: true }, + errors: [{ messageId: "unexpectedCall", data: { name: "Reflect" }, type: "CallExpression" }] + }, + { + code: "var x = (globalThis?.Reflect)();", + env: { es2020: true }, + errors: [{ messageId: "unexpectedCall", data: { name: "Reflect" }, type: "CallExpression" }] } ] }); diff --git a/tests/lib/rules/no-prototype-builtins.js b/tests/lib/rules/no-prototype-builtins.js index e4f0fa30f78..8f57545bef9 100644 --- a/tests/lib/rules/no-prototype-builtins.js +++ b/tests/lib/rules/no-prototype-builtins.js @@ -94,6 +94,18 @@ const invalid = [ data: { prop: "isPrototypeOf" }, type: "CallExpression" }] + }, + + // Optional chaining + { + code: "foo?.hasOwnProperty('bar')", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" } }] + }, + { + code: "(foo?.hasOwnProperty)('bar')", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" } }] } ]; diff --git a/tests/lib/rules/no-restricted-syntax.js b/tests/lib/rules/no-restricted-syntax.js index c8ba48fb8ec..cf8bc412366 100644 --- a/tests/lib/rules/no-restricted-syntax.js +++ b/tests/lib/rules/no-restricted-syntax.js @@ -128,6 +128,35 @@ ruleTester.run("no-restricted-syntax", rule, { code: "console.log(/a/i);", options: ["Literal[regex.flags=/./]"], errors: [{ messageId: "restrictedSyntax", data: { message: "Using 'Literal[regex.flags=/./]' is not allowed." }, type: "Literal" }] + }, + + // Optional chaining + { + code: "var foo = foo?.bar?.();", + options: ["ChainExpression"], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "restrictedSyntax", data: { message: "Using 'ChainExpression' is not allowed." }, type: "ChainExpression" }] + }, + { + code: "var foo = foo?.bar?.();", + options: ["[optional=true]"], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "restrictedSyntax", data: { message: "Using '[optional=true]' is not allowed." }, type: "CallExpression" }, + { messageId: "restrictedSyntax", data: { message: "Using '[optional=true]' is not allowed." }, type: "MemberExpression" } + ] } + + /* + * TODO(mysticatea): fix https://github.com/estools/esquery/issues/110 + * { + * code: "a?.b", + * options: [":nth-child(1)"], + * parserOptions: { ecmaVersion: 2020 }, + * errors: [ + * { messageId: "restrictedSyntax", data: { message: "Using ':nth-child(1)' is not allowed." }, type: "ExpressionStatement" } + * ] + * } + */ ] }); diff --git a/tests/lib/rules/no-self-assign.js b/tests/lib/rules/no-self-assign.js index 5149dac65b2..5a9bb6fcfed 100644 --- a/tests/lib/rules/no-self-assign.js +++ b/tests/lib/rules/no-self-assign.js @@ -135,6 +135,18 @@ ruleTester.run("no-self-assign", rule, { options: [{ props: true }], errors: [{ messageId: "selfAssignment", data: { name: "this.x" } }] }, - { code: "a['/(?0)/'] = a[/(?0)/]", options: [{ props: true }], parserOptions: { ecmaVersion: 2018 }, errors: [{ messageId: "selfAssignment", data: { name: "a[/(?0)/]" } }] } + { code: "a['/(?0)/'] = a[/(?0)/]", options: [{ props: true }], parserOptions: { ecmaVersion: 2018 }, errors: [{ messageId: "selfAssignment", data: { name: "a[/(?0)/]" } }] }, + + // Optional chaining + { + code: "(a?.b).c = (a?.b).c", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "selfAssignment", data: { name: "(a?.b).c" } }] + }, + { + code: "a.b = a?.b", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "selfAssignment", data: { name: "a?.b" } }] + } ] }); diff --git a/tests/lib/rules/no-setter-return.js b/tests/lib/rules/no-setter-return.js index b70b4e3f1c0..0c64e8b4811 100644 --- a/tests/lib/rules/no-setter-return.js +++ b/tests/lib/rules/no-setter-return.js @@ -39,7 +39,7 @@ function error(column, type = "ReturnStatement") { // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("no-setter-return", rule, { valid: [ @@ -505,6 +505,18 @@ ruleTester.run("no-setter-return", rule, { { code: "Object.defineProperty(foo, 'bar', { set: function(Object) { return 1; } })", errors: [error()] + }, + + // Optional chaining + { + code: "Object?.defineProperty(foo, 'bar', { set(val) { return 1; } })", + parserOptions: { ecmaVersion: 2020 }, + errors: [error()] + }, + { + code: "(Object?.defineProperty)(foo, 'bar', { set(val) { return 1; } })", + parserOptions: { ecmaVersion: 2020 }, + errors: [error()] } ] }); diff --git a/tests/lib/rules/no-throw-literal.js b/tests/lib/rules/no-throw-literal.js index 05002117793..7836cec7837 100644 --- a/tests/lib/rules/no-throw-literal.js +++ b/tests/lib/rules/no-throw-literal.js @@ -38,7 +38,9 @@ ruleTester.run("no-throw-literal", rule, { "throw foo ? 'literal' : new Error();", // ConditionalExpression (alternate) { code: "throw tag `${foo}`;", parserOptions: { ecmaVersion: 6 } }, // TaggedTemplateExpression { code: "function* foo() { var index = 0; throw yield index++; }", parserOptions: { ecmaVersion: 6 } }, // YieldExpression - { code: "async function foo() { throw await bar; }", parserOptions: { ecmaVersion: 8 } } // AwaitExpression + { code: "async function foo() { throw await bar; }", parserOptions: { ecmaVersion: 8 } }, // AwaitExpression + { code: "throw obj?.foo", parserOptions: { ecmaVersion: 2020 } }, // ChainExpression + { code: "throw obj?.foo()", parserOptions: { ecmaVersion: 2020 } } // ChainExpression ], invalid: [ { diff --git a/tests/lib/rules/no-unexpected-multiline.js b/tests/lib/rules/no-unexpected-multiline.js index 29d05a9215b..83c7bf67570 100644 --- a/tests/lib/rules/no-unexpected-multiline.js +++ b/tests/lib/rules/no-unexpected-multiline.js @@ -122,6 +122,24 @@ ruleTester.run("no-unexpected-multiline", rule, { >\`multiline\`; `, parser: require.resolve("../../fixtures/parsers/typescript-parsers/tagged-template-with-generic/tagged-template-with-generic-3") + }, + + // Optional chaining + { + code: "var a = b\n ?.(x || y).doSomething()", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var a = b\n ?.[a, b, c].forEach(doSomething)", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var a = b?.\n (x || y).doSomething()", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "var a = b?.\n [a, b, c].forEach(doSomething)", + parserOptions: { ecmaVersion: 2020 } } ], invalid: [ diff --git a/tests/lib/rules/no-unused-expressions.js b/tests/lib/rules/no-unused-expressions.js index 8ef2028c662..1674629c90c 100644 --- a/tests/lib/rules/no-unused-expressions.js +++ b/tests/lib/rules/no-unused-expressions.js @@ -74,6 +74,14 @@ ruleTester.run("no-unused-expressions", rule, { { code: "import(\"foo\")", parserOptions: { ecmaVersion: 11 } + }, + { + code: "func?.(\"foo\")", + parserOptions: { ecmaVersion: 11 } + }, + { + code: "obj?.foo(\"bar\")", + parserOptions: { ecmaVersion: 11 } } ], invalid: [ @@ -127,6 +135,23 @@ ruleTester.run("no-unused-expressions", rule, { options: [{ allowTaggedTemplates: false }], parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "unusedExpression" }] + }, + + // Optional chaining + { + code: "obj?.foo", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unusedExpression", type: "ExpressionStatement" }] + }, + { + code: "obj?.foo.bar", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unusedExpression", type: "ExpressionStatement" }] + }, + { + code: "obj?.foo().bar", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unusedExpression", type: "ExpressionStatement" }] } ] }); diff --git a/tests/lib/rules/no-useless-call.js b/tests/lib/rules/no-useless-call.js index 8713ee92c4d..528f2f1b129 100644 --- a/tests/lib/rules/no-useless-call.js +++ b/tests/lib/rules/no-useless-call.js @@ -44,7 +44,13 @@ ruleTester.run("no-useless-call", rule, { "foo.call();", "obj.foo.call();", "foo.apply();", - "obj.foo.apply();" + "obj.foo.apply();", + + // Optional chaining + { + code: "obj?.foo.bar.call(obj.foo, 1, 2);", + parserOptions: { ecmaVersion: 2020 } + } ], invalid: [ @@ -170,6 +176,86 @@ ruleTester.run("no-useless-call", rule, { data: { name: "apply" }, type: "CallExpression" }] + }, + + // Optional chaining + { + code: "foo.call?.(undefined, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unnecessaryCall", data: { name: "call" } }] + }, + { + code: "foo?.call(undefined, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unnecessaryCall", data: { name: "call" } }] + }, + { + code: "(foo?.call)(undefined, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unnecessaryCall", data: { name: "call" } }] + }, + { + code: "obj.foo.call?.(obj, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "obj?.foo.call(obj, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "(obj?.foo).call(obj, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "(obj?.foo.call)(obj, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "obj?.foo.bar.call(obj?.foo, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "(obj?.foo).bar.call(obj?.foo, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] + }, + { + code: "obj.foo?.bar.call(obj.foo, 1, 2);", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "unnecessaryCall", + data: { name: "call" }, + type: "CallExpression" + }] } ] }); diff --git a/tests/lib/rules/no-whitespace-before-property.js b/tests/lib/rules/no-whitespace-before-property.js index d10b30e20e8..31d50449583 100644 --- a/tests/lib/rules/no-whitespace-before-property.js +++ b/tests/lib/rules/no-whitespace-before-property.js @@ -99,7 +99,45 @@ ruleTester.run("no-whitespace-before-property", rule, { "foo[bar.baz('qux')]", "foo[(bar.baz() + 0) + qux]", "foo['bar ' + 1 + ' baz']", - "5['toExponential']()" + "5['toExponential']()", + + // Optional chaining + { + code: "obj?.prop", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "( obj )?.prop", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n ?.prop", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\n prop", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.[key]", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "( obj )?.[ key ]", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n ?.[key]", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj?.\n [key]", + parserOptions: { ecmaVersion: 2020 } + }, + { + code: "obj\n ?.\n [key]", + parserOptions: { ecmaVersion: 2020 } + } ], invalid: [ @@ -859,6 +897,56 @@ ruleTester.run("no-whitespace-before-property", rule, { messageId: "unexpectedWhitespace", data: { propName: "toExponential" } }] + }, + + // Optional chaining + { + code: "obj?. prop", + output: "obj?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] + }, + { + code: "obj ?.prop", + output: "obj?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] + }, + { + code: "obj?. [key]", + output: "obj?.[key]", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "key" } }] + }, + { + code: "obj ?.[key]", + output: "obj?.[key]", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "key" } }] + }, + { + code: "5 ?. prop", + output: "5?.prop", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] + }, + { + code: "5 ?. [key]", + output: "5?.[key]", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "key" } }] + }, + { + code: "obj/* comment */?. prop", + output: null, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] + }, + { + code: "obj ?./* comment */prop", + output: null, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedWhitespace", data: { propName: "prop" } }] } ] }); diff --git a/tests/lib/rules/operator-assignment.js b/tests/lib/rules/operator-assignment.js index f690f7eb7f9..ca7dfce6911 100644 --- a/tests/lib/rules/operator-assignment.js +++ b/tests/lib/rules/operator-assignment.js @@ -16,7 +16,7 @@ const rule = require("../../../lib/rules/operator-assignment"), // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 7 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); const EXPECTED_OPERATOR_ASSIGNMENT = [{ messageId: "replaced", type: "AssignmentExpression" }]; const UNEXPECTED_OPERATOR_ASSIGNMENT = [{ messageId: "unexpected", type: "AssignmentExpression" }]; @@ -398,6 +398,20 @@ ruleTester.run("operator-assignment", rule, { output: "foo= foo+(+bar===baz)", // tokens cannot be adjacent, but the right side will be parenthesised options: ["never"], errors: UNEXPECTED_OPERATOR_ASSIGNMENT - }] + }, + + // Optional chaining + { + code: "(obj?.a).b = (obj?.a).b + y", + output: null, + errors: EXPECTED_OPERATOR_ASSIGNMENT + }, + { + code: "obj.a = obj?.a + b", + output: null, + errors: EXPECTED_OPERATOR_ASSIGNMENT + } + + ] }); diff --git a/tests/lib/rules/padding-line-between-statements.js b/tests/lib/rules/padding-line-between-statements.js index e553610e353..931cdd24ccb 100644 --- a/tests/lib/rules/padding-line-between-statements.js +++ b/tests/lib/rules/padding-line-between-statements.js @@ -3632,6 +3632,22 @@ ruleTester.run("padding-line-between-statements", rule, { errors: [{ messageId: "expectedBlankLine" }] }, + // Optional chaining + { + code: "(function(){\n})?.()\nvar a = 2;", + output: "(function(){\n})?.()\n\nvar a = 2;", + options: [{ blankLine: "always", prev: "iife", next: "*" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedBlankLine" }] + }, + { + code: "void (function(){\n})?.()\nvar a = 2;", + output: "void (function(){\n})?.()\n\nvar a = 2;", + options: [{ blankLine: "always", prev: "iife", next: "*" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "expectedBlankLine" }] + }, + //---------------------------------------------------------------------- // import //---------------------------------------------------------------------- diff --git a/tests/lib/rules/prefer-arrow-callback.js b/tests/lib/rules/prefer-arrow-callback.js index cb380de251d..cb46c4a5021 100644 --- a/tests/lib/rules/prefer-arrow-callback.js +++ b/tests/lib/rules/prefer-arrow-callback.js @@ -21,7 +21,7 @@ const errors = [{ type: "FunctionExpression" }]; -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-arrow-callback", rule, { valid: [ @@ -40,7 +40,9 @@ ruleTester.run("prefer-arrow-callback", rule, { "foo(function bar() { arguments; }.bind(this));", "foo(function bar() { new.target; });", "foo(function bar() { new.target; }.bind(this));", - "foo(function bar() { this; }.bind(this, somethingElse));" + "foo(function bar() { this; }.bind(this, somethingElse));", + "foo((function() {}).bind.bar)", + "foo((function() { this.bar(); }).bind(obj).bind(this))" ], invalid: [ { @@ -165,13 +167,43 @@ ruleTester.run("prefer-arrow-callback", rule, { { code: "qux(async function (foo = 1, bar = 2, baz = 3) { return baz; })", output: "qux(async (foo = 1, bar = 2, baz = 3) => { return baz; })", - parserOptions: { ecmaVersion: 8 }, errors }, { code: "qux(async function (foo = 1, bar = 2, baz = 3) { return this; }.bind(this))", output: "qux(async (foo = 1, bar = 2, baz = 3) => { return this; })", - parserOptions: { ecmaVersion: 8 }, + errors + }, + { + code: "foo((bar || function() {}).bind(this))", + output: null, + errors + }, + { + code: "foo(function() {}.bind(this).bind(obj))", + output: "foo((() => {}).bind(obj))", + errors + }, + + // Optional chaining + { + code: "foo?.(function() {});", + output: "foo?.(() => {});", + errors + }, + { + code: "foo?.(function() { return this; }.bind(this));", + output: "foo?.(() => { return this; });", + errors + }, + { + code: "foo(function() { return this; }?.bind(this));", + output: "foo(() => { return this; });", + errors + }, + { + code: "foo((function() { return this; }?.bind)(this));", + output: null, errors } ] diff --git a/tests/lib/rules/prefer-destructuring.js b/tests/lib/rules/prefer-destructuring.js index 6f1b6e3a1be..c3d9c65706d 100644 --- a/tests/lib/rules/prefer-destructuring.js +++ b/tests/lib/rules/prefer-destructuring.js @@ -16,7 +16,7 @@ const rule = require("../../../lib/rules/prefer-destructuring"), // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-destructuring", rule, { valid: [ @@ -141,7 +141,11 @@ ruleTester.run("prefer-destructuring", rule, { { code: "var {bar} = object.foo;", options: [{ object: true }] - } + }, + + // Optional chaining + "var foo = array?.[0];", // because the fixed code can throw TypeError. + "var foo = object?.foo;" ], invalid: [ diff --git a/tests/lib/rules/prefer-exponentiation-operator.js b/tests/lib/rules/prefer-exponentiation-operator.js index 42bf8ec047d..da147b021fd 100644 --- a/tests/lib/rules/prefer-exponentiation-operator.js +++ b/tests/lib/rules/prefer-exponentiation-operator.js @@ -40,7 +40,7 @@ function invalid(code, output) { // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-exponentiation-operator", rule, { valid: [ @@ -346,6 +346,13 @@ ruleTester.run("prefer-exponentiation-operator", rule, { invalid("Math.pow(a, b/**/)", null), invalid("Math.pow(a, b//\n)", null), invalid("Math.pow(a, b)/* comment */;", "a**b/* comment */;"), - invalid("Math.pow(a, b)// comment\n;", "a**b// comment\n;") + invalid("Math.pow(a, b)// comment\n;", "a**b// comment\n;"), + + // Optional chaining + invalid("Math.pow?.(a, b)", "a**b"), + invalid("Math?.pow(a, b)", "a**b"), + invalid("Math?.pow?.(a, b)", "a**b"), + invalid("(Math?.pow)(a, b)", "a**b"), + invalid("(Math?.pow)?.(a, b)", "a**b") ] }); diff --git a/tests/lib/rules/prefer-numeric-literals.js b/tests/lib/rules/prefer-numeric-literals.js index d8ce4117140..d5d5751a6e3 100644 --- a/tests/lib/rules/prefer-numeric-literals.js +++ b/tests/lib/rules/prefer-numeric-literals.js @@ -16,7 +16,7 @@ const rule = require("../../../lib/rules/prefer-numeric-literals"), // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-numeric-literals", rule, { valid: [ @@ -318,6 +318,33 @@ ruleTester.run("prefer-numeric-literals", rule, { code: "parseInt('11', 2)//comment\n;", output: "0b11//comment\n;", errors: 1 + }, + + // Optional chaining + { + code: "parseInt?.(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of parseInt()." }] + }, + { + code: "Number?.parseInt(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of Number?.parseInt()." }] + }, + { + code: "Number?.parseInt?.(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of Number?.parseInt()." }] + }, + { + code: "(Number?.parseInt)(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of Number?.parseInt()." }] + }, + { + code: "(Number?.parseInt)?.(\"1F7\", 16) === 255;", + output: "0x1F7 === 255;", + errors: [{ message: "Use hexadecimal literals instead of Number?.parseInt()." }] } ] }); diff --git a/tests/lib/rules/prefer-promise-reject-errors.js b/tests/lib/rules/prefer-promise-reject-errors.js index 012e44ce7a5..de8f4c72524 100644 --- a/tests/lib/rules/prefer-promise-reject-errors.js +++ b/tests/lib/rules/prefer-promise-reject-errors.js @@ -16,7 +16,7 @@ const { RuleTester } = require("../../../lib/rule-tester"); // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 8 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-promise-reject-errors", rule, { @@ -42,7 +42,11 @@ ruleTester.run("prefer-promise-reject-errors", rule, { { code: "new Promise(function(resolve, reject) { reject() })", options: [{ allowEmptyReject: true }] - } + }, + + // Optional chaining + "Promise.reject(obj?.foo)", + "Promise.reject(obj?.foo())" ], invalid: [ @@ -87,7 +91,14 @@ ruleTester.run("prefer-promise-reject-errors", rule, { "new Promise((foo, arguments) => arguments(5))", "new Promise(function({}, reject) { reject(5) })", "new Promise(({}, reject) => reject(5))", - "new Promise((resolve, reject, somethingElse = reject(5)) => {})" + "new Promise((resolve, reject, somethingElse = reject(5)) => {})", + + // Optional chaining + "Promise.reject?.(5)", + "Promise?.reject(5)", + "Promise?.reject?.(5)", + "(Promise?.reject)(5)", + "(Promise?.reject)?.(5)" ].map(invalidCase => { const errors = { errors: [{ messageId: "rejectAnError", type: "CallExpression" }] }; diff --git a/tests/lib/rules/prefer-regex-literals.js b/tests/lib/rules/prefer-regex-literals.js index 9f6a2e6fbae..0ddaa8dae3d 100644 --- a/tests/lib/rules/prefer-regex-literals.js +++ b/tests/lib/rules/prefer-regex-literals.js @@ -16,7 +16,7 @@ const { RuleTester } = require("../../../lib/rule-tester"); // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-regex-literals", rule, { valid: [ @@ -222,6 +222,7 @@ ruleTester.run("prefer-regex-literals", rule, { env: { es2020: true }, errors: [{ messageId: "unexpectedRegExp", type: "CallExpression" }] }, + { code: "new RegExp(/a/);", options: [{ disallowRedundantWrapping: true }], @@ -241,6 +242,12 @@ ruleTester.run("prefer-regex-literals", rule, { code: "new RegExp('a');", options: [{ disallowRedundantWrapping: true }], errors: [{ messageId: "unexpectedRegExp", type: "NewExpression", line: 1, column: 1 }] + }, + + // Optional chaining + { + code: "new RegExp((String?.raw)`a`);", + errors: [{ messageId: "unexpectedRegExp" }] } ] }); diff --git a/tests/lib/rules/prefer-spread.js b/tests/lib/rules/prefer-spread.js index 580d7faeba9..7f48d845f1b 100644 --- a/tests/lib/rules/prefer-spread.js +++ b/tests/lib/rules/prefer-spread.js @@ -18,7 +18,7 @@ const { RuleTester } = require("../../../lib/rule-tester"); const errors = [{ messageId: "preferSpread", type: "CallExpression" }]; -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); ruleTester.run("prefer-spread", rule, { valid: [ @@ -39,7 +39,11 @@ ruleTester.run("prefer-spread", rule, { // ignores incomplete things. "foo.apply();", "obj.foo.apply();", - "obj.foo.apply(obj, ...args)" + "obj.foo.apply(obj, ...args)", + + // Optional chaining + "(a?.b).c.foo.apply(a?.b.c, args);", + "a?.b.c.foo.apply((a?.b).c, args);" ], invalid: [ { @@ -73,6 +77,44 @@ ruleTester.run("prefer-spread", rule, { { code: "[].concat.apply([\n/*empty*/\n], args);", errors + }, + + // Optional chaining + { + code: "foo.apply?.(undefined, args);", + errors + }, + { + code: "foo?.apply(undefined, args);", + errors + }, + { + code: "foo?.apply?.(undefined, args);", + errors + }, + { + code: "(foo?.apply)(undefined, args);", + errors + }, + { + code: "(foo?.apply)?.(undefined, args);", + errors + }, + { + code: "(obj?.foo).apply(obj, args);", + errors + }, + { + code: "a?.b.c.foo.apply(a?.b.c, args);", + errors + }, + { + code: "(a?.b.c).foo.apply(a?.b.c, args);", + errors + }, + { + code: "(a?.b).c.foo.apply((a?.b).c, args);", + errors } ] }); diff --git a/tests/lib/rules/radix.js b/tests/lib/rules/radix.js index 2766e80b597..52801c3d183 100644 --- a/tests/lib/rules/radix.js +++ b/tests/lib/rules/radix.js @@ -185,6 +185,28 @@ ruleTester.run("radix", rule, { messageId: "redundantRadix", type: "CallExpression" }] + }, + + // Optional chaining + { + code: "parseInt?.(\"10\");", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missingRadix" }] + }, + { + code: "Number.parseInt?.(\"10\");", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missingRadix" }] + }, + { + code: "Number?.parseInt(\"10\");", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missingRadix" }] + }, + { + code: "(Number?.parseInt)(\"10\");", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "missingRadix" }] } ] }); diff --git a/tests/lib/rules/use-isnan.js b/tests/lib/rules/use-isnan.js index aa1b1746fcd..c5a4b6bd686 100644 --- a/tests/lib/rules/use-isnan.js +++ b/tests/lib/rules/use-isnan.js @@ -385,6 +385,24 @@ ruleTester.run("use-isnan", rule, { code: "foo.bar.lastIndexOf(NaN)", options: [{ enforceForIndexOf: true }], errors: [{ messageId: "indexOfNaN", type: "CallExpression", data: { methodName: "lastIndexOf" } }] + }, + { + code: "foo.indexOf?.(NaN)", + options: [{ enforceForIndexOf: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + }, + { + code: "foo?.indexOf(NaN)", + options: [{ enforceForIndexOf: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] + }, + { + code: "(foo?.indexOf)(NaN)", + options: [{ enforceForIndexOf: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "indexOfNaN", data: { methodName: "indexOf" } }] } ] }); diff --git a/tests/lib/rules/utils/ast-utils.js b/tests/lib/rules/utils/ast-utils.js index 683e5ed2a6e..e3790f50f3f 100644 --- a/tests/lib/rules/utils/ast-utils.js +++ b/tests/lib/rules/utils/ast-utils.js @@ -10,6 +10,7 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, + util = require("util"), espree = require("espree"), astUtils = require("../../../../lib/rules/utils/ast-utils"), { Linter } = require("../../../../lib/linter"), @@ -478,6 +479,28 @@ describe("ast-utils", () => { assert.strictEqual(astUtils.getStaticStringValue(ast.body[0].expression), expectedResults[key]); }); }); + + it("should return text of regex literal even if it's not supported natively.", () => { + const node = { + type: "Literal", + value: null, + regex: { pattern: "(?:)", flags: "u" } + }; + const expectedText = "/(?:)/u"; + + assert.strictEqual(astUtils.getStaticStringValue(node), expectedText); + }); + + it("should return text of bigint literal even if it's not supported natively.", () => { + const node = { + type: "Literal", + value: null, + bigint: "100n" + }; + const expectedText = "100n"; + + assert.strictEqual(astUtils.getStaticStringValue(node), expectedText); + }); }); describe("getStaticPropertyName", () => { @@ -1411,6 +1434,134 @@ describe("ast-utils", () => { }); }); + describe("equalLiteralValue", () => { + describe("should return true if two regex values are same, even if it's not supported natively.", () => { + const patterns = [ + { + nodeA: { + type: "Literal", + value: /(?:)/u, + regex: { pattern: "(?:)", flags: "u" } + }, + nodeB: { + type: "Literal", + value: /(?:)/u, + regex: { pattern: "(?:)", flags: "u" } + }, + expected: true + }, + { + nodeA: { + type: "Literal", + value: null, + regex: { pattern: "(?:)", flags: "u" } + }, + nodeB: { + type: "Literal", + value: null, + regex: { pattern: "(?:)", flags: "u" } + }, + expected: true + }, + { + nodeA: { + type: "Literal", + value: null, + regex: { pattern: "(?:)", flags: "u" } + }, + nodeB: { + type: "Literal", + value: /(?:)/, // eslint-disable-line require-unicode-regexp + regex: { pattern: "(?:)", flags: "" } + }, + expected: false + }, + { + nodeA: { + type: "Literal", + value: null, + regex: { pattern: "(?:a)", flags: "u" } + }, + nodeB: { + type: "Literal", + value: null, + regex: { pattern: "(?:b)", flags: "u" } + }, + expected: false + } + ]; + + for (const { nodeA, nodeB, expected } of patterns) { + it(`should return ${expected} if it compared ${util.format("%o", nodeA)} and ${util.format("%o", nodeB)}`, () => { + assert.strictEqual(astUtils.equalLiteralValue(nodeA, nodeB), expected); + }); + } + }); + + describe("should return true if two bigint values are same, even if it's not supported natively.", () => { + const patterns = [ + { + nodeA: { + type: "Literal", + value: null, + bigint: "1" + }, + nodeB: { + type: "Literal", + value: null, + bigint: "1" + }, + expected: true + }, + { + nodeA: { + type: "Literal", + value: null, + bigint: "1" + }, + nodeB: { + type: "Literal", + value: null, + bigint: "2" + }, + expected: false + }, + { + nodeA: { + type: "Literal", + value: 1n, + bigint: "1" + }, + nodeB: { + type: "Literal", + value: 1n, + bigint: "1" + }, + expected: true + }, + { + nodeA: { + type: "Literal", + value: 1n, + bigint: "1" + }, + nodeB: { + type: "Literal", + value: 2n, + bigint: "2" + }, + expected: false + } + ]; + + for (const { nodeA, nodeB, expected } of patterns) { + it(`should return ${expected} if it compared ${util.format("%o", nodeA)} and ${util.format("%o", nodeB)}`, () => { + assert.strictEqual(astUtils.equalLiteralValue(nodeA, nodeB), expected); + }); + } + }); + }); + describe("hasOctalEscapeSequence", () => { /* eslint-disable quote-props */ diff --git a/tests/lib/rules/wrap-iife.js b/tests/lib/rules/wrap-iife.js index d18948f429a..035e265da40 100644 --- a/tests/lib/rules/wrap-iife.js +++ b/tests/lib/rules/wrap-iife.js @@ -603,6 +603,36 @@ ruleTester.run("wrap-iife", rule, { options: ["inside", { functionPrototypeMethods: true }], parserOptions: { ecmaVersion: 2020 }, errors: [wrapExpressionError] + }, + + // Optional chaining + { + code: "window.bar = function() { return 3; }.call?.(this, arg1);", + output: "window.bar = (function() { return 3; }).call?.(this, arg1);", + options: ["inside", { functionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [wrapInvocationError] + }, + { + code: "window.bar = function() { return 3; }?.call(this, arg1);", + output: "window.bar = (function() { return 3; })?.call(this, arg1);", + options: ["inside", { functionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [wrapInvocationError] + }, + { + code: "window.bar = (function() { return 3; }?.call)(this, arg1);", + output: "window.bar = ((function() { return 3; })?.call)(this, arg1);", + options: ["inside", { functionPrototypeMethods: true }], + parserOptions: { ecmaVersion: 2020 }, + errors: [wrapInvocationError] + }, + { + code: "new (function () {} ?.());", + output: "new ((function () {}) ?.());", + options: ["inside"], + parserOptions: { ecmaVersion: 2020 }, + errors: [wrapExpressionError] } ] }); diff --git a/tests/lib/rules/yoda.js b/tests/lib/rules/yoda.js index 43449401873..0fd2c3ee78f 100644 --- a/tests/lib/rules/yoda.js +++ b/tests/lib/rules/yoda.js @@ -288,6 +288,11 @@ ruleTester.run("yoda", rule, { code: "if('a' <= x && x < MAX) {}", options: ["never", { exceptRange: true }] }, + { + code: "if (0 <= obj?.a && obj?.a < 1) {}", + options: ["never", { exceptRange: true }], + parserOptions: { ecmaVersion: 2020 } + }, // onlyEquality { From 0a30a9948f813e89752d43ab8120f832877bf73c Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Sat, 18 Jul 2020 14:48:56 -0400 Subject: [PATCH 46/66] Build: changelog update for 7.5.0 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c83077629..81c467a26d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +v7.5.0 - July 18, 2020 + +* [`6ea3178`](https://github.com/eslint/eslint/commit/6ea3178776eae0e40c3f5498893e8aab0e23686b) Update: optional chaining support (fixes #12642) (#13416) (Toru Nagashima) +* [`540b1af`](https://github.com/eslint/eslint/commit/540b1af77278ae649b621aa8d4bf8d6de03c3155) Chore: enable consistent-meta-messages internal rule (#13487) (Milos Djermanovic) +* [`885a145`](https://github.com/eslint/eslint/commit/885a1455691265db88dc0befe9b48a69d69e8b9c) Docs: clarify behavior if `meta.fixable` is omitted (refs #13349) (#13493) (Milos Djermanovic) +* [`1a01b42`](https://github.com/eslint/eslint/commit/1a01b420eaab0de03dab5cc190a9f2a860c21a84) Docs: Update technology sponsors in README (#13478) (Nicholas C. Zakas) +* [`6ed9e8e`](https://github.com/eslint/eslint/commit/6ed9e8e4ff038c0259b0e7fe7ab7f4fd4ec55801) Upgrade: lodash@4.17.19 (#13499) (Yohan Siguret) +* [`45cdf00`](https://github.com/eslint/eslint/commit/45cdf00da6aeff3d584d37b0710fc8d6ad9456d6) Sponsors: Sync README with website (ESLint Jenkins) +* [`f1cc725`](https://github.com/eslint/eslint/commit/f1cc725ba1b8646dcf06a83716d96ad9bb726172) Docs: fix linebreaks between versions in changelog (#13488) (Milos Djermanovic) +* [`f4d7b9e`](https://github.com/eslint/eslint/commit/f4d7b9e1a599346b2f21ff9de003b311b51411e6) Update: deprecate id-blacklist rule (#13465) (Dimitri Mitropoulos) +* [`e14a645`](https://github.com/eslint/eslint/commit/e14a645aa495558081490f990ba221e21aa6b27c) Chore: use espree.latestEcmaVersion in fuzzer (#13484) (Milos Djermanovic) +* [`61097fe`](https://github.com/eslint/eslint/commit/61097fe5cc275d414a0c8e19b31c6060cb5568b7) Docs: Update int rule level to string (#13483) (Brandon Mills) +* [`c8f9c82`](https://github.com/eslint/eslint/commit/c8f9c8210cf4b9da8f07922093d7b219abad9f10) Update: Improve report location no-irregular-whitespace (refs #12334) (#13462) (Milos Djermanovic) +* [`f2e68ec`](https://github.com/eslint/eslint/commit/f2e68ec1d6cee6299e8a5cdf76c522c11d3008dd) Build: update webpack resolve.mainFields to match website config (#13457) (Milos Djermanovic) +* [`a96bc5e`](https://github.com/eslint/eslint/commit/a96bc5ec06f3a48bfe458bccd68d4d3b2a280ed9) Fix: arrow-body-style fixer for `in` wrap (fixes #11849) (#13228) (Anix) +* [`748734f`](https://github.com/eslint/eslint/commit/748734fdd497fbf61f3a616ff4a09169135b9396) Upgrade: Updated puppeteer version to v4.0.0 (#13444) (odidev) +* [`e951457`](https://github.com/eslint/eslint/commit/e951457b7aaa1b12b135588d36e3f4db4d7b8463) Docs: fix wording in configuring.md (#13469) (Piper) +* [`0af1d28`](https://github.com/eslint/eslint/commit/0af1d2828d27885483737867653ba1659af72005) Update: add allowSeparatedGroups option to sort-imports (fixes #12951) (#13455) (Milos Djermanovic) +* [`1050ee7`](https://github.com/eslint/eslint/commit/1050ee78a95da9484ff333dc1c74dac64c05da6f) Update: Improve report location for no-unneeded-ternary (refs #12334) (#13456) (Milos Djermanovic) +* [`b77b420`](https://github.com/eslint/eslint/commit/b77b4202bd1d5d1306f6f645e88d7a41a51715db) Update: Improve report location for max-len (refs #12334) (#13458) (Milos Djermanovic) +* [`095194c`](https://github.com/eslint/eslint/commit/095194c0fc0eb02aa69fde6b4280696e0e4de214) Fix: add end location to reports in object-curly-newline (refs #12334) (#13460) (Milos Djermanovic) +* [`10251bb`](https://github.com/eslint/eslint/commit/10251bbaeba80ac15244f385fc42cf2f2a30e5d2) Fix: add end location to reports in keyword-spacing (refs #12334) (#13461) (Milos Djermanovic) +* [`2ea7ee5`](https://github.com/eslint/eslint/commit/2ea7ee51a4e05ee76a6dae5954c3b6263b0970a3) Sponsors: Sync README with website (ESLint Jenkins) +* [`b55fd3b`](https://github.com/eslint/eslint/commit/b55fd3b8c05a29a465a794a524b06c1a28cddf0c) Sponsors: Sync README with website (ESLint Jenkins) + v7.4.0 - July 3, 2020 * [`f21bad2`](https://github.com/eslint/eslint/commit/f21bad2680406a2671b877f8dba47f4475d0cc64) Docs: fix description for `never` in multiline-ternary (fixes #13368) (#13452) (Milos Djermanovic) From f3a19d2a7ea9505000d14229a450dba133c10d5e Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Sat, 18 Jul 2020 14:48:56 -0400 Subject: [PATCH 47/66] 7.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 727a7a3a41c..2278adc8777 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "7.4.0", + "version": "7.5.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From 6c4aea44fd78e1eecea5fe3c37e1921e3b1e98a6 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Tue, 21 Jul 2020 19:07:56 +0200 Subject: [PATCH 48/66] Docs: add ECMAScript 2020 to README (#13510) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c2c34b5c2a2..ae15c7d69b2 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ Yes, ESLint natively supports parsing JSX syntax (this must be enabled in [confi ### What ECMAScript versions does ESLint support? -ESLint has full support for ECMAScript 3, 5 (default), 2015, 2016, 2017, 2018, and 2019. You can set your desired ECMAScript syntax (and other settings, like global variables or your target environments) through [configuration](https://eslint.org/docs/user-guide/configuring). +ESLint has full support for ECMAScript 3, 5 (default), 2015, 2016, 2017, 2018, 2019, and 2020. You can set your desired ECMAScript syntax (and other settings, like global variables or your target environments) through [configuration](https://eslint.org/docs/user-guide/configuring). ### What about experimental features? From e71e2980cd2e319afc70d8c859c7ffd59cf4157b Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Wed, 22 Jul 2020 09:29:56 +0900 Subject: [PATCH 49/66] Update: Change no-duplicate-case to comparing tokens (fixes #13485) (#13494) Change `no-duplicate-case` rule to change from detecting duplicates comparing text to detecting duplicates comparing tokens, like `no-dupe-else-if` rule. --- lib/rules/no-duplicate-case.js | 27 +++++++++++-- tests/lib/rules/no-duplicate-case.js | 60 ++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/lib/rules/no-duplicate-case.js b/lib/rules/no-duplicate-case.js index c8a0fa9da3c..e2d9665e7f5 100644 --- a/lib/rules/no-duplicate-case.js +++ b/lib/rules/no-duplicate-case.js @@ -6,6 +6,12 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -31,18 +37,31 @@ module.exports = { create(context) { const sourceCode = context.getSourceCode(); + /** + * Determines whether the two given nodes are considered to be equal. + * @param {ASTNode} a First node. + * @param {ASTNode} b Second node. + * @returns {boolean} `true` if the nodes are considered to be equal. + */ + function equal(a, b) { + if (a.type !== b.type) { + return false; + } + + return astUtils.equalTokens(a, b, sourceCode); + } return { SwitchStatement(node) { - const previousKeys = new Set(); + const previousTests = []; for (const switchCase of node.cases) { if (switchCase.test) { - const key = sourceCode.getText(switchCase.test); + const test = switchCase.test; - if (previousKeys.has(key)) { + if (previousTests.some(previousTest => equal(previousTest, test))) { context.report({ node: switchCase, messageId: "unexpected" }); } else { - previousKeys.add(key); + previousTests.push(test); } } } diff --git a/tests/lib/rules/no-duplicate-case.js b/tests/lib/rules/no-duplicate-case.js index a6e8e0ff34c..42dbda3f18d 100644 --- a/tests/lib/rules/no-duplicate-case.js +++ b/tests/lib/rules/no-duplicate-case.js @@ -128,6 +128,66 @@ ruleTester.run("no-duplicate-case", rule, { column: 74 } ] + }, + { + code: "var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p. p // comment\n .p1: break; default: break;}", + errors: [{ + messageId: "unexpected", + type: "SwitchCase", + column: 69 + }] + }, + { + code: "var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p .p\n/* comment */\n.p1: break; case p.p.p1: break; default: break;}", + errors: [{ + messageId: "unexpected", + type: "SwitchCase", + line: 3, + column: 13 + }] + }, + { + code: "var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p .p\n/* comment */\n.p1: break; case p. p // comment\n .p1: break; default: break;}", + errors: [{ + messageId: "unexpected", + type: "SwitchCase", + line: 3, + column: 13 + }] + }, + { + code: "var a = 1, p = {p: {p1: 1, p2: 1}}; switch (a) {case p.p.p1: break; case p. p // comment\n .p1: break; case p .p\n/* comment */\n.p1: break; default: break;}", + errors: [ + { + messageId: "unexpected", + type: "SwitchCase", + line: 1, + column: 69 + }, + { + messageId: "unexpected", + type: "SwitchCase", + line: 2, + column: 14 + } + ] + }, + { + code: "var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(a + 1).p1: break; case f(a+1).p1: break; default: break;}", + errors: [{ + messageId: "unexpected", + type: "SwitchCase", + column: 87 + }] + }, + { + code: "var a = 1, f = function(s) { return { p1: s } }; switch (a) {case f(\na + 1 // comment\n).p1: break; case f(a+1)\n.p1: break; default: break;}", + errors: [{ + messageId: "unexpected", + type: "SwitchCase", + line: 3, + column: 14 + }] } ] }); From f937eb95407f60d3772bcb956e227aaf99e48777 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Wed, 29 Jul 2020 08:11:38 -0400 Subject: [PATCH 50/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae15c7d69b2..54d9cadb0a2 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Shopify Salesforce Airbnb Microsoft FOSS Fund Sponsorships

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

+

Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

## Technology Sponsors From 16b10fe8ba3c78939d5ada4a25caf2f0c9e6a058 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 29 Jul 2020 17:26:31 -0700 Subject: [PATCH 51/66] Fix: Update the chatroom link to go directly to help channel (#13536) --- messages/extend-config-missing.txt | 2 +- messages/no-config-found.txt | 2 +- messages/plugin-conflict.txt | 2 +- messages/plugin-missing.txt | 2 +- messages/whitespace-found.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/messages/extend-config-missing.txt b/messages/extend-config-missing.txt index f7c5f71ebe3..4defd7ac4d1 100644 --- a/messages/extend-config-missing.txt +++ b/messages/extend-config-missing.txt @@ -2,4 +2,4 @@ ESLint couldn't find the config "<%- configName %>" to extend from. Please check The config "<%- configName %>" was referenced from the config file in "<%- importerName %>". -If you still have problems, please stop by https://eslint.org/chat to chat with the team. +If you still have problems, please stop by https://eslint.org/chat/help to chat with the team. diff --git a/messages/no-config-found.txt b/messages/no-config-found.txt index f1f7beb63b1..b46a7e5a7a6 100644 --- a/messages/no-config-found.txt +++ b/messages/no-config-found.txt @@ -4,4 +4,4 @@ ESLint couldn't find a configuration file. To set up a configuration file for th 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 +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 diff --git a/messages/plugin-conflict.txt b/messages/plugin-conflict.txt index f8b60631c58..3ab4b340ef2 100644 --- a/messages/plugin-conflict.txt +++ b/messages/plugin-conflict.txt @@ -4,4 +4,4 @@ ESLint couldn't determine the plugin "<%- pluginId %>" uniquely. 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 to chat with the team. +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.txt b/messages/plugin-missing.txt index 3d376733085..aa25f59ac44 100644 --- a/messages/plugin-missing.txt +++ b/messages/plugin-missing.txt @@ -8,4 +8,4 @@ It's likely that the plugin isn't installed correctly. Try reinstalling by runni 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 to chat with the team. +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/whitespace-found.txt b/messages/whitespace-found.txt index 7d72149a8fd..3eed1af5866 100644 --- a/messages/whitespace-found.txt +++ b/messages/whitespace-found.txt @@ -1,3 +1,3 @@ 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 to chat with the team. +If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. From f100143fa5f529aacb2b50e650a00d2697ca4c54 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 31 Jul 2020 08:11:30 -0400 Subject: [PATCH 52/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54d9cadb0a2..4f20db7129c 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Shopify Salesforce Airbnb Microsoft FOSS Fund Sponsorships

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

+

Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

## Technology Sponsors From 493b5b40cae7a076fdeb19740f8c88fb4ae9c1fb Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 31 Jul 2020 16:11:30 -0400 Subject: [PATCH 53/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f20db7129c..7e2b4c8cadc 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Shopify Salesforce Airbnb Microsoft FOSS Fund Sponsorships

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle TekHattan Marfeel Fire Stick Tricks

+

Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Marfeel Fire Stick Tricks

## Technology Sponsors From 318fe103dbf2548eee293ff456ef0b829dbe3db3 Mon Sep 17 00:00:00 2001 From: haya14busa Date: Sat, 1 Aug 2020 07:37:52 +0900 Subject: [PATCH 54/66] Fix: Do not output `undefined` as line and column when it's unavailable (#13519) --- lib/cli-engine/formatters/checkstyle.js | 4 ++-- tests/lib/cli-engine/formatters/checkstyle.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/cli-engine/formatters/checkstyle.js b/lib/cli-engine/formatters/checkstyle.js index ba4d1b5b3ec..f19b6fc0957 100644 --- a/lib/cli-engine/formatters/checkstyle.js +++ b/lib/cli-engine/formatters/checkstyle.js @@ -42,8 +42,8 @@ module.exports = function(results) { messages.forEach(message => { output += [ - `` diff --git a/tests/lib/cli-engine/formatters/checkstyle.js b/tests/lib/cli-engine/formatters/checkstyle.js index 2a72da15ece..218b15310ab 100644 --- a/tests/lib/cli-engine/formatters/checkstyle.js +++ b/tests/lib/cli-engine/formatters/checkstyle.js @@ -151,4 +151,21 @@ describe("formatter:checkstyle", () => { assert.strictEqual(result, ""); }); }); + + describe("when passing single message without line and column", () => { + const code = [{ + filePath: "foo.js", + messages: [{ + message: "Unexpected foo.", + severity: 2, + ruleId: "foo" + }] + }]; + + it("should return line and column as 0 instead of undefined", () => { + const result = formatter(code); + + assert.strictEqual(result, ""); + }); + }); }); From 6fb4edde3b7a7ae2faf8ac956a7342fbf80865fc Mon Sep 17 00:00:00 2001 From: Sam Chen Date: Sat, 1 Aug 2020 06:43:55 +0800 Subject: [PATCH 55/66] Docs: fix broken links in developer guide (#13518) --- docs/developer-guide/contributing/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/developer-guide/contributing/README.md b/docs/developer-guide/contributing/README.md index a22ea5d90a2..e341083febc 100644 --- a/docs/developer-guide/contributing/README.md +++ b/docs/developer-guide/contributing/README.md @@ -4,11 +4,11 @@ One of the great things about open source projects is that anyone can contribute This guide is intended for anyone who wants to contribute to an ESLint project. Please read it carefully as it answers a lot of the questions many newcomers have when first working with our projects. -## Read the [Code of Conduct](https://js.foundation/community/code-of-conduct) +## Read the [Code of Conduct](https://eslint.org/conduct) -ESLint welcomes contributions from everyone and adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct). We kindly request that you read over our code of conduct before contributing. +ESLint welcomes contributions from everyone and adheres to the [OpenJS Foundation Code of Conduct](https://eslint.org/conduct). We kindly request that you read over our code of conduct before contributing. -## [Signing the CLA](https://js.foundation/CLA) +## [Signing the CLA](https://openjsf.org/about/the-openjs-foundation-cla/) In order to submit code or documentation to an ESLint project, you will need to electronically sign our [Contributor License Agreement](https://cla.js.foundation/eslint/eslint). The CLA is you giving us permission to use your contribution. From ecb2b7343a0d14fb57d297a16be6c1b176fb3dbf Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 1 Aug 2020 00:47:12 +0200 Subject: [PATCH 56/66] Update: require `meta` for fixable rules in RuleTester (refs #13349) (#13489) * Update: require `meta` for fixable rules in RuleTester (refs #13349) * Fix JSDoc * Throw for legacy-format fixable rules --- lib/rule-tester/rule-tester.js | 10 +++ tests/fixtures/testers/rule-tester/no-var.js | 48 +++++++------ tests/lib/rule-tester/rule-tester.js | 71 ++++++++++++++++++++ 3 files changed, 107 insertions(+), 22 deletions(-) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 3e391576716..905f3418121 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -861,6 +861,16 @@ class RuleTester { ); } + // Rules that produce fixes must have `meta.fixable` property. + if (result.output !== item.code) { + assert.ok( + hasOwnProperty(rule, "meta"), + "Fixable rules should export a `meta.fixable` property." + ); + + // Linter throws if a rule that produced a fix has `meta` but doesn't have `meta.fixable`. + } + assertASTDidntChange(result.beforeAST, result.afterAST); } diff --git a/tests/fixtures/testers/rule-tester/no-var.js b/tests/fixtures/testers/rule-tester/no-var.js index 382885613d2..5841f15bfa1 100644 --- a/tests/fixtures/testers/rule-tester/no-var.js +++ b/tests/fixtures/testers/rule-tester/no-var.js @@ -7,27 +7,31 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { - - "use strict"; - - - var sourceCode = context.getSourceCode(); - - return { - - "VariableDeclaration": function(node) { - if (node.kind === "var") { - context.report({ - node: node, - loc: sourceCode.getFirstToken(node).loc, - message: "Bad var.", - fix: function(fixer) { - return fixer.remove(sourceCode.getFirstToken(node)); - } - }) +"use strict"; + +module.exports = { + + meta: { + fixable: "code" + }, + + create(context) { + + var sourceCode = context.getSourceCode(); + + return { + "VariableDeclaration": function(node) { + if (node.kind === "var") { + context.report({ + node: node, + loc: sourceCode.getFirstToken(node).loc, + message: "Bad var.", + fix: function(fixer) { + return fixer.remove(sourceCode.getFirstToken(node)); + } + }) + } } - } - }; - + }; + } }; diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 7e26e1a826c..3f2393621b6 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -304,6 +304,10 @@ describe("RuleTester", () => { it("should use strict equality to compare output", () => { const replaceProgramWith5Rule = { + meta: { + fixable: "code" + }, + create: context => ({ Program(node) { context.report({ node, message: "bad", fix: fixer => fixer.replaceText(node, "5") }); @@ -1237,6 +1241,73 @@ describe("RuleTester", () => { }, "Error must specify 'messageId' if 'data' is used."); }); + // fixable rules with or without `meta` property + it("should not throw an error if a rule that has `meta.fixable` produces fixes", () => { + const replaceProgramWith5Rule = { + meta: { + fixable: "code" + }, + create(context) { + return { + Program(node) { + context.report({ node, message: "bad", fix: fixer => fixer.replaceText(node, "5") }); + } + }; + } + }; + + ruleTester.run("replaceProgramWith5", replaceProgramWith5Rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", output: "5", errors: 1 } + ] + }); + }); + it("should throw an error if a new-format rule that doesn't have `meta` produces fixes", () => { + const replaceProgramWith5Rule = { + create(context) { + return { + Program(node) { + context.report({ node, message: "bad", fix: fixer => fixer.replaceText(node, "5") }); + } + }; + } + }; + + assert.throws(() => { + ruleTester.run("replaceProgramWith5", replaceProgramWith5Rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", output: "5", errors: 1 } + ] + }); + }, "Fixable rules should export a `meta.fixable` property."); + }); + it("should throw an error if a legacy-format rule produces fixes", () => { + + /** + * Legacy-format rule (a function instead of an object with `create` method). + * @param {RuleContext} context The ESLint rule context object. + * @returns {Object} Listeners. + */ + function replaceProgramWith5Rule(context) { + return { + Program(node) { + context.report({ node, message: "bad", fix: fixer => fixer.replaceText(node, "5") }); + } + }; + } + + assert.throws(() => { + ruleTester.run("replaceProgramWith5", replaceProgramWith5Rule, { + valid: [], + invalid: [ + { code: "var foo = bar;", output: "5", errors: 1 } + ] + }); + }, "Fixable rules should export a `meta.fixable` property."); + }); + describe("suggestions", () => { it("should pass with valid suggestions (tested using desc)", () => { ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { From bc58a3447b8aafa90c2f3f84fd4704348e06dcfe Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 31 Jul 2020 19:17:54 -0400 Subject: [PATCH 57/66] Build: changelog update for 7.6.0 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c467a26d4..be836630d92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +v7.6.0 - July 31, 2020 + +* [`ecb2b73`](https://github.com/eslint/eslint/commit/ecb2b7343a0d14fb57d297a16be6c1b176fb3dbf) Update: require `meta` for fixable rules in RuleTester (refs #13349) (#13489) (Milos Djermanovic) +* [`6fb4edd`](https://github.com/eslint/eslint/commit/6fb4edde3b7a7ae2faf8ac956a7342fbf80865fc) Docs: fix broken links in developer guide (#13518) (Sam Chen) +* [`318fe10`](https://github.com/eslint/eslint/commit/318fe103dbf2548eee293ff456ef0b829dbe3db3) Fix: Do not output `undefined` as line and column when it's unavailable (#13519) (haya14busa) +* [`493b5b4`](https://github.com/eslint/eslint/commit/493b5b40cae7a076fdeb19740f8c88fb4ae9c1fb) Sponsors: Sync README with website (ESLint Jenkins) +* [`f100143`](https://github.com/eslint/eslint/commit/f100143fa5f529aacb2b50e650a00d2697ca4c54) Sponsors: Sync README with website (ESLint Jenkins) +* [`16b10fe`](https://github.com/eslint/eslint/commit/16b10fe8ba3c78939d5ada4a25caf2f0c9e6a058) Fix: Update the chatroom link to go directly to help channel (#13536) (Nicholas C. Zakas) +* [`f937eb9`](https://github.com/eslint/eslint/commit/f937eb95407f60d3772bcb956e227aaf99e48777) Sponsors: Sync README with website (ESLint Jenkins) +* [`e71e298`](https://github.com/eslint/eslint/commit/e71e2980cd2e319afc70d8c859c7ffd59cf4157b) Update: Change no-duplicate-case to comparing tokens (fixes #13485) (#13494) (Yosuke Ota) +* [`6c4aea4`](https://github.com/eslint/eslint/commit/6c4aea44fd78e1eecea5fe3c37e1921e3b1e98a6) Docs: add ECMAScript 2020 to README (#13510) (Milos Djermanovic) + v7.5.0 - July 18, 2020 * [`6ea3178`](https://github.com/eslint/eslint/commit/6ea3178776eae0e40c3f5498893e8aab0e23686b) Update: optional chaining support (fixes #12642) (#13416) (Toru Nagashima) From e8f5289de5f40c4e68192cafa633d1a4595267c6 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Fri, 31 Jul 2020 19:17:54 -0400 Subject: [PATCH 58/66] 7.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2278adc8777..96f709e0bb8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "7.5.0", + "version": "7.6.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From 48d8ec8cf320c69aed17c6b6c78f19e7c1e587ca Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Sun, 2 Aug 2020 01:11:31 -0400 Subject: [PATCH 59/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e2b4c8cadc..548056ad21a 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

-

Shopify Salesforce Airbnb Microsoft FOSS Fund Sponsorships

Silver Sponsors

+

Salesforce Airbnb Microsoft FOSS Fund Sponsorships

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Marfeel Fire Stick Tricks

From 5c4c7f515c2e8e83f2186a66ddce75d6477abeb0 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Sun, 2 Aug 2020 02:11:29 -0400 Subject: [PATCH 60/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 548056ad21a..045d73c1113 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Salesforce Airbnb Microsoft FOSS Fund Sponsorships

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Marfeel Fire Stick Tricks

+

Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Marfeel

## Technology Sponsors From 9124a1599638a1caf4b7e252d1cb66abdc5e51c6 Mon Sep 17 00:00:00 2001 From: Mark de Dios Date: Sun, 2 Aug 2020 05:30:16 -0700 Subject: [PATCH 61/66] Chore: remove leche (fixes #13287) (#13533) * remove leche * fix cli engine tests * remove fs from config file tests, use simple mock with write file sync for tests * fix stubs for fs write file in eslint spec * remove leche from package --- package.json | 1 - tests/lib/cli-engine/cli-engine.js | 15 +++++---- tests/lib/eslint/eslint.js | 13 +++++--- tests/lib/init/config-file.js | 22 ++++++++----- tests/lib/shared/config-ops.js | 5 ++- tests/lib/shared/naming.js | 9 +++-- tests/lib/source-code/source-code.js | 49 ++++++++++++++-------------- 7 files changed, 60 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 96f709e0bb8..4433760134f 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,6 @@ "karma-mocha": "^1.3.0", "karma-mocha-reporter": "^2.2.3", "karma-webpack": "^4.0.0-rc.6", - "leche": "^2.2.3", "lint-staged": "^10.1.2", "load-perf": "^0.2.0", "markdownlint": "^0.19.0", diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index fb3c365bbbf..0ba01d2a5df 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -12,7 +12,6 @@ const assert = require("chai").assert, path = require("path"), sinon = require("sinon"), - leche = require("leche"), shell = require("shelljs"), fs = require("fs"), os = require("os"), @@ -4469,7 +4468,9 @@ describe("CLIEngine", () => { }); it("should call fs.writeFileSync() for each result with output", () => { - const fakeFS = leche.fake(fs), + const fakeFS = { + writeFileSync: () => {} + }, localCLIEngine = proxyquire("../../../lib/cli-engine/cli-engine", { fs: fakeFS }).CLIEngine, @@ -4486,7 +4487,6 @@ describe("CLIEngine", () => { ] }; - fakeFS.writeFileSync = function() {}; const spy = sinon.spy(fakeFS, "writeFileSync"); localCLIEngine.outputFixes(report); @@ -4498,7 +4498,9 @@ describe("CLIEngine", () => { }); it("should call fs.writeFileSync() for each result with output and not at all for a result without output", () => { - const fakeFS = leche.fake(fs), + const fakeFS = { + writeFileSync: () => {} + }, localCLIEngine = proxyquire("../../../lib/cli-engine/cli-engine", { fs: fakeFS }).CLIEngine, @@ -4518,7 +4520,6 @@ describe("CLIEngine", () => { ] }; - fakeFS.writeFileSync = function() {}; const spy = sinon.spy(fakeFS, "writeFileSync"); localCLIEngine.outputFixes(report); @@ -4555,12 +4556,12 @@ describe("CLIEngine", () => { describe("resolveFileGlobPatterns", () => { - leche.withData([ + [ [".", ["**/*.{js}"]], ["./", ["**/*.{js}"]], ["../", ["../**/*.{js}"]], ["", []] - ], (input, expected) => { + ].forEach(([input, expected]) => { it(`should correctly resolve ${input} to ${expected}`, () => { const engine = new CLIEngine(); diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 6807a57aa8a..d29dbb01640 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -16,7 +16,6 @@ const os = require("os"); const path = require("path"); const escapeStringRegExp = require("escape-string-regexp"); const fCache = require("file-entry-cache"); -const leche = require("leche"); const sinon = require("sinon"); const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); const shell = require("shelljs"); @@ -4427,8 +4426,10 @@ describe("ESLint", () => { }); it("should call fs.writeFile() for each result with output", async () => { - const fakeFS = leche.fake(fs); - const spy = fakeFS.writeFile = sinon.spy(callLastArgument); + const fakeFS = { + writeFile: sinon.spy(callLastArgument) + }; + const spy = fakeFS.writeFile; const localESLint = proxyquire("../../../lib/eslint/eslint", { fs: fakeFS }).ESLint; @@ -4451,8 +4452,10 @@ describe("ESLint", () => { }); it("should call fs.writeFile() for each result with output and not at all for a result without output", async () => { - const fakeFS = leche.fake(fs); - const spy = fakeFS.writeFile = sinon.spy(callLastArgument); + const fakeFS = { + writeFile: sinon.spy(callLastArgument) + }; + const spy = fakeFS.writeFile; const localESLint = proxyquire("../../../lib/eslint/eslint", { fs: fakeFS }).ESLint; diff --git a/tests/lib/init/config-file.js b/tests/lib/init/config-file.js index 69556b4b5b6..e9fe62e2c30 100644 --- a/tests/lib/init/config-file.js +++ b/tests/lib/init/config-file.js @@ -9,10 +9,8 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, - leche = require("leche"), sinon = require("sinon"), path = require("path"), - fs = require("fs"), yaml = require("js-yaml"), espree = require("espree"), ConfigFile = require("../../../lib/init/config-file"), @@ -59,15 +57,17 @@ describe("ConfigFile", () => { sinon.verifyAndRestore(); }); - leche.withData([ + [ ["JavaScript", "foo.js", espree.parse], ["JSON", "bar.json", JSON.parse], ["YAML", "foo.yaml", yaml.safeLoad], ["YML", "foo.yml", yaml.safeLoad] - ], (fileType, filename, validate) => { + ].forEach(([fileType, filename, validate]) => { it(`should write a file through fs when a ${fileType} path is passed`, () => { - const fakeFS = leche.fake(fs); + const fakeFS = { + writeFileSync: () => {} + }; sinon.mock(fakeFS).expects("writeFileSync").withExactArgs( filename, @@ -83,7 +83,9 @@ describe("ConfigFile", () => { }); it("should include a newline character at EOF", () => { - const fakeFS = leche.fake(fs); + const fakeFS = { + writeFileSync: () => {} + }; sinon.mock(fakeFS).expects("writeFileSync").withExactArgs( filename, @@ -100,7 +102,9 @@ describe("ConfigFile", () => { }); it("should make sure js config files match linting rules", () => { - const fakeFS = leche.fake(fs); + const fakeFS = { + writeFileSync: () => {} + }; const singleQuoteConfig = { rules: { @@ -122,7 +126,9 @@ describe("ConfigFile", () => { }); it("should still write a js config file even if linting fails", () => { - const fakeFS = leche.fake(fs); + const fakeFS = { + writeFileSync: () => {} + }; const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ baseConfig: config, fix: true, diff --git a/tests/lib/shared/config-ops.js b/tests/lib/shared/config-ops.js index 73f34155405..519d5a2fa0d 100644 --- a/tests/lib/shared/config-ops.js +++ b/tests/lib/shared/config-ops.js @@ -9,7 +9,6 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, - leche = require("leche"), util = require("util"), ConfigOps = require("../../../lib/shared/config-ops"); @@ -199,7 +198,7 @@ describe("ConfigOps", () => { describe("isError()", () => { - leche.withData([ + [ ["error", true], ["Error", true], [2, true], @@ -209,7 +208,7 @@ describe("ConfigOps", () => { [["error", "foo"], true], [["eRror", "bar"], true], [[2, "baz"], true] - ], (input, expected) => { + ].forEach(([input, expected]) => { it(`should return ${expected}when passed ${input}`, () => { const result = ConfigOps.isErrorSeverity(input); diff --git a/tests/lib/shared/naming.js b/tests/lib/shared/naming.js index 0a868a872ed..84bec232431 100644 --- a/tests/lib/shared/naming.js +++ b/tests/lib/shared/naming.js @@ -8,7 +8,6 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, - leche = require("leche"), naming = require("../../../lib/shared/naming"); //------------------------------------------------------------------------------ @@ -18,7 +17,7 @@ const assert = require("chai").assert, describe("naming", () => { describe("normalizePackageName()", () => { - leche.withData([ + [ ["foo", "eslint-config-foo"], ["eslint-config-foo", "eslint-config-foo"], ["@z/foo", "@z/eslint-config-foo"], @@ -26,7 +25,7 @@ describe("naming", () => { ["@z\\foo\\bar.js", "@z/eslint-config-foo/bar.js"], ["@z/eslint-config", "@z/eslint-config"], ["@z/eslint-config-foo", "@z/eslint-config-foo"] - ], (input, expected) => { + ].forEach(([input, expected]) => { it(`should return ${expected} when passed ${input}`, () => { const result = naming.normalizePackageName(input, "eslint-config"); @@ -38,14 +37,14 @@ describe("naming", () => { describe("getShorthandName()", () => { - leche.withData([ + [ ["foo", "foo"], ["eslint-config-foo", "foo"], ["@z", "@z"], ["@z/eslint-config", "@z"], ["@z/foo", "@z/foo"], ["@z/eslint-config-foo", "@z/foo"] - ], (input, expected) => { + ].forEach(([input, expected]) => { it(`should return ${expected} when passed ${input}`, () => { const result = naming.getShorthandName(input, "eslint-config"); diff --git a/tests/lib/source-code/source-code.js b/tests/lib/source-code/source-code.js index e89f9a3283d..c44d4a2e434 100644 --- a/tests/lib/source-code/source-code.js +++ b/tests/lib/source-code/source-code.js @@ -13,7 +13,6 @@ const fs = require("fs"), assert = require("chai").assert, espree = require("espree"), sinon = require("sinon"), - leche = require("leche"), { Linter } = require("../../../lib/linter"), SourceCode = require("../../../lib/source-code/source-code"), astUtils = require("../../../lib/shared/ast-utils"); @@ -1791,13 +1790,13 @@ describe("SourceCode", () => { describe("isSpaceBetween()", () => { describe("should return true when there is at least one whitespace character between two tokens", () => { - leche.withData([ + [ ["let foo", true], ["let foo", true], ["let /**/ foo", true], ["let/**/foo", false], ["let/*\n*/foo", false] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -1829,7 +1828,7 @@ describe("SourceCode", () => { }); }); - leche.withData([ + [ ["a+b", false], ["a +b", true], ["a/**/+b", false], @@ -1861,7 +1860,7 @@ describe("SourceCode", () => { ["a/* */+ ` /*\n*/ `/* */+c", true], ["a/* */+` /*\n*/ ` /* */+c", true], ["a/* */+ ` /*\n*/ ` /* */+c", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -1895,7 +1894,7 @@ describe("SourceCode", () => { }); describe("should return true when there is at least one whitespace character between a token and a node", () => { - leche.withData([ + [ [";let foo = bar", false], [";/**/let foo = bar", false], [";/* */let foo = bar", false], @@ -1923,7 +1922,7 @@ describe("SourceCode", () => { [";/* */\nlet foo = bar", true], [";\n/**/\nlet foo = bar", true], [";\n/* */\nlet foo = bar", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -1957,7 +1956,7 @@ describe("SourceCode", () => { }); describe("should return true when there is at least one whitespace character between a node and a token", () => { - leche.withData([ + [ ["let foo = bar;;", false], ["let foo = bar;;;", false], ["let foo = 1; let bar = 2;;", true], @@ -1985,7 +1984,7 @@ describe("SourceCode", () => { ["let foo = bar;/* */\n;", true], ["let foo = bar;\n/**/\n;", true], ["let foo = bar;\n/* */\n;", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -2019,7 +2018,7 @@ describe("SourceCode", () => { }); describe("should return true when there is at least one whitespace character between two nodes", () => { - leche.withData([ + [ ["let foo = bar;let baz = qux;", false], ["let foo = bar;/**/let baz = qux;", false], ["let foo = bar;/* */let baz = qux;", false], @@ -2045,7 +2044,7 @@ describe("SourceCode", () => { ["let foo = bar;\n/**/\nlet baz = qux;", true], ["let foo = bar;\n/* */\nlet baz = qux;", true], ["let foo = 1;let foo2 = 2; let foo3 = 3;", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -2142,9 +2141,9 @@ describe("SourceCode", () => { }); describe("should return false either of the arguments' location is inside the other one", () => { - leche.withData([ + [ ["let foo = bar;", false] - ], (code, expected) => { + ].forEach(([code, expected]) => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), sourceCode = new SourceCode(code, ast); @@ -2187,13 +2186,13 @@ describe("SourceCode", () => { describe("isSpaceBetweenTokens()", () => { describe("should return true when there is at least one whitespace character between two tokens", () => { - leche.withData([ + [ ["let foo", true], ["let foo", true], ["let /**/ foo", true], ["let/**/foo", false], ["let/*\n*/foo", false] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -2225,7 +2224,7 @@ describe("SourceCode", () => { }); }); - leche.withData([ + [ ["a+b", false], ["a +b", true], ["a/**/+b", false], @@ -2257,7 +2256,7 @@ describe("SourceCode", () => { ["a/* */+ ` /*\n*/ `/* */+c", true], ["a/* */+` /*\n*/ ` /* */+c", true], ["a/* */+ ` /*\n*/ ` /* */+c", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -2291,7 +2290,7 @@ describe("SourceCode", () => { }); describe("should return true when there is at least one whitespace character between a token and a node", () => { - leche.withData([ + [ [";let foo = bar", false], [";/**/let foo = bar", false], [";/* */let foo = bar", false], @@ -2319,7 +2318,7 @@ describe("SourceCode", () => { [";/* */\nlet foo = bar", true], [";\n/**/\nlet foo = bar", true], [";\n/* */\nlet foo = bar", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -2353,7 +2352,7 @@ describe("SourceCode", () => { }); describe("should return true when there is at least one whitespace character between a node and a token", () => { - leche.withData([ + [ ["let foo = bar;;", false], ["let foo = bar;;;", false], ["let foo = 1; let bar = 2;;", true], @@ -2381,7 +2380,7 @@ describe("SourceCode", () => { ["let foo = bar;/* */\n;", true], ["let foo = bar;\n/**/\n;", true], ["let foo = bar;\n/* */\n;", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -2415,7 +2414,7 @@ describe("SourceCode", () => { }); describe("should return true when there is at least one whitespace character between two nodes", () => { - leche.withData([ + [ ["let foo = bar;let baz = qux;", false], ["let foo = bar;/**/let baz = qux;", false], ["let foo = bar;/* */let baz = qux;", false], @@ -2441,7 +2440,7 @@ describe("SourceCode", () => { ["let foo = bar;\n/**/\nlet baz = qux;", true], ["let foo = bar;\n/* */\nlet baz = qux;", true], ["let foo = 1;let foo2 = 2; let foo3 = 3;", true] - ], (code, expected) => { + ].forEach(([code, expected]) => { describe("when the first given is located before the second", () => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), @@ -2538,9 +2537,9 @@ describe("SourceCode", () => { }); describe("should return false either of the arguments' location is inside the other one", () => { - leche.withData([ + [ ["let foo = bar;", false] - ], (code, expected) => { + ].forEach(([code, expected]) => { it(code, () => { const ast = espree.parse(code, DEFAULT_CONFIG), sourceCode = new SourceCode(code, ast); From ae9b54e59b01aa9f50ee31f5b6787d86e6b59de6 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Mon, 3 Aug 2020 02:11:29 -0400 Subject: [PATCH 62/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 045d73c1113..88e1051dc15 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Salesforce Airbnb Microsoft FOSS Fund Sponsorships

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Marfeel

+

Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Marfeel

## Technology Sponsors From 284e954f93126c50e0aa9b88f42afb03a47ad967 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Tue, 4 Aug 2020 01:11:32 -0400 Subject: [PATCH 63/66] Sponsors: Sync README with website --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 88e1051dc15..fed7bc7be29 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,11 @@ Toru Nagashima
Kai Cataldo + + +
+Milos Djermanovic +
@@ -218,11 +223,6 @@ The people who review and implement new features.
薛定谔的猫 - - -
-Milos Djermanovic -
@@ -238,6 +238,11 @@ The people who review and fix bugs and help triage issues. Pig Fang + +
+Anix +
+
YeonJuan From d4ce4d3b8492c3e4654ed1f51f2c48e6c0ad272f Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Tue, 4 Aug 2020 13:11:35 -0400 Subject: [PATCH 64/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fed7bc7be29..99f35cfc98e 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Salesforce Airbnb Microsoft FOSS Fund Sponsorships

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Marfeel

+

Nettikasinolista.com Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Marfeel

## Technology Sponsors From 07db7b8080c2f68ee28e7d447db356c33e6fddce Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Wed, 5 Aug 2020 10:11:40 -0400 Subject: [PATCH 65/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 99f35cfc98e..d562ffcf2f0 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Salesforce Airbnb Microsoft FOSS Fund Sponsorships

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

Nettikasinolista.com Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Marfeel

+

vpn for netflix Nettikasinolista.com Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Marfeel

## Technology Sponsors From 6677180495e16a02d150d0552e7e5d5f6b77fcc5 Mon Sep 17 00:00:00 2001 From: ESLint Jenkins Date: Thu, 6 Aug 2020 01:11:34 -0400 Subject: [PATCH 66/66] Sponsors: Sync README with website --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d562ffcf2f0..2bdd2c661eb 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Gold Sponsors

Salesforce Airbnb Microsoft FOSS Fund Sponsorships

Silver Sponsors

Liftoff AMP Project

Bronze Sponsors

-

vpn for netflix Nettikasinolista.com Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Marfeel

+

vpn for netflix Nettikasinolista.com Veikkaajat.com Nettikasinot.media My True Media Norgekasino Japanesecasino Bruce EduBirdie CasinoTop.com Casino Topp Writers Per Hour Anagram Solver Kasinot.fi Pelisivut Nettikasinot.org BonusFinder Deutschland Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Marfeel

## Technology Sponsors