1/** 2 * @fileoverview Rule to replace assignment expressions with operator assignment 3 * @author Brandon Mills 4 */ 5"use strict"; 6 7//------------------------------------------------------------------------------ 8// Requirements 9//------------------------------------------------------------------------------ 10 11const astUtils = require("./utils/ast-utils"); 12 13//------------------------------------------------------------------------------ 14// Helpers 15//------------------------------------------------------------------------------ 16 17/** 18 * Checks whether an operator is commutative and has an operator assignment 19 * shorthand form. 20 * @param {string} operator Operator to check. 21 * @returns {boolean} True if the operator is commutative and has a 22 * shorthand form. 23 */ 24function isCommutativeOperatorWithShorthand(operator) { 25 return ["*", "&", "^", "|"].indexOf(operator) >= 0; 26} 27 28/** 29 * Checks whether an operator is not commutative and has an operator assignment 30 * shorthand form. 31 * @param {string} operator Operator to check. 32 * @returns {boolean} True if the operator is not commutative and has 33 * a shorthand form. 34 */ 35function isNonCommutativeOperatorWithShorthand(operator) { 36 return ["+", "-", "/", "%", "<<", ">>", ">>>", "**"].indexOf(operator) >= 0; 37} 38 39//------------------------------------------------------------------------------ 40// Rule Definition 41//------------------------------------------------------------------------------ 42 43/** 44 * Determines if the left side of a node can be safely fixed (i.e. if it activates the same getters/setters and) 45 * toString calls regardless of whether assignment shorthand is used) 46 * @param {ASTNode} node The node on the left side of the expression 47 * @returns {boolean} `true` if the node can be fixed 48 */ 49function canBeFixed(node) { 50 return ( 51 node.type === "Identifier" || 52 ( 53 node.type === "MemberExpression" && 54 (node.object.type === "Identifier" || node.object.type === "ThisExpression") && 55 (!node.computed || node.property.type === "Literal") 56 ) 57 ); 58} 59 60module.exports = { 61 meta: { 62 type: "suggestion", 63 64 docs: { 65 description: "require or disallow assignment operator shorthand where possible", 66 category: "Stylistic Issues", 67 recommended: false, 68 url: "https://eslint.org/docs/rules/operator-assignment" 69 }, 70 71 schema: [ 72 { 73 enum: ["always", "never"] 74 } 75 ], 76 77 fixable: "code", 78 messages: { 79 replaced: "Assignment can be replaced with operator assignment.", 80 unexpected: "Unexpected operator assignment shorthand." 81 } 82 }, 83 84 create(context) { 85 86 const sourceCode = context.getSourceCode(); 87 88 /** 89 * Returns the operator token of an AssignmentExpression or BinaryExpression 90 * @param {ASTNode} node An AssignmentExpression or BinaryExpression node 91 * @returns {Token} The operator token in the node 92 */ 93 function getOperatorToken(node) { 94 return sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator); 95 } 96 97 /** 98 * Ensures that an assignment uses the shorthand form where possible. 99 * @param {ASTNode} node An AssignmentExpression node. 100 * @returns {void} 101 */ 102 function verify(node) { 103 if (node.operator !== "=" || node.right.type !== "BinaryExpression") { 104 return; 105 } 106 107 const left = node.left; 108 const expr = node.right; 109 const operator = expr.operator; 110 111 if (isCommutativeOperatorWithShorthand(operator) || isNonCommutativeOperatorWithShorthand(operator)) { 112 if (astUtils.isSameReference(left, expr.left, true)) { 113 context.report({ 114 node, 115 messageId: "replaced", 116 fix(fixer) { 117 if (canBeFixed(left) && canBeFixed(expr.left)) { 118 const equalsToken = getOperatorToken(node); 119 const operatorToken = getOperatorToken(expr); 120 const leftText = sourceCode.getText().slice(node.range[0], equalsToken.range[0]); 121 const rightText = sourceCode.getText().slice(operatorToken.range[1], node.right.range[1]); 122 123 // Check for comments that would be removed. 124 if (sourceCode.commentsExistBetween(equalsToken, operatorToken)) { 125 return null; 126 } 127 128 return fixer.replaceText(node, `${leftText}${expr.operator}=${rightText}`); 129 } 130 return null; 131 } 132 }); 133 } else if (astUtils.isSameReference(left, expr.right, true) && isCommutativeOperatorWithShorthand(operator)) { 134 135 /* 136 * This case can't be fixed safely. 137 * If `a` and `b` both have custom valueOf() behavior, then fixing `a = b * a` to `a *= b` would 138 * change the execution order of the valueOf() functions. 139 */ 140 context.report({ 141 node, 142 messageId: "replaced" 143 }); 144 } 145 } 146 } 147 148 /** 149 * Warns if an assignment expression uses operator assignment shorthand. 150 * @param {ASTNode} node An AssignmentExpression node. 151 * @returns {void} 152 */ 153 function prohibit(node) { 154 if (node.operator !== "=" && !astUtils.isLogicalAssignmentOperator(node.operator)) { 155 context.report({ 156 node, 157 messageId: "unexpected", 158 fix(fixer) { 159 if (canBeFixed(node.left)) { 160 const firstToken = sourceCode.getFirstToken(node); 161 const operatorToken = getOperatorToken(node); 162 const leftText = sourceCode.getText().slice(node.range[0], operatorToken.range[0]); 163 const newOperator = node.operator.slice(0, -1); 164 let rightText; 165 166 // Check for comments that would be duplicated. 167 if (sourceCode.commentsExistBetween(firstToken, operatorToken)) { 168 return null; 169 } 170 171 // If this change would modify precedence (e.g. `foo *= bar + 1` => `foo = foo * (bar + 1)`), parenthesize the right side. 172 if ( 173 astUtils.getPrecedence(node.right) <= astUtils.getPrecedence({ type: "BinaryExpression", operator: newOperator }) && 174 !astUtils.isParenthesised(sourceCode, node.right) 175 ) { 176 rightText = `${sourceCode.text.slice(operatorToken.range[1], node.right.range[0])}(${sourceCode.getText(node.right)})`; 177 } else { 178 const tokenAfterOperator = sourceCode.getTokenAfter(operatorToken, { includeComments: true }); 179 let rightTextPrefix = ""; 180 181 if ( 182 operatorToken.range[1] === tokenAfterOperator.range[0] && 183 !astUtils.canTokensBeAdjacent({ type: "Punctuator", value: newOperator }, tokenAfterOperator) 184 ) { 185 rightTextPrefix = " "; // foo+=+bar -> foo= foo+ +bar 186 } 187 188 rightText = `${rightTextPrefix}${sourceCode.text.slice(operatorToken.range[1], node.range[1])}`; 189 } 190 191 return fixer.replaceText(node, `${leftText}= ${leftText}${newOperator}${rightText}`); 192 } 193 return null; 194 } 195 }); 196 } 197 } 198 199 return { 200 AssignmentExpression: context.options[0] !== "never" ? verify : prohibit 201 }; 202 203 } 204}; 205