]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/no-extra-boolean-cast.js
b90757b112658c8bcd8ad869230574ae58bf12f5
[pve-eslint.git] / eslint / lib / rules / no-extra-boolean-cast.js
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-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 return isInBooleanContext(node) ||
115 (isLogicalContext(node.parent) &&
116
117 // For nested logical statements
118 isInFlaggedContext(node.parent)
119 );
120 }
121
122
123 /**
124 * Check if a node has comments inside.
125 * @param {ASTNode} node The node to check.
126 * @returns {boolean} `true` if it has comments inside.
127 */
128 function hasCommentsInside(node) {
129 return Boolean(sourceCode.getCommentsInside(node).length);
130 }
131
132 /**
133 * Checks if the given node is wrapped in grouping parentheses. Parentheses for constructs such as if() don't count.
134 * @param {ASTNode} node The node to check.
135 * @returns {boolean} `true` if the node is parenthesized.
136 * @private
137 */
138 function isParenthesized(node) {
139 return eslintUtils.isParenthesized(1, node, sourceCode);
140 }
141
142 /**
143 * Determines whether the given node needs to be parenthesized when replacing the previous node.
144 * It assumes that `previousNode` is the node to be reported by this rule, so it has a limited list
145 * of possible parent node types. By the same assumption, the node's role in a particular parent is already known.
146 * For example, if the parent is `ConditionalExpression`, `previousNode` must be its `test` child.
147 * @param {ASTNode} previousNode Previous node.
148 * @param {ASTNode} node The node to check.
149 * @returns {boolean} `true` if the node needs to be parenthesized.
150 */
151 function needsParens(previousNode, node) {
152 if (isParenthesized(previousNode)) {
153
154 // parentheses around the previous node will stay, so there is no need for an additional pair
155 return false;
156 }
157
158 // parent of the previous node will become parent of the replacement node
159 const parent = previousNode.parent;
160
161 switch (parent.type) {
162 case "CallExpression":
163 case "NewExpression":
164 return node.type === "SequenceExpression";
165 case "IfStatement":
166 case "DoWhileStatement":
167 case "WhileStatement":
168 case "ForStatement":
169 return false;
170 case "ConditionalExpression":
171 return precedence(node) <= precedence(parent);
172 case "UnaryExpression":
173 return precedence(node) < precedence(parent);
174 case "LogicalExpression":
175 if (astUtils.isMixedLogicalAndCoalesceExpressions(node, parent)) {
176 return true;
177 }
178 if (previousNode === parent.left) {
179 return precedence(node) < precedence(parent);
180 }
181 return precedence(node) <= precedence(parent);
182
183 /* istanbul ignore next */
184 default:
185 throw new Error(`Unexpected parent type: ${parent.type}`);
186 }
187 }
188
189 return {
190 UnaryExpression(node) {
191 const parent = node.parent;
192
193
194 // Exit early if it's guaranteed not to match
195 if (node.operator !== "!" ||
196 parent.type !== "UnaryExpression" ||
197 parent.operator !== "!") {
198 return;
199 }
200
201
202 if (isInFlaggedContext(parent)) {
203 context.report({
204 node: parent,
205 messageId: "unexpectedNegation",
206 fix(fixer) {
207 if (hasCommentsInside(parent)) {
208 return null;
209 }
210
211 if (needsParens(parent, node.argument)) {
212 return fixer.replaceText(parent, `(${sourceCode.getText(node.argument)})`);
213 }
214
215 let prefix = "";
216 const tokenBefore = sourceCode.getTokenBefore(parent);
217 const firstReplacementToken = sourceCode.getFirstToken(node.argument);
218
219 if (
220 tokenBefore &&
221 tokenBefore.range[1] === parent.range[0] &&
222 !astUtils.canTokensBeAdjacent(tokenBefore, firstReplacementToken)
223 ) {
224 prefix = " ";
225 }
226
227 return fixer.replaceText(parent, prefix + sourceCode.getText(node.argument));
228 }
229 });
230 }
231 },
232
233 CallExpression(node) {
234 if (node.callee.type !== "Identifier" || node.callee.name !== "Boolean") {
235 return;
236 }
237
238 if (isInFlaggedContext(node)) {
239 context.report({
240 node,
241 messageId: "unexpectedCall",
242 fix(fixer) {
243 const parent = node.parent;
244
245 if (node.arguments.length === 0) {
246 if (parent.type === "UnaryExpression" && parent.operator === "!") {
247
248 /*
249 * !Boolean() -> true
250 */
251
252 if (hasCommentsInside(parent)) {
253 return null;
254 }
255
256 const replacement = "true";
257 let prefix = "";
258 const tokenBefore = sourceCode.getTokenBefore(parent);
259
260 if (
261 tokenBefore &&
262 tokenBefore.range[1] === parent.range[0] &&
263 !astUtils.canTokensBeAdjacent(tokenBefore, replacement)
264 ) {
265 prefix = " ";
266 }
267
268 return fixer.replaceText(parent, prefix + replacement);
269 }
270
271 /*
272 * Boolean() -> false
273 */
274
275 if (hasCommentsInside(node)) {
276 return null;
277 }
278
279 return fixer.replaceText(node, "false");
280 }
281
282 if (node.arguments.length === 1) {
283 const argument = node.arguments[0];
284
285 if (argument.type === "SpreadElement" || hasCommentsInside(node)) {
286 return null;
287 }
288
289 /*
290 * Boolean(expression) -> expression
291 */
292
293 if (needsParens(node, argument)) {
294 return fixer.replaceText(node, `(${sourceCode.getText(argument)})`);
295 }
296
297 return fixer.replaceText(node, sourceCode.getText(argument));
298 }
299
300 // two or more arguments
301 return null;
302 }
303 });
304 }
305 }
306 };
307
308 }
309 };