2 * @fileoverview Rule to replace assignment expressions with operator assignment
3 * @author Brandon Mills
7 //------------------------------------------------------------------------------
9 //------------------------------------------------------------------------------
11 const astUtils = require("./utils/ast-utils");
13 //------------------------------------------------------------------------------
15 //------------------------------------------------------------------------------
18 * Checks whether an operator is commutative and has an operator assignment
20 * @param {string} operator Operator to check.
21 * @returns {boolean} True if the operator is commutative and has a
24 function isCommutativeOperatorWithShorthand(operator) {
25 return ["*", "&", "^", "|"].indexOf(operator) >= 0;
29 * Checks whether an operator is not commuatative and has an operator assignment
31 * @param {string} operator Operator to check.
32 * @returns {boolean} True if the operator is not commuatative and has
35 function isNonCommutativeOperatorWithShorthand(operator) {
36 return ["+", "-", "/", "%", "<<", ">>", ">>>", "**"].indexOf(operator) >= 0;
39 //------------------------------------------------------------------------------
41 //------------------------------------------------------------------------------
44 * Checks whether two expressions reference the same value. For example:
49 * @param {ASTNode} a Left side of the comparison.
50 * @param {ASTNode} b Right side of the comparison.
51 * @returns {boolean} True if both sides match and reference the same value.
54 if (a.type !== b.type) {
60 return a.name === b.name;
63 return a.value === b.value;
65 case "MemberExpression":
72 return same(a.object, b.object) && same(a.property, b.property);
74 case "ThisExpression":
83 * Determines if the left side of a node can be safely fixed (i.e. if it activates the same getters/setters and)
84 * toString calls regardless of whether assignment shorthand is used)
85 * @param {ASTNode} node The node on the left side of the expression
86 * @returns {boolean} `true` if the node can be fixed
88 function canBeFixed(node) {
90 node.type === "Identifier" ||
92 node.type === "MemberExpression" &&
93 (node.object.type === "Identifier" || node.object.type === "ThisExpression") &&
94 (!node.computed || node.property.type === "Literal")
104 description: "require or disallow assignment operator shorthand where possible",
105 category: "Stylistic Issues",
107 url: "https://eslint.org/docs/rules/operator-assignment"
112 enum: ["always", "never"]
118 replaced: "Assignment can be replaced with operator assignment.",
119 unexpected: "Unexpected operator assignment shorthand."
125 const sourceCode = context.getSourceCode();
128 * Returns the operator token of an AssignmentExpression or BinaryExpression
129 * @param {ASTNode} node An AssignmentExpression or BinaryExpression node
130 * @returns {Token} The operator token in the node
132 function getOperatorToken(node) {
133 return sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
137 * Ensures that an assignment uses the shorthand form where possible.
138 * @param {ASTNode} node An AssignmentExpression node.
141 function verify(node) {
142 if (node.operator !== "=" || node.right.type !== "BinaryExpression") {
146 const left = node.left;
147 const expr = node.right;
148 const operator = expr.operator;
150 if (isCommutativeOperatorWithShorthand(operator) || isNonCommutativeOperatorWithShorthand(operator)) {
151 if (same(left, expr.left)) {
154 messageId: "replaced",
156 if (canBeFixed(left)) {
157 const equalsToken = getOperatorToken(node);
158 const operatorToken = getOperatorToken(expr);
159 const leftText = sourceCode.getText().slice(node.range[0], equalsToken.range[0]);
160 const rightText = sourceCode.getText().slice(operatorToken.range[1], node.right.range[1]);
162 // Check for comments that would be removed.
163 if (sourceCode.commentsExistBetween(equalsToken, operatorToken)) {
167 return fixer.replaceText(node, `${leftText}${expr.operator}=${rightText}`);
172 } else if (same(left, expr.right) && isCommutativeOperatorWithShorthand(operator)) {
175 * This case can't be fixed safely.
176 * If `a` and `b` both have custom valueOf() behavior, then fixing `a = b * a` to `a *= b` would
177 * change the execution order of the valueOf() functions.
181 messageId: "replaced"
188 * Warns if an assignment expression uses operator assignment shorthand.
189 * @param {ASTNode} node An AssignmentExpression node.
192 function prohibit(node) {
193 if (node.operator !== "=") {
196 messageId: "unexpected",
198 if (canBeFixed(node.left)) {
199 const firstToken = sourceCode.getFirstToken(node);
200 const operatorToken = getOperatorToken(node);
201 const leftText = sourceCode.getText().slice(node.range[0], operatorToken.range[0]);
202 const newOperator = node.operator.slice(0, -1);
205 // Check for comments that would be duplicated.
206 if (sourceCode.commentsExistBetween(firstToken, operatorToken)) {
210 // If this change would modify precedence (e.g. `foo *= bar + 1` => `foo = foo * (bar + 1)`), parenthesize the right side.
212 astUtils.getPrecedence(node.right) <= astUtils.getPrecedence({ type: "BinaryExpression", operator: newOperator }) &&
213 !astUtils.isParenthesised(sourceCode, node.right)
215 rightText = `${sourceCode.text.slice(operatorToken.range[1], node.right.range[0])}(${sourceCode.getText(node.right)})`;
217 const firstRightToken = sourceCode.getFirstToken(node.right);
218 let rightTextPrefix = "";
221 operatorToken.range[1] === firstRightToken.range[0] &&
222 !astUtils.canTokensBeAdjacent(newOperator, firstRightToken)
224 rightTextPrefix = " "; // foo+=+bar -> foo= foo+ +bar
227 rightText = `${rightTextPrefix}${sourceCode.text.slice(operatorToken.range[1], node.range[1])}`;
230 return fixer.replaceText(node, `${leftText}= ${leftText}${newOperator}${rightText}`);
239 AssignmentExpression: context.options[0] !== "never" ? verify : prohibit