diff --git a/docs/src/rules/logical-assignment-operators.md b/docs/src/rules/logical-assignment-operators.md index 058afcd3ad7..1b59cfa70bb 100644 --- a/docs/src/rules/logical-assignment-operators.md +++ b/docs/src/rules/logical-assignment-operators.md @@ -10,7 +10,7 @@ For example `a = a || b` can be shortened to `a ||= b`. ## Rule Details -This rule requires or disallows logical assignment operator shorthand. +This rule requires or disallows logical assignment operator shorthand. ### Options @@ -27,6 +27,9 @@ Object option (only available if string option is set to `"always"`): #### always +This option checks for expressions that can be shortened using logical assignment operator. For example, `a = a || b` can be shortened to `a ||= b`. +Expressions with associativity such as `a = a || b || c` are reported as being able to be shortened to `a ||= b || c` unless the evaluation order is explicitly defined using parentheses, such as `a = (a || b) || c`. + Examples of **incorrect** code for this rule with the default `"always"` option: ::: incorrect @@ -40,6 +43,9 @@ a = a ?? b a || (a = b) a && (a = b) a ?? (a = b) +a = a || b || c +a = a && b && c +a = a ?? b ?? c ``` ::: @@ -58,6 +64,8 @@ a = b || c a || (b = c) if (a) a = b + +a = (a || b) || c ``` ::: diff --git a/lib/rules/logical-assignment-operators.js b/lib/rules/logical-assignment-operators.js index 27ca585e995..c084c04c8ed 100644 --- a/lib/rules/logical-assignment-operators.js +++ b/lib/rules/logical-assignment-operators.js @@ -150,6 +150,31 @@ function isInsideWithBlock(node) { return node.parent.type === "WithStatement" && node.parent.body === node ? true : isInsideWithBlock(node.parent); } +/** + * Gets the leftmost operand of a consecutive logical expression. + * @param {SourceCode} sourceCode The ESLint source code object + * @param {LogicalExpression} node LogicalExpression + * @returns {Expression} Leftmost operand + */ +function getLeftmostOperand(sourceCode, node) { + let left = node.left; + + while (left.type === "LogicalExpression" && left.operator === node.operator) { + + if (astUtils.isParenthesised(sourceCode, left)) { + + /* + * It should have associativity, + * but ignore it if use parentheses to make the evaluation order clear. + */ + return left; + } + left = left.left; + } + return left; + +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -318,7 +343,10 @@ module.exports = { // foo = foo || bar "AssignmentExpression[operator='='][right.type='LogicalExpression']"(assignment) { - if (!astUtils.isSameReference(assignment.left, assignment.right.left)) { + const leftOperand = getLeftmostOperand(sourceCode, assignment.right); + + if (!astUtils.isSameReference(assignment.left, leftOperand) + ) { return; } @@ -342,10 +370,10 @@ module.exports = { yield ruleFixer.insertTextBefore(assignmentOperatorToken, assignment.right.operator); // -> foo ||= bar - const logicalOperatorToken = getOperatorToken(assignment.right); + const logicalOperatorToken = getOperatorToken(leftOperand.parent); const firstRightOperandToken = sourceCode.getTokenAfter(logicalOperatorToken); - yield ruleFixer.removeRange([assignment.right.range[0], firstRightOperandToken.range[0]]); + yield ruleFixer.removeRange([leftOperand.parent.range[0], firstRightOperandToken.range[0]]); } }; diff --git a/tests/lib/rules/logical-assignment-operators.js b/tests/lib/rules/logical-assignment-operators.js index 36756815a90..471416322d2 100644 --- a/tests/lib/rules/logical-assignment-operators.js +++ b/tests/lib/rules/logical-assignment-operators.js @@ -354,6 +354,28 @@ ruleTester.run("logical-assignment-operators", rule, { }, { code: "a.b = a.b || c", options: ["never"] + }, + + // 3 or more operands + { + code: "a = a && b || c", + options: ["always"] + }, + { + code: "a = a && b && c || d", + options: ["always"] + }, + { + code: "a = (a || b) || c", // Allow if parentheses are used. + options: ["always"] + }, + { + code: "a = (a && b) && c", // Allow if parentheses are used. + options: ["always"] + }, + { + code: "a = (a ?? b) ?? c", // Allow if parentheses are used. + options: ["always"] } ], invalid: [ @@ -1511,6 +1533,151 @@ ruleTester.run("logical-assignment-operators", rule, { output: "(a.b.c ||= d) as number" }] }] + }, + + // 3 or more operands + { + code: "a = a || b || c", + output: "a ||= b || c", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = a && b && c", + output: "a &&= b && c", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "&&=" }, + suggestions: [] + }] + }, + { + code: "a = a ?? b ?? c", + output: "a ??= b ?? c", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "??=" }, + suggestions: [] + }] + }, + { + code: "a = a || b && c", + output: "a ||= b && c", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = a || b || c || d", + output: "a ||= b || c || d", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = a && b && c && d", + output: "a &&= b && c && d", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "&&=" }, + suggestions: [] + }] + }, + { + code: "a = a ?? b ?? c ?? d", + output: "a ??= b ?? c ?? d", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "??=" }, + suggestions: [] + }] + }, + { + code: "a = a || b || c && d", + output: "a ||= b || c && d", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = a || b && c || d", + output: "a ||= b && c || d", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = (a) || b || c", + output: "a ||= b || c", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = a || (b || c) || d", + output: "a ||= (b || c) || d", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = (a || b || c)", + output: "a ||= (b || c)", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = ((a) || (b || c) || d)", + output: "a ||= ((b || c) || d)", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] } ] });