Skip to content
Permalink
Newer
Older
100644 209 lines (180 sloc) 8.58 KB
Ignoring revisions in .git-blame-ignore-revs.
September 14, 2020 10:42
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
11
const 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.
January 18, 2023 20:50
20
* @param {string} operator Operator to check.
21
* @returns {boolean} True if the operator is commutative and has a
September 14, 2020 10:42
22
* shorthand form.
23
*/
24
function isCommutativeOperatorWithShorthand(operator) {
January 18, 2023 20:50
25
return ["*", "&", "^", "|"].includes(operator);
September 14, 2020 10:42
26
}
27
28
/**
29
* Checks whether an operator is not commutative and has an operator assignment
30
* shorthand form.
January 18, 2023 20:50
31
* @param {string} operator Operator to check.
32
* @returns {boolean} True if the operator is not commutative and has
September 14, 2020 10:42
33
* a shorthand form.
34
*/
35
function isNonCommutativeOperatorWithShorthand(operator) {
January 18, 2023 20:50
36
return ["+", "-", "/", "%", "<<", ">>", ">>>", "**"].includes(operator);
September 14, 2020 10:42
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
*/
49
function 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
January 18, 2023 20:50
60
/** @type {import('../shared/types').Rule} */
September 14, 2020 10:42
61
module.exports = {
62
meta: {
63
type: "suggestion",
64
65
docs: {
January 18, 2023 20:50
66
description: "Require or disallow assignment operator shorthand where possible",
September 14, 2020 10:42
67
recommended: false,
July 13, 2023 09:09
68
url: "https://eslint.org/docs/latest/rules/operator-assignment"
September 14, 2020 10:42
69
},
70
71
schema: [
72
{
73
enum: ["always", "never"]
74
}
75
],
76
77
fixable: "code",
78
messages: {
January 18, 2023 20:50
79
replaced: "Assignment (=) can be replaced with operator assignment ({{operator}}).",
80
unexpected: "Unexpected operator assignment ({{operator}}) shorthand."
September 14, 2020 10:42
81
}
82
},
83
84
create(context) {
85
July 13, 2023 09:09
86
const sourceCode = context.sourceCode;
September 14, 2020 10:42
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.
January 18, 2023 20:50
99
* @param {ASTNode} node An AssignmentExpression node.
September 14, 2020 10:42
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)) {
January 18, 2023 20:50
112
const replacementOperator = `${operator}=`;
113
September 14, 2020 10:42
114
if (astUtils.isSameReference(left, expr.left, true)) {
115
context.report({
116
node,
117
messageId: "replaced",
January 18, 2023 20:50
118
data: { operator: replacementOperator },
September 14, 2020 10:42
119
fix(fixer) {
120
if (canBeFixed(left) && canBeFixed(expr.left)) {
121
const equalsToken = getOperatorToken(node);
122
const operatorToken = getOperatorToken(expr);
123
const leftText = sourceCode.getText().slice(node.range[0], equalsToken.range[0]);
124
const rightText = sourceCode.getText().slice(operatorToken.range[1], node.right.range[1]);
125
126
// Check for comments that would be removed.
127
if (sourceCode.commentsExistBetween(equalsToken, operatorToken)) {
128
return null;
129
}
130
January 18, 2023 20:50
131
return fixer.replaceText(node, `${leftText}${replacementOperator}${rightText}`);
September 14, 2020 10:42
132
}
133
return null;
134
}
135
});
136
} else if (astUtils.isSameReference(left, expr.right, true) && isCommutativeOperatorWithShorthand(operator)) {
137
138
/*
139
* This case can't be fixed safely.
140
* If `a` and `b` both have custom valueOf() behavior, then fixing `a = b * a` to `a *= b` would
141
* change the execution order of the valueOf() functions.
142
*/
143
context.report({
144
node,
July 27, 2021 16:54
145
messageId: "replaced",
January 18, 2023 20:50
146
data: { operator: replacementOperator }
September 14, 2020 10:42
147
});
148
}
149
}
150
}
151
152
/**
153
* Warns if an assignment expression uses operator assignment shorthand.
January 18, 2023 20:50
154
* @param {ASTNode} node An AssignmentExpression node.
September 14, 2020 10:42
155
* @returns {void}
156
*/
157
function prohibit(node) {
158
if (node.operator !== "=" && !astUtils.isLogicalAssignmentOperator(node.operator)) {
159
context.report({
160
node,
161
messageId: "unexpected",
July 27, 2021 16:54
162
data: { operator: node.operator },
September 14, 2020 10:42
163
fix(fixer) {
164
if (canBeFixed(node.left)) {
165
const firstToken = sourceCode.getFirstToken(node);
166
const operatorToken = getOperatorToken(node);
167
const leftText = sourceCode.getText().slice(node.range[0], operatorToken.range[0]);
168
const newOperator = node.operator.slice(0, -1);
169
let rightText;
170
171
// Check for comments that would be duplicated.
172
if (sourceCode.commentsExistBetween(firstToken, operatorToken)) {
173
return null;
174
}
175
176
// If this change would modify precedence (e.g. `foo *= bar + 1` => `foo = foo * (bar + 1)`), parenthesize the right side.
177
if (
178
astUtils.getPrecedence(node.right) <= astUtils.getPrecedence({ type: "BinaryExpression", operator: newOperator }) &&
179
!astUtils.isParenthesised(sourceCode, node.right)
180
) {
181
rightText = `${sourceCode.text.slice(operatorToken.range[1], node.right.range[0])}(${sourceCode.getText(node.right)})`;
182
} else {
183
const tokenAfterOperator = sourceCode.getTokenAfter(operatorToken, { includeComments: true });
184
let rightTextPrefix = "";
185
186
if (
187
operatorToken.range[1] === tokenAfterOperator.range[0] &&
188
!astUtils.canTokensBeAdjacent({ type: "Punctuator", value: newOperator }, tokenAfterOperator)
189
) {
190
rightTextPrefix = " "; // foo+=+bar -> foo= foo+ +bar
191
}
192
193
rightText = `${rightTextPrefix}${sourceCode.text.slice(operatorToken.range[1], node.range[1])}`;
194
}
195
196
return fixer.replaceText(node, `${leftText}= ${leftText}${newOperator}${rightText}`);
197
}
198
return null;
199
}
200
});
201
}
202
}
203
204
return {
205
AssignmentExpression: context.options[0] !== "never" ? verify : prohibit
206
};
207
208
}
209
};