UNPKG

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