UNPKG

7.92 kBJavaScriptView Raw
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("../util/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 commuatative and has an operator assignment
30 * shorthand form.
31 * @param {string} operator Operator to check.
32 * @returns {boolean} True if the operator is not commuatative 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 * Checks whether two expressions reference the same value. For example:
45 * a = a
46 * a.b = a.b
47 * a[0] = a[0]
48 * a['b'] = a['b']
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.
52 */
53function same(a, b) {
54 if (a.type !== b.type) {
55 return false;
56 }
57
58 switch (a.type) {
59 case "Identifier":
60 return a.name === b.name;
61
62 case "Literal":
63 return a.value === b.value;
64
65 case "MemberExpression":
66
67 /*
68 * x[0] = x[0]
69 * x[y] = x[y]
70 * x.y = x.y
71 */
72 return same(a.object, b.object) && same(a.property, b.property);
73
74 default:
75 return false;
76 }
77}
78
79/**
80 * Determines if the left side of a node can be safely fixed (i.e. if it activates the same getters/setters and)
81 * toString calls regardless of whether assignment shorthand is used)
82 * @param {ASTNode} node The node on the left side of the expression
83 * @returns {boolean} `true` if the node can be fixed
84 */
85function canBeFixed(node) {
86 return node.type === "Identifier" ||
87 node.type === "MemberExpression" && node.object.type === "Identifier" && (!node.computed || node.property.type === "Literal");
88}
89
90module.exports = {
91 meta: {
92 type: "suggestion",
93
94 docs: {
95 description: "require or disallow assignment operator shorthand where possible",
96 category: "Stylistic Issues",
97 recommended: false,
98 url: "https://eslint.org/docs/rules/operator-assignment"
99 },
100
101 schema: [
102 {
103 enum: ["always", "never"]
104 }
105 ],
106
107 fixable: "code",
108 messages: {
109 replaced: "Assignment can be replaced with operator assignment.",
110 unexpected: "Unexpected operator assignment shorthand."
111 }
112 },
113
114 create(context) {
115
116 const sourceCode = context.getSourceCode();
117
118 /**
119 * Returns the operator token of an AssignmentExpression or BinaryExpression
120 * @param {ASTNode} node An AssignmentExpression or BinaryExpression node
121 * @returns {Token} The operator token in the node
122 */
123 function getOperatorToken(node) {
124 return sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
125 }
126
127 /**
128 * Ensures that an assignment uses the shorthand form where possible.
129 * @param {ASTNode} node An AssignmentExpression node.
130 * @returns {void}
131 */
132 function verify(node) {
133 if (node.operator !== "=" || node.right.type !== "BinaryExpression") {
134 return;
135 }
136
137 const left = node.left;
138 const expr = node.right;
139 const operator = expr.operator;
140
141 if (isCommutativeOperatorWithShorthand(operator) || isNonCommutativeOperatorWithShorthand(operator)) {
142 if (same(left, expr.left)) {
143 context.report({
144 node,
145 messageId: "replaced",
146 fix(fixer) {
147 if (canBeFixed(left)) {
148 const equalsToken = getOperatorToken(node);
149 const operatorToken = getOperatorToken(expr);
150 const leftText = sourceCode.getText().slice(node.range[0], equalsToken.range[0]);
151 const rightText = sourceCode.getText().slice(operatorToken.range[1], node.right.range[1]);
152
153 return fixer.replaceText(node, `${leftText}${expr.operator}=${rightText}`);
154 }
155 return null;
156 }
157 });
158 } else if (same(left, expr.right) && isCommutativeOperatorWithShorthand(operator)) {
159
160 /*
161 * This case can't be fixed safely.
162 * If `a` and `b` both have custom valueOf() behavior, then fixing `a = b * a` to `a *= b` would
163 * change the execution order of the valueOf() functions.
164 */
165 context.report({
166 node,
167 messageId: "replaced"
168 });
169 }
170 }
171 }
172
173 /**
174 * Warns if an assignment expression uses operator assignment shorthand.
175 * @param {ASTNode} node An AssignmentExpression node.
176 * @returns {void}
177 */
178 function prohibit(node) {
179 if (node.operator !== "=") {
180 context.report({
181 node,
182 messageId: "unexpected",
183 fix(fixer) {
184 if (canBeFixed(node.left)) {
185 const operatorToken = getOperatorToken(node);
186 const leftText = sourceCode.getText().slice(node.range[0], operatorToken.range[0]);
187 const newOperator = node.operator.slice(0, -1);
188 let rightText;
189
190 // If this change would modify precedence (e.g. `foo *= bar + 1` => `foo = foo * (bar + 1)`), parenthesize the right side.
191 if (
192 astUtils.getPrecedence(node.right) <= astUtils.getPrecedence({ type: "BinaryExpression", operator: newOperator }) &&
193 !astUtils.isParenthesised(sourceCode, node.right)
194 ) {
195 rightText = `${sourceCode.text.slice(operatorToken.range[1], node.right.range[0])}(${sourceCode.getText(node.right)})`;
196 } else {
197 rightText = sourceCode.text.slice(operatorToken.range[1], node.range[1]);
198 }
199
200 return fixer.replaceText(node, `${leftText}= ${leftText}${newOperator}${rightText}`);
201 }
202 return null;
203 }
204 });
205 }
206 }
207
208 return {
209 AssignmentExpression: context.options[0] !== "never" ? verify : prohibit
210 };
211
212 }
213};