Skip to content
Permalink
Browse files

Update: support logical assignments in core rules (refs #13569) (#13618)

* update operator-assignment rule

* update astUtils.couldBeError

* update constructor-super rule

* add tests for operator-linebreak rule

* add tests for space-infix-ops rule

* add tests for no-invalid-this rule

* add tests for no-param-reassign rule

* add tests for no-bitwise rule

* add tests for func-name-matching rule

* add tests for prefer-destructuring rule

* add tests for no-extend-native rule

* Update docs/rules/operator-assignment.md

Co-authored-by: Brandon Mills <btmills@users.noreply.github.com>

* Fix &&= in astUtils.couldBeError

* Fix &&= in constructor-super

* Fix comment

Co-authored-by: Brandon Mills <btmills@users.noreply.github.com>
  • Loading branch information
mdjermanovic and btmills committed Aug 31, 2020
1 parent 3729219 commit db7488e6326fd1b7ea04c5062beb1c5f75fc15ed
@@ -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:
@@ -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 (
@@ -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",
@@ -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
};
@@ -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.
{
@@ -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 },
@@ -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 }] },
@@ -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" } }]
}

]
@@ -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: []
}
];

@@ -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" }
}]
}
]
});
@@ -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"

0 comments on commit db7488e

Please sign in to comment.
You can’t perform that action at this time.