From 3fa39a633b37544fec7cedfc1f2b0e62e9312a72 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 25 Dec 2019 06:04:30 +0100 Subject: [PATCH] Update: Handle locally unsupported regex in computed property keys (#12056) --- lib/rules/utils/ast-utils.js | 59 +++++++++++---- tests/lib/rules/no-dupe-keys.js | 4 +- tests/lib/rules/no-restricted-properties.js | 11 +++ tests/lib/rules/no-self-assign.js | 4 +- tests/lib/rules/sort-keys.js | 6 ++ tests/lib/rules/utils/ast-utils.js | 84 +++++++++++++++++++++ tests/lib/rules/yoda.js | 17 +++++ 7 files changed, 167 insertions(+), 18 deletions(-) diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js index 01c6b47b82e..b4e8e29806d 100644 --- a/lib/rules/utils/ast-utils.js +++ b/lib/rules/utils/ast-utils.js @@ -865,6 +865,44 @@ 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}`; + } + + // 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. @@ -911,23 +949,12 @@ module.exports = { // no default } - switch (prop && prop.type) { - case "Literal": - return String(prop.value); - - case "TemplateLiteral": - if (prop.expressions.length === 0 && prop.quasis.length === 1) { - return prop.quasis[0].value.cooked; - } - break; - - case "Identifier": - if (!node.computed) { - return prop.name; - } - break; + if (prop) { + if (prop.type === "Identifier" && !node.computed) { + return prop.name; + } - // no default + return module.exports.getStaticStringValue(prop); } return null; diff --git a/tests/lib/rules/no-dupe-keys.js b/tests/lib/rules/no-dupe-keys.js index a0a1e7a8c9e..a6062a2a24b 100644 --- a/tests/lib/rules/no-dupe-keys.js +++ b/tests/lib/rules/no-dupe-keys.js @@ -32,6 +32,7 @@ ruleTester.run("no-dupe-keys", rule, { { code: "var x = { a: b, ...c }", parserOptions: { ecmaVersion: 2018 } }, { code: "var x = { get a() {}, set a (value) {} };", parserOptions: { ecmaVersion: 6 } }, { code: "var x = { a: 1, b: { a: 2 } };", parserOptions: { ecmaVersion: 6 } }, + { code: "var x = ({ null: 1, [/(?0)/]: 2 })", parserOptions: { ecmaVersion: 2018 } }, { code: "var {a, a} = obj", parserOptions: { ecmaVersion: 6 } } ], invalid: [ @@ -44,6 +45,7 @@ ruleTester.run("no-dupe-keys", rule, { { code: "var foo = {\n bar: 1,\n bar: 1,\n}", errors: [{ messageId: "unexpected", data: { name: "bar" }, line: 3, column: 3 }] }, { code: "var x = { a: 1, get a() {} };", parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "unexpected", data: { name: "a" }, type: "ObjectExpression" }] }, { code: "var x = { a: 1, set a(value) {} };", parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "unexpected", data: { name: "a" }, type: "ObjectExpression" }] }, - { code: "var x = { a: 1, b: { a: 2 }, get b() {} };", parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "unexpected", data: { name: "b" }, type: "ObjectExpression" }] } + { code: "var x = { a: 1, b: { a: 2 }, get b() {} };", parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "unexpected", data: { name: "b" }, type: "ObjectExpression" }] }, + { code: "var x = ({ '/(?0)/': 1, [/(?0)/]: 2 })", parserOptions: { ecmaVersion: 2018 }, errors: [{ messageId: "unexpected", data: { name: "/(?0)/" }, type: "ObjectExpression" }] } ] }); diff --git a/tests/lib/rules/no-restricted-properties.js b/tests/lib/rules/no-restricted-properties.js index c1be7a12c84..9c5c57950f3 100644 --- a/tests/lib/rules/no-restricted-properties.js +++ b/tests/lib/rules/no-restricted-properties.js @@ -95,6 +95,12 @@ ruleTester.run("no-restricted-properties", rule, { options: [{ object: "foo" }] + }, { + code: "foo[/(?0)/]", + options: [{ + property: "null" + }], + parserOptions: { ecmaVersion: 2018 } }, { code: "let bar = foo;", options: [{ object: "foo", property: "bar" }], @@ -248,6 +254,11 @@ ruleTester.run("no-restricted-properties", rule, { code: "foo.bar.baz();", options: [{ property: "bar" }], errors: [{ message: "'bar' is restricted from being used.", type: "MemberExpression" }] + }, { + code: "foo[/(?0)/]", + options: [{ property: "/(?0)/" }], + parserOptions: { ecmaVersion: 2018 }, + errors: [{ message: "'/(?0)/' is restricted from being used.", type: "MemberExpression" }] }, { code: "require.call({}, 'foo')", options: [{ diff --git a/tests/lib/rules/no-self-assign.js b/tests/lib/rules/no-self-assign.js index eaa0d02d12d..c20c01fcb5a 100644 --- a/tests/lib/rules/no-self-assign.js +++ b/tests/lib/rules/no-self-assign.js @@ -50,6 +50,7 @@ ruleTester.run("no-self-assign", rule, { { code: "a[b] = a.b", options: [{ props: true }] }, { code: "a.b().c = a.b().c", options: [{ props: true }] }, { code: "b().c = b().c", options: [{ props: true }] }, + { code: "a.null = a[/(?0)/]", options: [{ props: true }], parserOptions: { ecmaVersion: 2018 } }, { code: "a[b + 1] = a[b + 1]", options: [{ props: true }] }, // it ignores non-simple computed properties. { code: "a.b = a.b", @@ -133,6 +134,7 @@ ruleTester.run("no-self-assign", rule, { code: "this.x = this.x", options: [{ props: true }], errors: ["'this.x' is assigned to itself."] - } + }, + { code: "a['/(?0)/'] = a[/(?0)/]", options: [{ props: true }], parserOptions: { ecmaVersion: 2018 }, errors: ["'a[/(?0)/]' is assigned to itself."] } ] }); diff --git a/tests/lib/rules/sort-keys.js b/tests/lib/rules/sort-keys.js index eb26c5250e5..31ca1c2ac21 100644 --- a/tests/lib/rules/sort-keys.js +++ b/tests/lib/rules/sort-keys.js @@ -33,6 +33,7 @@ ruleTester.run("sort-keys", rule, { { code: "var obj = {$:1, A:3, _:2, a:4}", options: [] }, { code: "var obj = {1:1, '11':2, 2:4, A:3}", options: [] }, { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: [] }, + { code: "var obj = { [/(?0)/]: 1, '/(?0)/': 2 }", options: [], parserOptions: { ecmaVersion: 2018 } }, // ignore non-simple computed properties. { code: "var obj = {a:1, b:3, [a + b]: -1, c:2}", options: [], parserOptions: { ecmaVersion: 6 } }, @@ -204,6 +205,11 @@ ruleTester.run("sort-keys", rule, { code: "var obj = {'#':1, À:3, 'Z':2, è:4}", errors: ["Expected object keys to be in ascending order. 'Z' should be before 'À'."] }, + { + code: "var obj = { null: 1, [/(?0)/]: 2 }", + parserOptions: { ecmaVersion: 2018 }, + errors: ["Expected object keys to be in ascending order. '/(?0)/' should be before 'null'."] + }, // not ignore properties not separated by spread properties { diff --git a/tests/lib/rules/utils/ast-utils.js b/tests/lib/rules/utils/ast-utils.js index fb5526ff610..1acc227015a 100644 --- a/tests/lib/rules/utils/ast-utils.js +++ b/tests/lib/rules/utils/ast-utils.js @@ -403,6 +403,83 @@ describe("ast-utils", () => { }); }); + describe("getStaticStringValue", () => { + + /* eslint-disable quote-props */ + const expectedResults = { + + // string literals + "''": "", + "'foo'": "foo", + + // boolean literals + "false": "false", + "true": "true", + + // null literal + "null": "null", + + // number literals + "0": "0", + "0.": "0", + ".0": "0", + "1": "1", + "1.": "1", + ".1": "0.1", + "12": "12", + ".12": "0.12", + "0.12": "0.12", + "12.34": "12.34", + "12e3": "12000", + "12e-3": "0.012", + "12.34e5": "1234000", + "12.34e-5": "0.0001234", + "011": "9", + "081": "81", + "0b11": "3", + "0b011": "3", + "0o11": "9", + "0o011": "9", + "0x11": "17", + "0x011": "17", + + // regexp literals + "/a/": "/a/", + "/a/i": "/a/i", + "/[0-9]/": "/[0-9]/", + "/(?0)/": "/(?0)/", + "/(?0)/s": "/(?0)/s", + "/(?<=a)b/s": "/(?<=a)b/s", + + // simple template literals + "``": "", + "`foo`": "foo", + + // unsupported + "`${''}`": null, + "`${0}`": null, + "tag``": null, + "-0": null, + "-1": null, + "1 + 2": null, + "[]": null, + "({})": null, + "foo": null, + "undefined": null, + "this": null, + "(function () {})": null + }; + /* eslint-enable quote-props */ + + Object.keys(expectedResults).forEach(key => { + it(`should return ${expectedResults[key]} for ${key}`, () => { + const ast = espree.parse(key, { ecmaVersion: 2018 }); + + assert.strictEqual(astUtils.getStaticStringValue(ast.body[0].expression), expectedResults[key]); + }); + }); + }); + describe("getStaticPropertyName", () => { it("should return 'b' for `a.b`", () => { const ast = espree.parse("a.b"); @@ -509,6 +586,13 @@ describe("ast-utils", () => { assert.strictEqual(astUtils.getStaticPropertyName(node), "100"); }); + it("should return '/(?0)/' for `[/(?0)/]: 1`", () => { + const ast = espree.parse("({[/(?0)/]: 1})", { ecmaVersion: 2018 }); + const node = ast.body[0].expression.properties[0]; + + assert.strictEqual(astUtils.getStaticPropertyName(node), "/(?0)/"); + }); + it("should return null for `[b]: 1`", () => { const ast = espree.parse("({[b]: 1})", { ecmaVersion: 6 }); const node = ast.body[0].expression.properties[0]; diff --git a/tests/lib/rules/yoda.js b/tests/lib/rules/yoda.js index 7908499d88f..414cb5a0460 100644 --- a/tests/lib/rules/yoda.js +++ b/tests/lib/rules/yoda.js @@ -91,6 +91,10 @@ ruleTester.run("yoda", rule, { }, { code: "if (0 <= a.b && a[\"b\"] <= 100) {}", options: ["never", { exceptRange: true }] + }, { + code: "if (1 <= a['/(?0)/'] && a[/(?0)/] <= 100) {}", + options: ["never", { exceptRange: true }], + parserOptions: { ecmaVersion: 2018 } }, // onlyEquality @@ -412,6 +416,19 @@ ruleTester.run("yoda", rule, { } ] }, + { + code: "if (0 <= a.null && a[/(?0)/] <= 1) {}", + output: "if (a.null >= 0 && a[/(?0)/] <= 1) {}", + options: ["never", { exceptRange: true }], + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + messageId: "expected", + data: { expectedSide: "right", operator: "<=" }, + type: "BinaryExpression" + } + ] + }, { code: "if (3 == a) {}", output: "if (a == 3) {}",