| /** |
| * @fileoverview Rule to replace assignment expressions with operator assignment |
| * @author Brandon Mills |
| */ |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Requirements |
| //------------------------------------------------------------------------------ |
| |
| const astUtils = require("./utils/ast-utils"); |
| |
| //------------------------------------------------------------------------------ |
| // Helpers |
| //------------------------------------------------------------------------------ |
| |
| /** |
| * Checks whether an operator is commutative and has an operator assignment |
| * shorthand form. |
| * @param {string} operator Operator to check. |
| * @returns {boolean} True if the operator is commutative and has a |
| * shorthand form. |
| */ |
| function isCommutativeOperatorWithShorthand(operator) { |
| return ["*", "&", "^", "|"].includes(operator); |
| } |
| |
| /** |
| * Checks whether an operator is not commutative and has an operator assignment |
| * shorthand form. |
| * @param {string} operator Operator to check. |
| * @returns {boolean} True if the operator is not commutative and has |
| * a shorthand form. |
| */ |
| function isNonCommutativeOperatorWithShorthand(operator) { |
| return ["+", "-", "/", "%", "<<", ">>", ">>>", "**"].includes(operator); |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| /** |
| * Determines if the left side of a node can be safely fixed (i.e. if it activates the same getters/setters and) |
| * toString calls regardless of whether assignment shorthand is used) |
| * @param {ASTNode} node The node on the left side of the expression |
| * @returns {boolean} `true` if the node can be fixed |
| */ |
| function canBeFixed(node) { |
| return ( |
| node.type === "Identifier" || |
| (node.type === "MemberExpression" && |
| (node.object.type === "Identifier" || |
| node.object.type === "ThisExpression") && |
| (!node.computed || node.property.type === "Literal")) |
| ); |
| } |
| |
| /** @type {import('../types').Rule.RuleModule} */ |
| module.exports = { |
| meta: { |
| type: "suggestion", |
| |
| defaultOptions: ["always"], |
| |
| docs: { |
| description: |
| "Require or disallow assignment operator shorthand where possible", |
| recommended: false, |
| frozen: true, |
| url: "https://eslint.org/docs/latest/rules/operator-assignment", |
| }, |
| |
| schema: [ |
| { |
| enum: ["always", "never"], |
| }, |
| ], |
| |
| fixable: "code", |
| messages: { |
| replaced: |
| "Assignment (=) can be replaced with operator assignment ({{operator}}).", |
| unexpected: |
| "Unexpected operator assignment ({{operator}}) shorthand.", |
| }, |
| }, |
| |
| create(context) { |
| const never = context.options[0] === "never"; |
| const sourceCode = context.sourceCode; |
| |
| /** |
| * Returns the operator token of an AssignmentExpression or BinaryExpression |
| * @param {ASTNode} node An AssignmentExpression or BinaryExpression node |
| * @returns {Token} The operator token in the node |
| */ |
| function getOperatorToken(node) { |
| return sourceCode.getFirstTokenBetween( |
| node.left, |
| node.right, |
| token => token.value === node.operator, |
| ); |
| } |
| |
| /** |
| * Ensures that an assignment uses the shorthand form where possible. |
| * @param {ASTNode} node An AssignmentExpression node. |
| * @returns {void} |
| */ |
| function verify(node) { |
| if ( |
| node.operator !== "=" || |
| node.right.type !== "BinaryExpression" |
| ) { |
| return; |
| } |
| |
| const left = node.left; |
| const expr = node.right; |
| const operator = expr.operator; |
| |
| if ( |
| isCommutativeOperatorWithShorthand(operator) || |
| isNonCommutativeOperatorWithShorthand(operator) |
| ) { |
| const replacementOperator = `${operator}=`; |
| |
| if (astUtils.isSameReference(left, expr.left, true)) { |
| context.report({ |
| node, |
| messageId: "replaced", |
| data: { operator: replacementOperator }, |
| fix(fixer) { |
| if (canBeFixed(left) && canBeFixed(expr.left)) { |
| const equalsToken = getOperatorToken(node); |
| const operatorToken = getOperatorToken(expr); |
| const leftText = sourceCode |
| .getText() |
| .slice(node.range[0], equalsToken.range[0]); |
| const rightText = sourceCode |
| .getText() |
| .slice( |
| operatorToken.range[1], |
| node.right.range[1], |
| ); |
| |
| // Check for comments that would be removed. |
| if ( |
| sourceCode.commentsExistBetween( |
| equalsToken, |
| operatorToken, |
| ) |
| ) { |
| return null; |
| } |
| |
| return fixer.replaceText( |
| node, |
| `${leftText}${replacementOperator}${rightText}`, |
| ); |
| } |
| return null; |
| }, |
| }); |
| } else if ( |
| astUtils.isSameReference(left, expr.right, true) && |
| isCommutativeOperatorWithShorthand(operator) |
| ) { |
| /* |
| * This case can't be fixed safely. |
| * If `a` and `b` both have custom valueOf() behavior, then fixing `a = b * a` to `a *= b` would |
| * change the execution order of the valueOf() functions. |
| */ |
| context.report({ |
| node, |
| messageId: "replaced", |
| data: { operator: replacementOperator }, |
| }); |
| } |
| } |
| } |
| |
| /** |
| * Warns if an assignment expression uses operator assignment shorthand. |
| * @param {ASTNode} node An AssignmentExpression node. |
| * @returns {void} |
| */ |
| function prohibit(node) { |
| if ( |
| node.operator !== "=" && |
| !astUtils.isLogicalAssignmentOperator(node.operator) |
| ) { |
| context.report({ |
| node, |
| messageId: "unexpected", |
| data: { operator: node.operator }, |
| fix(fixer) { |
| if (canBeFixed(node.left)) { |
| const firstToken = sourceCode.getFirstToken(node); |
| const operatorToken = getOperatorToken(node); |
| const leftText = sourceCode |
| .getText() |
| .slice(node.range[0], operatorToken.range[0]); |
| const newOperator = node.operator.slice(0, -1); |
| let rightText; |
| |
| // Check for comments that would be duplicated. |
| if ( |
| sourceCode.commentsExistBetween( |
| firstToken, |
| operatorToken, |
| ) |
| ) { |
| return null; |
| } |
| |
| // If this change would modify precedence (e.g. `foo *= bar + 1` => `foo = foo * (bar + 1)`), parenthesize the right side. |
| if ( |
| astUtils.getPrecedence(node.right) <= |
| astUtils.getPrecedence({ |
| type: "BinaryExpression", |
| operator: newOperator, |
| }) && |
| !astUtils.isParenthesised( |
| sourceCode, |
| node.right, |
| ) |
| ) { |
| rightText = `${sourceCode.text.slice(operatorToken.range[1], node.right.range[0])}(${sourceCode.getText(node.right)})`; |
| } else { |
| const tokenAfterOperator = |
| sourceCode.getTokenAfter(operatorToken, { |
| includeComments: true, |
| }); |
| let rightTextPrefix = ""; |
| |
| if ( |
| operatorToken.range[1] === |
| tokenAfterOperator.range[0] && |
| !astUtils.canTokensBeAdjacent( |
| { |
| type: "Punctuator", |
| value: newOperator, |
| }, |
| tokenAfterOperator, |
| ) |
| ) { |
| rightTextPrefix = " "; // foo+=+bar -> foo= foo+ +bar |
| } |
| |
| rightText = `${rightTextPrefix}${sourceCode.text.slice(operatorToken.range[1], node.range[1])}`; |
| } |
| |
| return fixer.replaceText( |
| node, |
| `${leftText}= ${leftText}${newOperator}${rightText}`, |
| ); |
| } |
| return null; |
| }, |
| }); |
| } |
| } |
| |
| return { |
| AssignmentExpression: !never ? verify : prohibit, |
| }; |
| }, |
| }; |