diff --git a/docs/rules/operator-assignment.md b/docs/rules/operator-assignment.md index 977edfd34c5..b926462b309 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. It does not report the logical assignment operators `&&=`, `||=`, and `??=` because their short-circuiting behavior is different from the other assignment operators. + ## Options This rule has a single string option: diff --git a/lib/rules/constructor-super.js b/lib/rules/constructor-super.js index 65ed7422c25..8787fc569a4 100644 --- a/lib/rules/constructor-super.js +++ b/lib/rules/constructor-super.js @@ -60,7 +60,23 @@ function isPossibleConstructor(node) { return node.name !== "undefined"; case "AssignmentExpression": - return isPossibleConstructor(node.right); + if (["=", "&&="].includes(node.operator)) { + return isPossibleConstructor(node.right); + } + + if (["||=", "??="].includes(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/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 8f4d863e999..6a42ce3686f 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 //------------------------------------------------------------------------------ @@ -1576,7 +1587,20 @@ module.exports = { return true; // possibly an error object. case "AssignmentExpression": - return module.exports.couldBeError(node.right); + if (["=", "&&="].includes(node.operator)) { + return module.exports.couldBeError(node.right); + } + + if (["||=", "??="].includes(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; @@ -1763,5 +1787,6 @@ module.exports = { isSpecificId, isSpecificMemberAccess, equalLiteralValue, - isSameReference + isSameReference, + isLogicalAssignmentOperator }; diff --git a/tests/lib/rules/constructor-super.js b/tests/lib/rules/constructor-super.js index e20da576b5e..bfde6a85508 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,17 @@ 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 || 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 +122,36 @@ 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" }] + }, + { + + // `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" }] + }, + { + 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. { 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 }, 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 }] }, diff --git a/tests/lib/rules/no-extend-native.js b/tests/lib/rules/no-extend-native.js index de3b0cd3f42..ca5b430fafe 100644 --- a/tests/lib/rules/no-extend-native.js +++ b/tests/lib/rules/no-extend-native.js @@ -166,6 +166,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" } }] } ] 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: [] } ]; 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" } + }] } ] }); diff --git a/tests/lib/rules/no-throw-literal.js b/tests/lib/rules/no-throw-literal.js index 7836cec7837..745044abe47 100644 --- a/tests/lib/rules/no-throw-literal.js +++ b/tests/lib/rules/no-throw-literal.js @@ -30,7 +30,9 @@ 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.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 +106,29 @@ 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" + }] + }, + { + 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" 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/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 + }] } ] }); 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 }] diff --git a/tests/lib/rules/prefer-promise-reject-errors.js b/tests/lib/rules/prefer-promise-reject-errors.js index de8f4c72524..23e299bc983 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,19 @@ 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())", + + // 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/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" + }] } ] }); diff --git a/tests/lib/rules/utils/ast-utils.js b/tests/lib/rules/utils/ast-utils.js index 4c4bcc9c8fc..a419e672177 100644 --- a/tests/lib/rules/utils/ast-utils.js +++ b/tests/lib/rules/utils/ast-utils.js @@ -1063,12 +1063,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": false, + "foo.bar ??= 2": true, + "foo[bar] ||= 2": true, "foo ? 1 : 2": false, "foo ? bar : 2": true, "foo ? 1 : bar": true, @@ -1078,7 +1093,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]); }); @@ -1688,4 +1703,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); + }); + }); + }); });