UNPKG

7.82 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("../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 docs: {
93 description: "require or disallow assignment operator shorthand where possible",
94 category: "Stylistic Issues",
95 recommended: false,
96 url: "https://eslint.org/docs/rules/operator-assignment"
97 },
98
99 schema: [
100 {
101 enum: ["always", "never"]
102 }
103 ],
104
105 fixable: "code"
106 },
107
108 create(context) {
109
110 const sourceCode = context.getSourceCode();
111
112 /**
113 * Returns the operator token of an AssignmentExpression or BinaryExpression
114 * @param {ASTNode} node An AssignmentExpression or BinaryExpression node
115 * @returns {Token} The operator token in the node
116 */
117 function getOperatorToken(node) {
118 return sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
119 }
120
121 /**
122 * Ensures that an assignment uses the shorthand form where possible.
123 * @param {ASTNode} node An AssignmentExpression node.
124 * @returns {void}
125 */
126 function verify(node) {
127 if (node.operator !== "=" || node.right.type !== "BinaryExpression") {
128 return;
129 }
130
131 const left = node.left;
132 const expr = node.right;
133 const operator = expr.operator;
134
135 if (isCommutativeOperatorWithShorthand(operator) || isNonCommutativeOperatorWithShorthand(operator)) {
136 if (same(left, expr.left)) {
137 context.report({
138 node,
139 message: "Assignment can be replaced with operator assignment.",
140 fix(fixer) {
141 if (canBeFixed(left)) {
142 const equalsToken = getOperatorToken(node);
143 const operatorToken = getOperatorToken(expr);
144 const leftText = sourceCode.getText().slice(node.range[0], equalsToken.range[0]);
145 const rightText = sourceCode.getText().slice(operatorToken.range[1], node.right.range[1]);
146
147 return fixer.replaceText(node, `${leftText}${expr.operator}=${rightText}`);
148 }
149 return null;
150 }
151 });
152 } else if (same(left, expr.right) && isCommutativeOperatorWithShorthand(operator)) {
153
154 /*
155 * This case can't be fixed safely.
156 * If `a` and `b` both have custom valueOf() behavior, then fixing `a = b * a` to `a *= b` would
157 * change the execution order of the valueOf() functions.
158 */
159 context.report({
160 node,
161 message: "Assignment can be replaced with operator assignment."
162 });
163 }
164 }
165 }
166
167 /**
168 * Warns if an assignment expression uses operator assignment shorthand.
169 * @param {ASTNode} node An AssignmentExpression node.
170 * @returns {void}
171 */
172 function prohibit(node) {
173 if (node.operator !== "=") {
174 context.report({
175 node,
176 message: "Unexpected operator assignment shorthand.",
177 fix(fixer) {
178 if (canBeFixed(node.left)) {
179 const operatorToken = getOperatorToken(node);
180 const leftText = sourceCode.getText().slice(node.range[0], operatorToken.range[0]);
181 const newOperator = node.operator.slice(0, -1);
182 let rightText;
183
184 // If this change would modify precedence (e.g. `foo *= bar + 1` => `foo = foo * (bar + 1)`), parenthesize the right side.
185 if (
186 astUtils.getPrecedence(node.right) <= astUtils.getPrecedence({ type: "BinaryExpression", operator: newOperator }) &&
187 !astUtils.isParenthesised(sourceCode, node.right)
188 ) {
189 rightText = `${sourceCode.text.slice(operatorToken.range[1], node.right.range[0])}(${sourceCode.getText(node.right)})`;
190 } else {
191 rightText = sourceCode.text.slice(operatorToken.range[1], node.range[1]);
192 }
193
194 return fixer.replaceText(node, `${leftText}= ${leftText}${newOperator}${rightText}`);
195 }
196 return null;
197 }
198 });
199 }
200 }
201
202 return {
203 AssignmentExpression: context.options[0] !== "never" ? verify : prohibit
204 };
205
206 }
207};