1 | /**
|
2 | * @fileoverview Rule to flag unnecessary double negation in Boolean contexts
|
3 | * @author Brandon Mills
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const astUtils = require("./utils/ast-utils");
|
13 | const eslintUtils = require("eslint-utils");
|
14 |
|
15 | const precedence = astUtils.getPrecedence;
|
16 |
|
17 | //------------------------------------------------------------------------------
|
18 | // Rule Definition
|
19 | //------------------------------------------------------------------------------
|
20 |
|
21 | module.exports = {
|
22 | meta: {
|
23 | type: "suggestion",
|
24 |
|
25 | docs: {
|
26 | description: "disallow unnecessary boolean casts",
|
27 | category: "Possible Errors",
|
28 | recommended: true,
|
29 | url: "https://eslint.org/docs/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.getSourceCode();
|
52 |
|
53 | // Node types which have a test which will coerce values to booleans.
|
54 | const BOOLEAN_NODE_TYPES = [
|
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.indexOf(node.parent.type) !== -1 &&
|
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 | * @returns {boolean} `true` if the node needs to be parenthesized.
|
154 | */
|
155 | function needsParens(previousNode, node) {
|
156 | if (previousNode.parent.type === "ChainExpression") {
|
157 | return needsParens(previousNode.parent, node);
|
158 | }
|
159 | if (isParenthesized(previousNode)) {
|
160 |
|
161 | // parentheses around the previous node will stay, so there is no need for an additional pair
|
162 | return false;
|
163 | }
|
164 |
|
165 | // parent of the previous node will become parent of the replacement node
|
166 | const parent = previousNode.parent;
|
167 |
|
168 | switch (parent.type) {
|
169 | case "CallExpression":
|
170 | case "NewExpression":
|
171 | return node.type === "SequenceExpression";
|
172 | case "IfStatement":
|
173 | case "DoWhileStatement":
|
174 | case "WhileStatement":
|
175 | case "ForStatement":
|
176 | return false;
|
177 | case "ConditionalExpression":
|
178 | return precedence(node) <= precedence(parent);
|
179 | case "UnaryExpression":
|
180 | return precedence(node) < precedence(parent);
|
181 | case "LogicalExpression":
|
182 | if (astUtils.isMixedLogicalAndCoalesceExpressions(node, parent)) {
|
183 | return true;
|
184 | }
|
185 | if (previousNode === parent.left) {
|
186 | return precedence(node) < precedence(parent);
|
187 | }
|
188 | return precedence(node) <= precedence(parent);
|
189 |
|
190 | /* istanbul ignore next */
|
191 | default:
|
192 | throw new Error(`Unexpected parent type: ${parent.type}`);
|
193 | }
|
194 | }
|
195 |
|
196 | return {
|
197 | UnaryExpression(node) {
|
198 | const parent = node.parent;
|
199 |
|
200 |
|
201 | // Exit early if it's guaranteed not to match
|
202 | if (node.operator !== "!" ||
|
203 | parent.type !== "UnaryExpression" ||
|
204 | parent.operator !== "!") {
|
205 | return;
|
206 | }
|
207 |
|
208 |
|
209 | if (isInFlaggedContext(parent)) {
|
210 | context.report({
|
211 | node: parent,
|
212 | messageId: "unexpectedNegation",
|
213 | fix(fixer) {
|
214 | if (hasCommentsInside(parent)) {
|
215 | return null;
|
216 | }
|
217 |
|
218 | if (needsParens(parent, node.argument)) {
|
219 | return fixer.replaceText(parent, `(${sourceCode.getText(node.argument)})`);
|
220 | }
|
221 |
|
222 | let prefix = "";
|
223 | const tokenBefore = sourceCode.getTokenBefore(parent);
|
224 | const firstReplacementToken = sourceCode.getFirstToken(node.argument);
|
225 |
|
226 | if (
|
227 | tokenBefore &&
|
228 | tokenBefore.range[1] === parent.range[0] &&
|
229 | !astUtils.canTokensBeAdjacent(tokenBefore, firstReplacementToken)
|
230 | ) {
|
231 | prefix = " ";
|
232 | }
|
233 |
|
234 | return fixer.replaceText(parent, prefix + sourceCode.getText(node.argument));
|
235 | }
|
236 | });
|
237 | }
|
238 | },
|
239 |
|
240 | CallExpression(node) {
|
241 | if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") {
|
242 | return;
|
243 | }
|
244 |
|
245 | if (isInFlaggedContext(node)) {
|
246 | context.report({
|
247 | node,
|
248 | messageId: "unexpectedCall",
|
249 | fix(fixer) {
|
250 | const parent = node.parent;
|
251 |
|
252 | if (node.arguments.length === 0) {
|
253 | if (parent.type === "UnaryExpression" && parent.operator === "!") {
|
254 |
|
255 | /*
|
256 | * !Boolean() -> true
|
257 | */
|
258 |
|
259 | if (hasCommentsInside(parent)) {
|
260 | return null;
|
261 | }
|
262 |
|
263 | const replacement = "true";
|
264 | let prefix = "";
|
265 | const tokenBefore = sourceCode.getTokenBefore(parent);
|
266 |
|
267 | if (
|
268 | tokenBefore &&
|
269 | tokenBefore.range[1] === parent.range[0] &&
|
270 | !astUtils.canTokensBeAdjacent(tokenBefore, replacement)
|
271 | ) {
|
272 | prefix = " ";
|
273 | }
|
274 |
|
275 | return fixer.replaceText(parent, prefix + replacement);
|
276 | }
|
277 |
|
278 | /*
|
279 | * Boolean() -> false
|
280 | */
|
281 |
|
282 | if (hasCommentsInside(node)) {
|
283 | return null;
|
284 | }
|
285 |
|
286 | return fixer.replaceText(node, "false");
|
287 | }
|
288 |
|
289 | if (node.arguments.length === 1) {
|
290 | const argument = node.arguments[0];
|
291 |
|
292 | if (argument.type === "SpreadElement" || hasCommentsInside(node)) {
|
293 | return null;
|
294 | }
|
295 |
|
296 | /*
|
297 | * Boolean(expression) -> expression
|
298 | */
|
299 |
|
300 | if (needsParens(node, argument)) {
|
301 | return fixer.replaceText(node, `(${sourceCode.getText(argument)})`);
|
302 | }
|
303 |
|
304 | return fixer.replaceText(node, sourceCode.getText(argument));
|
305 | }
|
306 |
|
307 | // two or more arguments
|
308 | return null;
|
309 | }
|
310 | });
|
311 | }
|
312 | }
|
313 | };
|
314 |
|
315 | }
|
316 | };
|