1 | /**
|
---|
2 | * @fileoverview Rule to flag unnecessary double negation in Boolean contexts
|
---|
3 | * @author Brandon Mills
|
---|
4 | */
|
---|
5 |
|
---|
6 | "use strict";
|
---|
7 |
|
---|
8 | //------------------------------------------------------------------------------
|
---|
9 | // Requirements
|
---|
10 | //------------------------------------------------------------------------------
|
---|
11 |
|
---|
12 | const astUtils = require("./utils/ast-utils");
|
---|
13 | const eslintUtils = require("@eslint-community/eslint-utils");
|
---|
14 |
|
---|
15 | const precedence = astUtils.getPrecedence;
|
---|
16 |
|
---|
17 | //------------------------------------------------------------------------------
|
---|
18 | // Rule Definition
|
---|
19 | //------------------------------------------------------------------------------
|
---|
20 |
|
---|
21 | /** @type {import('../shared/types').Rule} */
|
---|
22 | module.exports = {
|
---|
23 | meta: {
|
---|
24 | type: "suggestion",
|
---|
25 |
|
---|
26 | docs: {
|
---|
27 | description: "Disallow unnecessary boolean casts",
|
---|
28 | recommended: true,
|
---|
29 | url: "https://eslint.org/docs/latest/rules/no-extra-boolean-cast"
|
---|
30 | },
|
---|
31 |
|
---|
32 | schema: [{
|
---|
33 | type: "object",
|
---|
34 | properties: {
|
---|
35 | enforceForLogicalOperands: {
|
---|
36 | type: "boolean",
|
---|
37 | default: false
|
---|
38 | }
|
---|
39 | },
|
---|
40 | additionalProperties: false
|
---|
41 | }],
|
---|
42 | fixable: "code",
|
---|
43 |
|
---|
44 | messages: {
|
---|
45 | unexpectedCall: "Redundant Boolean call.",
|
---|
46 | unexpectedNegation: "Redundant double negation."
|
---|
47 | }
|
---|
48 | },
|
---|
49 |
|
---|
50 | create(context) {
|
---|
51 | const sourceCode = context.sourceCode;
|
---|
52 |
|
---|
53 | // Node types which have a test which will coerce values to booleans.
|
---|
54 | const BOOLEAN_NODE_TYPES = new Set([
|
---|
55 | "IfStatement",
|
---|
56 | "DoWhileStatement",
|
---|
57 | "WhileStatement",
|
---|
58 | "ConditionalExpression",
|
---|
59 | "ForStatement"
|
---|
60 | ]);
|
---|
61 |
|
---|
62 | /**
|
---|
63 | * Check if a node is a Boolean function or constructor.
|
---|
64 | * @param {ASTNode} node the node
|
---|
65 | * @returns {boolean} If the node is Boolean function or constructor
|
---|
66 | */
|
---|
67 | function isBooleanFunctionOrConstructorCall(node) {
|
---|
68 |
|
---|
69 | // Boolean(<bool>) and new Boolean(<bool>)
|
---|
70 | return (node.type === "CallExpression" || node.type === "NewExpression") &&
|
---|
71 | node.callee.type === "Identifier" &&
|
---|
72 | node.callee.name === "Boolean";
|
---|
73 | }
|
---|
74 |
|
---|
75 | /**
|
---|
76 | * Checks whether the node is a logical expression and that the option is enabled
|
---|
77 | * @param {ASTNode} node the node
|
---|
78 | * @returns {boolean} if the node is a logical expression and option is enabled
|
---|
79 | */
|
---|
80 | function isLogicalContext(node) {
|
---|
81 | return node.type === "LogicalExpression" &&
|
---|
82 | (node.operator === "||" || node.operator === "&&") &&
|
---|
83 | (context.options.length && context.options[0].enforceForLogicalOperands === true);
|
---|
84 |
|
---|
85 | }
|
---|
86 |
|
---|
87 |
|
---|
88 | /**
|
---|
89 | * Check if a node is in a context where its value would be coerced to a boolean at runtime.
|
---|
90 | * @param {ASTNode} node The node
|
---|
91 | * @returns {boolean} If it is in a boolean context
|
---|
92 | */
|
---|
93 | function isInBooleanContext(node) {
|
---|
94 | return (
|
---|
95 | (isBooleanFunctionOrConstructorCall(node.parent) &&
|
---|
96 | node === node.parent.arguments[0]) ||
|
---|
97 |
|
---|
98 | (BOOLEAN_NODE_TYPES.has(node.parent.type) &&
|
---|
99 | node === node.parent.test) ||
|
---|
100 |
|
---|
101 | // !<bool>
|
---|
102 | (node.parent.type === "UnaryExpression" &&
|
---|
103 | node.parent.operator === "!")
|
---|
104 | );
|
---|
105 | }
|
---|
106 |
|
---|
107 | /**
|
---|
108 | * Checks whether the node is a context that should report an error
|
---|
109 | * Acts recursively if it is in a logical context
|
---|
110 | * @param {ASTNode} node the node
|
---|
111 | * @returns {boolean} If the node is in one of the flagged contexts
|
---|
112 | */
|
---|
113 | function isInFlaggedContext(node) {
|
---|
114 | if (node.parent.type === "ChainExpression") {
|
---|
115 | return isInFlaggedContext(node.parent);
|
---|
116 | }
|
---|
117 |
|
---|
118 | return isInBooleanContext(node) ||
|
---|
119 | (isLogicalContext(node.parent) &&
|
---|
120 |
|
---|
121 | // For nested logical statements
|
---|
122 | isInFlaggedContext(node.parent)
|
---|
123 | );
|
---|
124 | }
|
---|
125 |
|
---|
126 |
|
---|
127 | /**
|
---|
128 | * Check if a node has comments inside.
|
---|
129 | * @param {ASTNode} node The node to check.
|
---|
130 | * @returns {boolean} `true` if it has comments inside.
|
---|
131 | */
|
---|
132 | function hasCommentsInside(node) {
|
---|
133 | return Boolean(sourceCode.getCommentsInside(node).length);
|
---|
134 | }
|
---|
135 |
|
---|
136 | /**
|
---|
137 | * Checks if the given node is wrapped in grouping parentheses. Parentheses for constructs such as if() don't count.
|
---|
138 | * @param {ASTNode} node The node to check.
|
---|
139 | * @returns {boolean} `true` if the node is parenthesized.
|
---|
140 | * @private
|
---|
141 | */
|
---|
142 | function isParenthesized(node) {
|
---|
143 | return eslintUtils.isParenthesized(1, node, sourceCode);
|
---|
144 | }
|
---|
145 |
|
---|
146 | /**
|
---|
147 | * Determines whether the given node needs to be parenthesized when replacing the previous node.
|
---|
148 | * It assumes that `previousNode` is the node to be reported by this rule, so it has a limited list
|
---|
149 | * of possible parent node types. By the same assumption, the node's role in a particular parent is already known.
|
---|
150 | * For example, if the parent is `ConditionalExpression`, `previousNode` must be its `test` child.
|
---|
151 | * @param {ASTNode} previousNode Previous node.
|
---|
152 | * @param {ASTNode} node The node to check.
|
---|
153 | * @throws {Error} (Unreachable.)
|
---|
154 | * @returns {boolean} `true` if the node needs to be parenthesized.
|
---|
155 | */
|
---|
156 | function needsParens(previousNode, node) {
|
---|
157 | if (previousNode.parent.type === "ChainExpression") {
|
---|
158 | return needsParens(previousNode.parent, node);
|
---|
159 | }
|
---|
160 | if (isParenthesized(previousNode)) {
|
---|
161 |
|
---|
162 | // parentheses around the previous node will stay, so there is no need for an additional pair
|
---|
163 | return false;
|
---|
164 | }
|
---|
165 |
|
---|
166 | // parent of the previous node will become parent of the replacement node
|
---|
167 | const parent = previousNode.parent;
|
---|
168 |
|
---|
169 | switch (parent.type) {
|
---|
170 | case "CallExpression":
|
---|
171 | case "NewExpression":
|
---|
172 | return node.type === "SequenceExpression";
|
---|
173 | case "IfStatement":
|
---|
174 | case "DoWhileStatement":
|
---|
175 | case "WhileStatement":
|
---|
176 | case "ForStatement":
|
---|
177 | return false;
|
---|
178 | case "ConditionalExpression":
|
---|
179 | return precedence(node) <= precedence(parent);
|
---|
180 | case "UnaryExpression":
|
---|
181 | return precedence(node) < precedence(parent);
|
---|
182 | case "LogicalExpression":
|
---|
183 | if (astUtils.isMixedLogicalAndCoalesceExpressions(node, parent)) {
|
---|
184 | return true;
|
---|
185 | }
|
---|
186 | if (previousNode === parent.left) {
|
---|
187 | return precedence(node) < precedence(parent);
|
---|
188 | }
|
---|
189 | return precedence(node) <= precedence(parent);
|
---|
190 |
|
---|
191 | /* c8 ignore next */
|
---|
192 | default:
|
---|
193 | throw new Error(`Unexpected parent type: ${parent.type}`);
|
---|
194 | }
|
---|
195 | }
|
---|
196 |
|
---|
197 | return {
|
---|
198 | UnaryExpression(node) {
|
---|
199 | const parent = node.parent;
|
---|
200 |
|
---|
201 |
|
---|
202 | // Exit early if it's guaranteed not to match
|
---|
203 | if (node.operator !== "!" ||
|
---|
204 | parent.type !== "UnaryExpression" ||
|
---|
205 | parent.operator !== "!") {
|
---|
206 | return;
|
---|
207 | }
|
---|
208 |
|
---|
209 |
|
---|
210 | if (isInFlaggedContext(parent)) {
|
---|
211 | context.report({
|
---|
212 | node: parent,
|
---|
213 | messageId: "unexpectedNegation",
|
---|
214 | fix(fixer) {
|
---|
215 | if (hasCommentsInside(parent)) {
|
---|
216 | return null;
|
---|
217 | }
|
---|
218 |
|
---|
219 | if (needsParens(parent, node.argument)) {
|
---|
220 | return fixer.replaceText(parent, `(${sourceCode.getText(node.argument)})`);
|
---|
221 | }
|
---|
222 |
|
---|
223 | let prefix = "";
|
---|
224 | const tokenBefore = sourceCode.getTokenBefore(parent);
|
---|
225 | const firstReplacementToken = sourceCode.getFirstToken(node.argument);
|
---|
226 |
|
---|
227 | if (
|
---|
228 | tokenBefore &&
|
---|
229 | tokenBefore.range[1] === parent.range[0] &&
|
---|
230 | !astUtils.canTokensBeAdjacent(tokenBefore, firstReplacementToken)
|
---|
231 | ) {
|
---|
232 | prefix = " ";
|
---|
233 | }
|
---|
234 |
|
---|
235 | return fixer.replaceText(parent, prefix + sourceCode.getText(node.argument));
|
---|
236 | }
|
---|
237 | });
|
---|
238 | }
|
---|
239 | },
|
---|
240 |
|
---|
241 | CallExpression(node) {
|
---|
242 | if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") {
|
---|
243 | return;
|
---|
244 | }
|
---|
245 |
|
---|
246 | if (isInFlaggedContext(node)) {
|
---|
247 | context.report({
|
---|
248 | node,
|
---|
249 | messageId: "unexpectedCall",
|
---|
250 | fix(fixer) {
|
---|
251 | const parent = node.parent;
|
---|
252 |
|
---|
253 | if (node.arguments.length === 0) {
|
---|
254 | if (parent.type === "UnaryExpression" && parent.operator === "!") {
|
---|
255 |
|
---|
256 | /*
|
---|
257 | * !Boolean() -> true
|
---|
258 | */
|
---|
259 |
|
---|
260 | if (hasCommentsInside(parent)) {
|
---|
261 | return null;
|
---|
262 | }
|
---|
263 |
|
---|
264 | const replacement = "true";
|
---|
265 | let prefix = "";
|
---|
266 | const tokenBefore = sourceCode.getTokenBefore(parent);
|
---|
267 |
|
---|
268 | if (
|
---|
269 | tokenBefore &&
|
---|
270 | tokenBefore.range[1] === parent.range[0] &&
|
---|
271 | !astUtils.canTokensBeAdjacent(tokenBefore, replacement)
|
---|
272 | ) {
|
---|
273 | prefix = " ";
|
---|
274 | }
|
---|
275 |
|
---|
276 | return fixer.replaceText(parent, prefix + replacement);
|
---|
277 | }
|
---|
278 |
|
---|
279 | /*
|
---|
280 | * Boolean() -> false
|
---|
281 | */
|
---|
282 |
|
---|
283 | if (hasCommentsInside(node)) {
|
---|
284 | return null;
|
---|
285 | }
|
---|
286 |
|
---|
287 | return fixer.replaceText(node, "false");
|
---|
288 | }
|
---|
289 |
|
---|
290 | if (node.arguments.length === 1) {
|
---|
291 | const argument = node.arguments[0];
|
---|
292 |
|
---|
293 | if (argument.type === "SpreadElement" || hasCommentsInside(node)) {
|
---|
294 | return null;
|
---|
295 | }
|
---|
296 |
|
---|
297 | /*
|
---|
298 | * Boolean(expression) -> expression
|
---|
299 | */
|
---|
300 |
|
---|
301 | if (needsParens(node, argument)) {
|
---|
302 | return fixer.replaceText(node, `(${sourceCode.getText(argument)})`);
|
---|
303 | }
|
---|
304 |
|
---|
305 | return fixer.replaceText(node, sourceCode.getText(argument));
|
---|
306 | }
|
---|
307 |
|
---|
308 | // two or more arguments
|
---|
309 | return null;
|
---|
310 | }
|
---|
311 | });
|
---|
312 | }
|
---|
313 | }
|
---|
314 | };
|
---|
315 |
|
---|
316 | }
|
---|
317 | };
|
---|