From ae3541bbdb167248a9f8229e42605e898c8c28fe Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Tue, 25 Aug 2020 18:52:14 +0200 Subject: [PATCH 01/15] update operator-assignment rule --- docs/rules/operator-assignment.md | 2 ++ lib/rules/operator-assignment.js | 2 +- lib/rules/utils/ast-utils.js | 14 ++++++++++++- tests/lib/rules/operator-assignment.js | 28 +++++++++++++++++++++++++- tests/lib/rules/utils/ast-utils.js | 24 ++++++++++++++++++++++ 5 files changed, 67 insertions(+), 3 deletions(-) diff --git a/docs/rules/operator-assignment.md b/docs/rules/operator-assignment.md index 977edfd34c5..789e4cf1d54 100644 --- a/docs/rules/operator-assignment.md +++ b/docs/rules/operator-assignment.md @@ -23,6 +23,8 @@ JavaScript provides shorthand operators that combine variable assignment and som This rule requires or disallows assignment operator shorthand where possible. +The rule applies to the operators listed in the above table. + ## Options This rule has a single string option: diff --git a/lib/rules/operator-assignment.js b/lib/rules/operator-assignment.js index aee79077f44..fdb0884922b 100644 --- a/lib/rules/operator-assignment.js +++ b/lib/rules/operator-assignment.js @@ -151,7 +151,7 @@ module.exports = { * @returns {void} */ function prohibit(node) { - if (node.operator !== "=") { + if (node.operator !== "=" && !astUtils.isLogicalAssignmentOperator(node.operator)) { context.report({ node, messageId: "unexpected", diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js index d0dd770d199..e387c829009 100644 --- a/lib/rules/utils/ast-utils.js +++ b/lib/rules/utils/ast-utils.js @@ -40,6 +40,8 @@ const STATEMENT_LIST_PARENTS = new Set(["Program", "BlockStatement", "SwitchCase const DECIMAL_INTEGER_PATTERN = /^(0|[1-9]\d*)$/u; const OCTAL_ESCAPE_PATTERN = /^(?:[^\\]|\\[^0-7]|\\0(?![0-9]))*\\(?:[1-7]|0[0-9])/u; +const LOGICAL_ASSIGNMENT_OPERATORS = new Set(["&&=", "||=", "??="]); + /** * Checks reference if is non initializer and writable. * @param {Reference} reference A reference to check. @@ -722,6 +724,15 @@ function isMixedLogicalAndCoalesceExpressions(left, right) { ); } +/** + * Checks if the given operator is a logical assignment operator. + * @param {string} operator The operator to check. + * @returns {boolean} `true` if the operator is a logical assignment operator. + */ +function isLogicalAssignmentOperator(operator) { + return LOGICAL_ASSIGNMENT_OPERATORS.has(operator); +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -1754,5 +1765,6 @@ module.exports = { isSpecificId, isSpecificMemberAccess, equalLiteralValue, - isSameReference + isSameReference, + isLogicalAssignmentOperator }; diff --git a/tests/lib/rules/operator-assignment.js b/tests/lib/rules/operator-assignment.js index ca7dfce6911..1d07b0cd8ed 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: 2020 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2021 } }); const EXPECTED_OPERATOR_ASSIGNMENT = [{ messageId: "replaced", type: "AssignmentExpression" }]; const UNEXPECTED_OPERATOR_ASSIGNMENT = [{ messageId: "unexpected", type: "AssignmentExpression" }]; @@ -84,6 +84,32 @@ ruleTester.run("operator-assignment", rule, { { code: "this.x = foo.this.x + y", options: ["always"] + }, + + // does not check logical operators + { + code: "x = x && y", + options: ["always"] + }, + { + code: "x = x || y", + options: ["always"] + }, + { + code: "x = x ?? y", + options: ["always"] + }, + { + code: "x &&= y", + options: ["never"] + }, + { + code: "x ||= y", + options: ["never"] + }, + { + code: "x ??= y", + options: ["never"] } ], diff --git a/tests/lib/rules/utils/ast-utils.js b/tests/lib/rules/utils/ast-utils.js index e3790f50f3f..d07345b8b3e 100644 --- a/tests/lib/rules/utils/ast-utils.js +++ b/tests/lib/rules/utils/ast-utils.js @@ -1639,4 +1639,28 @@ describe("ast-utils", () => { }); }); }); + + describe("isLogicalAssignmentOperator", () => { + const expectedResults = { + "&&=": true, + "||=": true, + "??=": true, + "&&": false, + "||": false, + "??": false, + "=": false, + "&=": false, + "|=": false, + "+=": false, + "**=": false, + "==": false, + "===": false + }; + + Object.entries(expectedResults).forEach(([key, value]) => { + it(`should return ${value} for ${key}`, () => { + assert.strictEqual(astUtils.isLogicalAssignmentOperator(key), value); + }); + }); + }); }); From d9766a1bd6e48548bddd5349eb884d9f097d5091 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Tue, 25 Aug 2020 22:33:53 +0200 Subject: [PATCH 02/15] update astUtils.couldBeError --- lib/rules/utils/ast-utils.js | 15 +++++++++++- tests/lib/rules/no-throw-literal.js | 21 +++++++++++++++-- .../lib/rules/prefer-promise-reject-errors.js | 23 ++++++++++++++++--- tests/lib/rules/utils/ast-utils.js | 17 +++++++++++++- 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js index e387c829009..75d7f445087 100644 --- a/lib/rules/utils/ast-utils.js +++ b/lib/rules/utils/ast-utils.js @@ -1578,7 +1578,20 @@ module.exports = { return true; // possibly an error object. case "AssignmentExpression": - return module.exports.couldBeError(node.right); + if (node.operator === "=") { + return module.exports.couldBeError(node.right); + } + + if (isLogicalAssignmentOperator(node.operator)) { + return module.exports.couldBeError(node.left) || module.exports.couldBeError(node.right); + } + + /** + * All other assignment operators are mathematical assignment operators (arithmetic or bitwise). + * An assignment expression with a mathematical operator can either evaluate to a primitive value, + * or throw, depending on the operands. Thus, it cannot evaluate to an `Error` object. + */ + return false; case "SequenceExpression": { const exprs = node.expressions; diff --git a/tests/lib/rules/no-throw-literal.js b/tests/lib/rules/no-throw-literal.js index 7836cec7837..a1f32bc1d46 100644 --- a/tests/lib/rules/no-throw-literal.js +++ b/tests/lib/rules/no-throw-literal.js @@ -30,7 +30,10 @@ ruleTester.run("no-throw-literal", rule, { "throw new foo();", // NewExpression "throw foo.bar;", // MemberExpression "throw foo[bar];", // MemberExpression - "throw foo = new Error();", // AssignmentExpression + "throw foo = new Error();", // AssignmentExpression with the `=` operator + { code: "throw foo &&= 'literal'", parserOptions: { ecmaVersion: 2021 } }, // AssignmentExpression with a logical operator + { code: "throw foo.bar ||= 'literal'", parserOptions: { ecmaVersion: 2021 } }, // AssignmentExpression with a logical operator + { code: "throw foo[bar] ??= 'literal'", parserOptions: { ecmaVersion: 2021 } }, // AssignmentExpression with a logical operator "throw 1, 2, new Error();", // SequenceExpression "throw 'literal' && new Error();", // LogicalExpression (right) "throw new Error() || 'literal';", // LogicalExpression (left) @@ -104,7 +107,21 @@ ruleTester.run("no-throw-literal", rule, { // AssignmentExpression { - code: "throw foo = 'error';", + code: "throw foo = 'error';", // RHS is a literal + errors: [{ + messageId: "object", + type: "ThrowStatement" + }] + }, + { + code: "throw foo += new Error();", // evaluates to a primitive value, or throws while evaluating + errors: [{ + messageId: "object", + type: "ThrowStatement" + }] + }, + { + code: "throw foo &= new Error();", // evaluates to a primitive value, or throws while evaluating errors: [{ messageId: "object", type: "ThrowStatement" diff --git a/tests/lib/rules/prefer-promise-reject-errors.js b/tests/lib/rules/prefer-promise-reject-errors.js index de8f4c72524..2230f8350d4 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: 2020 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2021 } }); ruleTester.run("prefer-promise-reject-errors", rule, { @@ -29,6 +29,8 @@ ruleTester.run("prefer-promise-reject-errors", rule, { "Promise.reject(new Error())", "Promise.reject(new TypeError)", "Promise.reject(new Error('foo'))", + "Promise.reject(foo || 5)", + "Promise.reject(5 && foo)", "new Foo((resolve, reject) => reject(5))", "new Promise(function(resolve, reject) { return function(reject) { reject(5) } })", "new Promise(function(resolve, reject) { if (foo) { const reject = somethingElse; reject(5) } })", @@ -46,7 +48,13 @@ ruleTester.run("prefer-promise-reject-errors", rule, { // Optional chaining "Promise.reject(obj?.foo)", - "Promise.reject(obj?.foo())" + "Promise.reject(obj?.foo())", + + // Assignments + "Promise.reject(foo = new Error())", + "Promise.reject(foo ||= 5)", + "Promise.reject(foo.bar ??= 5)", + "Promise.reject(foo[bar] &&= 5)" ], invalid: [ @@ -98,7 +106,16 @@ ruleTester.run("prefer-promise-reject-errors", rule, { "Promise?.reject(5)", "Promise?.reject?.(5)", "(Promise?.reject)(5)", - "(Promise?.reject)?.(5)" + "(Promise?.reject)?.(5)", + + // Assignments with mathematical operators will either evaluate to a primitive value or throw a TypeError + "Promise.reject(foo += new Error())", + "Promise.reject(foo -= new Error())", + "Promise.reject(foo **= new Error())", + "Promise.reject(foo <<= new Error())", + "Promise.reject(foo |= new Error())", + "Promise.reject(foo &= new Error())" + ].map(invalidCase => { const errors = { errors: [{ messageId: "rejectAnError", type: "CallExpression" }] }; diff --git a/tests/lib/rules/utils/ast-utils.js b/tests/lib/rules/utils/ast-utils.js index d07345b8b3e..6fbaef478e2 100644 --- a/tests/lib/rules/utils/ast-utils.js +++ b/tests/lib/rules/utils/ast-utils.js @@ -1014,12 +1014,27 @@ describe("ast-utils", () => { "foo.bar": true, "(foo = bar)": true, "(foo = 1)": false, + "(foo += bar)": false, + "(foo -= bar)": false, + "(foo *= bar)": false, + "(foo /= bar)": false, + "(foo %= bar)": false, + "(foo **= bar)": false, + "(foo <<= bar)": false, + "(foo >>= bar)": false, + "(foo >>>= bar)": false, + "(foo &= bar)": false, + "(foo |= bar)": false, + "(foo ^= bar)": false, "(1, 2, 3)": false, "(foo, 2, 3)": false, "(1, 2, foo)": true, "1 && 2": false, "1 && foo": true, "foo && 2": true, + "foo &&= 2": true, + "foo.bar ??= 2": true, + "foo[bar] ||= 2": true, "foo ? 1 : 2": false, "foo ? bar : 2": true, "foo ? 1 : bar": true, @@ -1029,7 +1044,7 @@ describe("ast-utils", () => { Object.keys(EXPECTED_RESULTS).forEach(key => { it(`returns ${EXPECTED_RESULTS[key]} for ${key}`, () => { - const ast = espree.parse(key, { ecmaVersion: 6 }); + const ast = espree.parse(key, { ecmaVersion: 2021 }); assert.strictEqual(astUtils.couldBeError(ast.body[0].expression), EXPECTED_RESULTS[key]); }); From 07ba492d2cc6ff1dd29155b4e93977ba230c87de Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Tue, 25 Aug 2020 23:32:31 +0200 Subject: [PATCH 03/15] update constructor-super rule --- lib/rules/constructor-super.js | 25 ++++++++++++++++++- tests/lib/rules/constructor-super.js | 37 +++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/lib/rules/constructor-super.js b/lib/rules/constructor-super.js index 65ed7422c25..edd15002927 100644 --- a/lib/rules/constructor-super.js +++ b/lib/rules/constructor-super.js @@ -5,6 +5,13 @@ "use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ @@ -60,7 +67,23 @@ function isPossibleConstructor(node) { return node.name !== "undefined"; case "AssignmentExpression": - return isPossibleConstructor(node.right); + if (node.operator === "=") { + return isPossibleConstructor(node.right); + } + + if (astUtils.isLogicalAssignmentOperator(node.operator)) { + return ( + isPossibleConstructor(node.left) || + isPossibleConstructor(node.right) + ); + } + + /** + * All other assignment operators are mathematical assignment operators (arithmetic or bitwise). + * An assignment expression with a mathematical operator can either evaluate to a primitive value, + * or throw, depending on the operands. Thus, it cannot evaluate to a constructor function. + */ + return false; case "LogicalExpression": return ( diff --git a/tests/lib/rules/constructor-super.js b/tests/lib/rules/constructor-super.js index e20da576b5e..7a3a812e96b 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: 2020 } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2021 } }); ruleTester.run("constructor-super", rule, { valid: [ @@ -37,7 +37,18 @@ ruleTester.run("constructor-super", rule, { "class A extends B { constructor() { if (true) { super(); } else { super(); } } }", "class A extends (class B {}) { constructor() { super(); } }", "class A extends (B = C) { constructor() { super(); } }", + "class A extends (B &&= C) { constructor() { super(); } }", + "class A extends (B ||= C) { constructor() { super(); } }", + "class A extends (B ??= C) { constructor() { super(); } }", + "class A extends (B &&= 5) { constructor() { super(); } }", + "class A extends (B ||= 5) { constructor() { super(); } }", + "class A extends (B ??= 5) { constructor() { super(); } }", "class A extends (B || C) { constructor() { super(); } }", + "class A extends (B && 5) { constructor() { super(); } }", + "class A extends (5 && B) { constructor() { super(); } }", + "class A extends (B || 5) { constructor() { super(); } }", + "class A extends (B ?? 5) { constructor() { super(); } }", + "class A extends (a ? B : C) { constructor() { super(); } }", "class A extends (B, C) { constructor() { super(); } }", @@ -112,6 +123,30 @@ ruleTester.run("constructor-super", rule, { code: "class A extends 'test' { constructor() { super(); } }", errors: [{ messageId: "badSuper", type: "CallExpression" }] }, + { + code: "class A extends (B = 5) { constructor() { super(); } }", + errors: [{ messageId: "badSuper", type: "CallExpression" }] + }, + { + code: "class A extends (B += C) { constructor() { super(); } }", + errors: [{ messageId: "badSuper", type: "CallExpression" }] + }, + { + code: "class A extends (B -= C) { constructor() { super(); } }", + errors: [{ messageId: "badSuper", type: "CallExpression" }] + }, + { + code: "class A extends (B **= C) { constructor() { super(); } }", + errors: [{ messageId: "badSuper", type: "CallExpression" }] + }, + { + code: "class A extends (B |= C) { constructor() { super(); } }", + errors: [{ messageId: "badSuper", type: "CallExpression" }] + }, + { + code: "class A extends (B &= C) { constructor() { super(); } }", + errors: [{ messageId: "badSuper", type: "CallExpression" }] + }, // derived classes. { From ce386e67787a86b26d2e915a4ae4c64a796bea41 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 26 Aug 2020 00:59:29 +0200 Subject: [PATCH 04/15] add tests for operator-linebreak rule --- tests/lib/rules/operator-linebreak.js | 134 +++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/tests/lib/rules/operator-linebreak.js b/tests/lib/rules/operator-linebreak.js index 18a37e3fc80..1eeb9f5790e 100644 --- a/tests/lib/rules/operator-linebreak.js +++ b/tests/lib/rules/operator-linebreak.js @@ -57,7 +57,48 @@ ruleTester.run("operator-linebreak", rule, { { code: "1 + 1\n", options: ["none"] }, { code: "answer = everything ? 42 : foo;", options: ["none"] }, { code: "answer = everything \n?\n 42 : foo;", options: [null, { overrides: { "?": "ignore" } }] }, - { code: "answer = everything ? 42 \n:\n foo;", options: [null, { overrides: { ":": "ignore" } }] } + { code: "answer = everything ? 42 \n:\n foo;", options: [null, { overrides: { ":": "ignore" } }] }, + + { + code: "a \n &&= b", + options: ["after", { overrides: { "&&=": "ignore" } }], + parserOptions: { ecmaVersion: 2021 } + }, + { + code: "a ??= \n b", + options: ["before", { overrides: { "??=": "ignore" } }], + parserOptions: { ecmaVersion: 2021 } + }, + { + code: "a ||= \n b", + options: ["after", { overrides: { "=": "before" } }], + parserOptions: { ecmaVersion: 2021 } + }, + { + code: "a \n &&= b", + options: ["before", { overrides: { "&=": "after" } }], + parserOptions: { ecmaVersion: 2021 } + }, + { + code: "a \n ||= b", + options: ["before", { overrides: { "|=": "after" } }], + parserOptions: { ecmaVersion: 2021 } + }, + { + code: "a &&= \n b", + options: ["after", { overrides: { "&&": "before" } }], + parserOptions: { ecmaVersion: 2021 } + }, + { + code: "a ||= \n b", + options: ["after", { overrides: { "||": "before" } }], + parserOptions: { ecmaVersion: 2021 } + }, + { + code: "a ??= \n b", + options: ["after", { overrides: { "??": "before" } }], + parserOptions: { ecmaVersion: 2021 } + } ], invalid: [ @@ -638,6 +679,97 @@ ruleTester.run("operator-linebreak", rule, { messageId: "operatorAtBeginning", data: { operator: "??" } }] + }, + + { + code: "a \n &&= b", + output: "a &&= \n b", + options: ["after"], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "operatorAtEnd", + data: { operator: "&&=" }, + type: "AssignmentExpression", + line: 2, + column: 3, + endLine: 2, + endColumn: 6 + }] + }, + { + code: "a ||=\n b", + output: "a\n ||= b", + options: ["before"], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "operatorAtBeginning", + data: { operator: "||=" }, + type: "AssignmentExpression", + line: 1, + column: 3, + endLine: 1, + endColumn: 6 + }] + }, + { + code: "a ??=\n b", + output: "a ??= b", + options: ["none"], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "noLinebreak", + data: { operator: "??=" }, + type: "AssignmentExpression", + line: 1, + column: 4, + endLine: 1, + endColumn: 7 + }] + }, + { + code: "a \n &&= b", + output: "a &&= b", + options: ["before", { overrides: { "&&=": "none" } }], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "noLinebreak", + data: { operator: "&&=" }, + type: "AssignmentExpression", + line: 2, + column: 3, + endLine: 2, + endColumn: 6 + }] + }, + { + code: "a ||=\nb", + output: "a\n||= b", + options: ["after", { overrides: { "||=": "before" } }], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "operatorAtBeginning", + data: { operator: "||=" }, + type: "AssignmentExpression", + line: 1, + column: 3, + endLine: 1, + endColumn: 6 + }] + }, + { + code: "a\n??=b", + output: "a??=\nb", + options: ["none", { overrides: { "??=": "after" } }], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "operatorAtEnd", + data: { operator: "??=" }, + type: "AssignmentExpression", + line: 2, + column: 1, + endLine: 2, + endColumn: 4 + }] } ] }); From 0cf40adb06d1d6ec695b945ae3599d4a5ce9aeec Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 26 Aug 2020 01:23:55 +0200 Subject: [PATCH 05/15] add tests for space-infix-ops rule --- tests/lib/rules/space-infix-ops.js | 50 ++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/tests/lib/rules/space-infix-ops.js b/tests/lib/rules/space-infix-ops.js index 65c1e562002..2e9edbaa8cf 100644 --- a/tests/lib/rules/space-infix-ops.js +++ b/tests/lib/rules/space-infix-ops.js @@ -47,7 +47,11 @@ ruleTester.run("space-infix-ops", rule, { { code: "const foo = function(a: number = 0): Bar { };", parser: parser("type-annotations/function-expression-type-annotation"), parserOptions: { ecmaVersion: 6 } }, // TypeScript Type Aliases - { code: "type Foo = T;", parser: parser("typescript-parsers/type-alias"), parserOptions: { ecmaVersion: 6 } } + { code: "type Foo = T;", parser: parser("typescript-parsers/type-alias"), parserOptions: { ecmaVersion: 6 } }, + + { code: "a &&= b", parserOptions: { ecmaVersion: 2021 } }, + { code: "a ||= b", parserOptions: { ecmaVersion: 2021 } }, + { code: "a ??= b", parserOptions: { ecmaVersion: 2021 } } ], invalid: [ { @@ -408,7 +412,6 @@ ruleTester.run("space-infix-ops", rule, { }, // Type Annotations - { code: "var a: Foo= b;", output: "var a: Foo = b;", @@ -433,6 +436,49 @@ ruleTester.run("space-infix-ops", rule, { column: 23, type: "AssignmentPattern" }] + }, + + { + code: "a&&=b", + output: "a &&= b", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "missingSpace", + data: { operator: "&&=" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 5, + type: "AssignmentExpression" + }] + }, + { + code: "a ||=b", + output: "a ||= b", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "missingSpace", + data: { operator: "||=" }, + line: 1, + column: 3, + endLine: 1, + endColumn: 6, + type: "AssignmentExpression" + }] + }, + { + code: "a??= b", + output: "a ??= b", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "missingSpace", + data: { operator: "??=" }, + line: 1, + column: 2, + endLine: 1, + endColumn: 5, + type: "AssignmentExpression" + }] } ] }); From f64e9980128df9295821b78ae5f321a12055eb92 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 26 Aug 2020 01:50:27 +0200 Subject: [PATCH 06/15] add tests for no-invalid-this rule --- tests/lib/rules/no-invalid-this.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/lib/rules/no-invalid-this.js b/tests/lib/rules/no-invalid-this.js index 3eb4e7b0960..6e1b757a712 100644 --- a/tests/lib/rules/no-invalid-this.js +++ b/tests/lib/rules/no-invalid-this.js @@ -719,6 +719,24 @@ const patterns = [ errors, valid: [NORMAL], invalid: [USE_STRICT, IMPLIED_STRICT, MODULES] + }, + { + code: "obj.method &&= function () { console.log(this); z(x => console.log(x, this)); }", + parserOptions: { ecmaVersion: 2021 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "obj.method ||= function () { console.log(this); z(x => console.log(x, this)); }", + parserOptions: { ecmaVersion: 2021 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "obj.method ??= function () { console.log(this); z(x => console.log(x, this)); }", + parserOptions: { ecmaVersion: 2021 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] } ]; From d2e7f024b1c0012aef6a17af40dfa6278ffbaa67 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 26 Aug 2020 02:03:33 +0200 Subject: [PATCH 07/15] add tests for no-param-reassign rule --- tests/lib/rules/no-param-reassign.js | 51 ++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/lib/rules/no-param-reassign.js b/tests/lib/rules/no-param-reassign.js index a79249d1ef6..5f521cbc3cd 100644 --- a/tests/lib/rules/no-param-reassign.js +++ b/tests/lib/rules/no-param-reassign.js @@ -368,6 +368,57 @@ ruleTester.run("no-param-reassign", rule, { messageId: "assignmentToFunctionParamProp", data: { name: "a" } }] + }, + { + code: "function foo(a) { a &&= b; }", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "assignmentToFunctionParam", + data: { name: "a" } + }] + }, + { + code: "function foo(a) { a ||= b; }", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "assignmentToFunctionParam", + data: { name: "a" } + }] + }, + { + code: "function foo(a) { a ??= b; }", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "assignmentToFunctionParam", + data: { name: "a" } + }] + }, + { + code: "function foo(a) { a.b &&= c; }", + options: [{ props: true }], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "assignmentToFunctionParamProp", + data: { name: "a" } + }] + }, + { + code: "function foo(a) { a.b.c ||= d; }", + options: [{ props: true }], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "assignmentToFunctionParamProp", + data: { name: "a" } + }] + }, + { + code: "function foo(a) { a[b] ??= c; }", + options: [{ props: true }], + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "assignmentToFunctionParamProp", + data: { name: "a" } + }] } ] }); From 1210ab181ceae62a3c90a2766bcf56f43089eecd Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 26 Aug 2020 02:09:37 +0200 Subject: [PATCH 08/15] add tests for no-bitwise rule --- tests/lib/rules/no-bitwise.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/lib/rules/no-bitwise.js b/tests/lib/rules/no-bitwise.js index ae3e8a8084a..25cc286f95e 100644 --- a/tests/lib/rules/no-bitwise.js +++ b/tests/lib/rules/no-bitwise.js @@ -22,7 +22,12 @@ ruleTester.run("no-bitwise", rule, { valid: [ "a + b", "!a", + "a && b", + "a || b", "a += b", + { code: "a &&= b", parserOptions: { ecmaVersion: 2021 } }, + { code: "a ||= b", parserOptions: { ecmaVersion: 2021 } }, + { code: "a ??= b", parserOptions: { ecmaVersion: 2021 } }, { code: "~[1, 2, 3].indexOf(1)", options: [{ allow: ["~"] }] }, { code: "~1<<2 === -8", options: [{ allow: ["~", "<<"] }] }, { code: "a|0", options: [{ int32Hint: true }] }, From 3e0291b306a6bdae6945350082ca468e26804413 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 26 Aug 2020 02:29:04 +0200 Subject: [PATCH 09/15] add tests for func-name-matching rule --- tests/lib/rules/func-name-matching.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/lib/rules/func-name-matching.js b/tests/lib/rules/func-name-matching.js index 4add17ea9d0..908e9108f3e 100644 --- a/tests/lib/rules/func-name-matching.js +++ b/tests/lib/rules/func-name-matching.js @@ -29,6 +29,9 @@ ruleTester.run("func-name-matching", rule, { "foo = function foo() {};", { code: "foo = function foo() {};", options: ["always"] }, { code: "foo = function bar() {};", options: ["never"] }, + { code: "foo &&= function foo() {};", parserOptions: { ecmaVersion: 2021 } }, + { code: "obj.foo ||= function foo() {};", parserOptions: { ecmaVersion: 2021 } }, + { code: "obj['foo'] ??= function foo() {};", parserOptions: { ecmaVersion: 2021 } }, "obj.foo = function foo() {};", { code: "obj.foo = function foo() {};", options: ["always"] }, { code: "obj.foo = function bar() {};", options: ["never"] }, @@ -284,6 +287,27 @@ ruleTester.run("func-name-matching", rule, { { messageId: "matchVariable", data: { funcName: "bar", name: "foo" } } ] }, + { + code: "foo &&= function bar() {};", + parserOptions: { ecmaVersion: 2021 }, + errors: [ + { messageId: "matchVariable", data: { funcName: "bar", name: "foo" } } + ] + }, + { + code: "obj.foo ||= function bar() {};", + parserOptions: { ecmaVersion: 2021 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "bar", name: "foo" } } + ] + }, + { + code: "obj['foo'] ??= function bar() {};", + parserOptions: { ecmaVersion: 2021 }, + errors: [ + { messageId: "matchProperty", data: { funcName: "bar", name: "foo" } } + ] + }, { code: "obj.foo = function bar() {};", parserOptions: { ecmaVersion: 6 }, From 71088a4ec4d3933760f1e06ef0138d9232482944 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 26 Aug 2020 02:37:58 +0200 Subject: [PATCH 10/15] add tests for prefer-destructuring rule --- tests/lib/rules/prefer-destructuring.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/lib/rules/prefer-destructuring.js b/tests/lib/rules/prefer-destructuring.js index c3d9c65706d..517158efb88 100644 --- a/tests/lib/rules/prefer-destructuring.js +++ b/tests/lib/rules/prefer-destructuring.js @@ -98,7 +98,19 @@ ruleTester.run("prefer-destructuring", rule, { }, "[foo] = array;", "foo += array[0]", + { + code: "foo &&= array[0]", + parserOptions: { ecmaVersion: 2021 } + }, "foo += bar.foo", + { + code: "foo ||= bar.foo", + parserOptions: { ecmaVersion: 2021 } + }, + { + code: "foo ??= bar['foo']", + parserOptions: { ecmaVersion: 2021 } + }, { code: "foo = object.foo;", options: [{ AssignmentExpression: { object: false } }, { enforceForRenamedProperties: true }] From 04c6323e8e16de1dcc0f7f63a01fed6e4184d557 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 26 Aug 2020 02:54:05 +0200 Subject: [PATCH 11/15] add tests for no-extend-native rule --- tests/lib/rules/no-extend-native.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/lib/rules/no-extend-native.js b/tests/lib/rules/no-extend-native.js index db07c123314..69bb4e0f7e9 100644 --- a/tests/lib/rules/no-extend-native.js +++ b/tests/lib/rules/no-extend-native.js @@ -160,6 +160,23 @@ ruleTester.run("no-extend-native", rule, { code: "(Object?.defineProperty)(Object.prototype, 'p', { value: 0 })", parserOptions: { ecmaVersion: 2020 }, errors: [{ messageId: "unexpected", data: { builtin: "Object" } }] + }, + + // Logical assignments + { + code: "Array.prototype.p &&= 0", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ messageId: "unexpected", data: { builtin: "Array" } }] + }, + { + code: "Array.prototype.p ||= 0", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ messageId: "unexpected", data: { builtin: "Array" } }] + }, + { + code: "Array.prototype.p ??= 0", + parserOptions: { ecmaVersion: 2021 }, + errors: [{ messageId: "unexpected", data: { builtin: "Array" } }] } ] From f47602d7a1fcf221281266d97330fbe5de133bb0 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 29 Aug 2020 02:45:15 +0200 Subject: [PATCH 12/15] Update docs/rules/operator-assignment.md Co-authored-by: Brandon Mills --- docs/rules/operator-assignment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/operator-assignment.md b/docs/rules/operator-assignment.md index 789e4cf1d54..b926462b309 100644 --- a/docs/rules/operator-assignment.md +++ b/docs/rules/operator-assignment.md @@ -23,7 +23,7 @@ JavaScript provides shorthand operators that combine variable assignment and som This rule requires or disallows assignment operator shorthand where possible. -The rule applies to the operators listed in the above table. +The rule applies to the operators listed in the above table. It does not report the logical assignment operators `&&=`, `||=`, and `??=` because their short-circuiting behavior is different from the other assignment operators. ## Options From 6ca0ce0fd268c10c4ee5dbfb0d878751529376d1 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 29 Aug 2020 03:39:34 +0200 Subject: [PATCH 13/15] Fix &&= in astUtils.couldBeError --- lib/rules/utils/ast-utils.js | 4 ++-- tests/lib/rules/no-throw-literal.js | 9 ++++++++- tests/lib/rules/prefer-promise-reject-errors.js | 7 +++++-- tests/lib/rules/utils/ast-utils.js | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js index 75d7f445087..cee6975d350 100644 --- a/lib/rules/utils/ast-utils.js +++ b/lib/rules/utils/ast-utils.js @@ -1578,11 +1578,11 @@ module.exports = { return true; // possibly an error object. case "AssignmentExpression": - if (node.operator === "=") { + if (["=", "&&="].includes(node.operator)) { return module.exports.couldBeError(node.right); } - if (isLogicalAssignmentOperator(node.operator)) { + if (["||=", "??="].includes(node.operator)) { return module.exports.couldBeError(node.left) || module.exports.couldBeError(node.right); } diff --git a/tests/lib/rules/no-throw-literal.js b/tests/lib/rules/no-throw-literal.js index a1f32bc1d46..745044abe47 100644 --- a/tests/lib/rules/no-throw-literal.js +++ b/tests/lib/rules/no-throw-literal.js @@ -31,7 +31,6 @@ ruleTester.run("no-throw-literal", rule, { "throw foo.bar;", // MemberExpression "throw foo[bar];", // MemberExpression "throw foo = new Error();", // AssignmentExpression with the `=` operator - { code: "throw foo &&= 'literal'", parserOptions: { ecmaVersion: 2021 } }, // AssignmentExpression with a logical operator { code: "throw foo.bar ||= 'literal'", parserOptions: { ecmaVersion: 2021 } }, // AssignmentExpression with a logical operator { code: "throw foo[bar] ??= 'literal'", parserOptions: { ecmaVersion: 2021 } }, // AssignmentExpression with a logical operator "throw 1, 2, new Error();", // SequenceExpression @@ -127,6 +126,14 @@ ruleTester.run("no-throw-literal", rule, { type: "ThrowStatement" }] }, + { + code: "throw foo &&= 'literal'", // evaluates either to a falsy value of `foo` (which, then, cannot be an Error object), or to 'literal' + parserOptions: { ecmaVersion: 2021 }, + errors: [{ + messageId: "object", + type: "ThrowStatement" + }] + }, // SequenceExpression { diff --git a/tests/lib/rules/prefer-promise-reject-errors.js b/tests/lib/rules/prefer-promise-reject-errors.js index 2230f8350d4..23e299bc983 100644 --- a/tests/lib/rules/prefer-promise-reject-errors.js +++ b/tests/lib/rules/prefer-promise-reject-errors.js @@ -54,7 +54,7 @@ ruleTester.run("prefer-promise-reject-errors", rule, { "Promise.reject(foo = new Error())", "Promise.reject(foo ||= 5)", "Promise.reject(foo.bar ??= 5)", - "Promise.reject(foo[bar] &&= 5)" + "Promise.reject(foo[bar] ??= 5)" ], invalid: [ @@ -114,7 +114,10 @@ ruleTester.run("prefer-promise-reject-errors", rule, { "Promise.reject(foo **= new Error())", "Promise.reject(foo <<= new Error())", "Promise.reject(foo |= new Error())", - "Promise.reject(foo &= new Error())" + "Promise.reject(foo &= new Error())", + + // evaluates either to a falsy value of `foo` (which, then, cannot be an Error object), or to `5` + "Promise.reject(foo &&= 5)" ].map(invalidCase => { const errors = { errors: [{ messageId: "rejectAnError", type: "CallExpression" }] }; diff --git a/tests/lib/rules/utils/ast-utils.js b/tests/lib/rules/utils/ast-utils.js index 6fbaef478e2..3026c4a0fbf 100644 --- a/tests/lib/rules/utils/ast-utils.js +++ b/tests/lib/rules/utils/ast-utils.js @@ -1032,7 +1032,7 @@ describe("ast-utils", () => { "1 && 2": false, "1 && foo": true, "foo && 2": true, - "foo &&= 2": true, + "foo &&= 2": false, "foo.bar ??= 2": true, "foo[bar] ||= 2": true, "foo ? 1 : 2": false, From cdc991de086cb874eb9042031e9b3be66df9d2ca Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 29 Aug 2020 04:02:30 +0200 Subject: [PATCH 14/15] Fix &&= in constructor-super --- lib/rules/constructor-super.js | 11 ++--------- tests/lib/rules/constructor-super.js | 7 ++++++- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/rules/constructor-super.js b/lib/rules/constructor-super.js index edd15002927..8787fc569a4 100644 --- a/lib/rules/constructor-super.js +++ b/lib/rules/constructor-super.js @@ -5,13 +5,6 @@ "use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const astUtils = require("./utils/ast-utils"); - //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ @@ -67,11 +60,11 @@ function isPossibleConstructor(node) { return node.name !== "undefined"; case "AssignmentExpression": - if (node.operator === "=") { + if (["=", "&&="].includes(node.operator)) { return isPossibleConstructor(node.right); } - if (astUtils.isLogicalAssignmentOperator(node.operator)) { + if (["||=", "??="].includes(node.operator)) { return ( isPossibleConstructor(node.left) || isPossibleConstructor(node.right) diff --git a/tests/lib/rules/constructor-super.js b/tests/lib/rules/constructor-super.js index 7a3a812e96b..c7154cab0be 100644 --- a/tests/lib/rules/constructor-super.js +++ b/tests/lib/rules/constructor-super.js @@ -40,7 +40,6 @@ ruleTester.run("constructor-super", rule, { "class A extends (B &&= C) { constructor() { super(); } }", "class A extends (B ||= C) { constructor() { super(); } }", "class A extends (B ??= C) { constructor() { super(); } }", - "class A extends (B &&= 5) { constructor() { super(); } }", "class A extends (B ||= 5) { constructor() { super(); } }", "class A extends (B ??= 5) { constructor() { super(); } }", "class A extends (B || C) { constructor() { super(); } }", @@ -127,6 +126,12 @@ ruleTester.run("constructor-super", rule, { code: "class A extends (B = 5) { constructor() { super(); } }", errors: [{ messageId: "badSuper", type: "CallExpression" }] }, + { + + // `B &&= 5` evaluates either to a falsy value of `B` (which, then, cannot be an Error object), or to '5' + code: "class A extends (B &&= 5) { constructor() { super(); } }", + errors: [{ messageId: "badSuper", type: "CallExpression" }] + }, { code: "class A extends (B += C) { constructor() { super(); } }", errors: [{ messageId: "badSuper", type: "CallExpression" }] From 274beed885101bf78bdf070ea1a95cd813fadcfa Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 29 Aug 2020 04:04:26 +0200 Subject: [PATCH 15/15] Fix comment --- tests/lib/rules/constructor-super.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/rules/constructor-super.js b/tests/lib/rules/constructor-super.js index c7154cab0be..bfde6a85508 100644 --- a/tests/lib/rules/constructor-super.js +++ b/tests/lib/rules/constructor-super.js @@ -128,7 +128,7 @@ ruleTester.run("constructor-super", rule, { }, { - // `B &&= 5` evaluates either to a falsy value of `B` (which, then, cannot be an Error object), or to '5' + // `B &&= 5` evaluates either to a falsy value of `B` (which, then, cannot be a constructor), or to '5' code: "class A extends (B &&= 5) { constructor() { super(); } }", errors: [{ messageId: "badSuper", type: "CallExpression" }] },