1 | /**
|
2 | * @fileoverview Disallow parenthesising higher precedence subexpressions.
|
3 | * @author Michael Ficarra
|
4 | */
|
5 | ;
|
6 |
|
7 | //------------------------------------------------------------------------------
|
8 | // Rule Definition
|
9 | //------------------------------------------------------------------------------
|
10 |
|
11 | const astUtils = require("./utils/ast-utils.js");
|
12 |
|
13 | module.exports = {
|
14 | meta: {
|
15 | type: "layout",
|
16 |
|
17 | docs: {
|
18 | description: "disallow unnecessary parentheses",
|
19 | category: "Possible Errors",
|
20 | recommended: false,
|
21 | url: "https://eslint.org/docs/rules/no-extra-parens"
|
22 | },
|
23 |
|
24 | fixable: "code",
|
25 |
|
26 | schema: {
|
27 | anyOf: [
|
28 | {
|
29 | type: "array",
|
30 | items: [
|
31 | {
|
32 | enum: ["functions"]
|
33 | }
|
34 | ],
|
35 | minItems: 0,
|
36 | maxItems: 1
|
37 | },
|
38 | {
|
39 | type: "array",
|
40 | items: [
|
41 | {
|
42 | enum: ["all"]
|
43 | },
|
44 | {
|
45 | type: "object",
|
46 | properties: {
|
47 | conditionalAssign: { type: "boolean" },
|
48 | nestedBinaryExpressions: { type: "boolean" },
|
49 | returnAssign: { type: "boolean" },
|
50 | ignoreJSX: { enum: ["none", "all", "single-line", "multi-line"] },
|
51 | enforceForArrowConditionals: { type: "boolean" }
|
52 | },
|
53 | additionalProperties: false
|
54 | }
|
55 | ],
|
56 | minItems: 0,
|
57 | maxItems: 2
|
58 | }
|
59 | ]
|
60 | },
|
61 |
|
62 | messages: {
|
63 | unexpected: "Unnecessary parentheses around expression."
|
64 | }
|
65 | },
|
66 |
|
67 | create(context) {
|
68 | const sourceCode = context.getSourceCode();
|
69 |
|
70 | const tokensToIgnore = new WeakSet();
|
71 | const isParenthesised = astUtils.isParenthesised.bind(astUtils, sourceCode);
|
72 | const precedence = astUtils.getPrecedence;
|
73 | const ALL_NODES = context.options[0] !== "functions";
|
74 | const EXCEPT_COND_ASSIGN = ALL_NODES && context.options[1] && context.options[1].conditionalAssign === false;
|
75 | const NESTED_BINARY = ALL_NODES && context.options[1] && context.options[1].nestedBinaryExpressions === false;
|
76 | const EXCEPT_RETURN_ASSIGN = ALL_NODES && context.options[1] && context.options[1].returnAssign === false;
|
77 | const IGNORE_JSX = ALL_NODES && context.options[1] && context.options[1].ignoreJSX;
|
78 | const IGNORE_ARROW_CONDITIONALS = ALL_NODES && context.options[1] &&
|
79 | context.options[1].enforceForArrowConditionals === false;
|
80 |
|
81 | const PRECEDENCE_OF_ASSIGNMENT_EXPR = precedence({ type: "AssignmentExpression" });
|
82 | const PRECEDENCE_OF_UPDATE_EXPR = precedence({ type: "UpdateExpression" });
|
83 |
|
84 | let reportsBuffer;
|
85 |
|
86 | /**
|
87 | * Determines if this rule should be enforced for a node given the current configuration.
|
88 | * @param {ASTNode} node - The node to be checked.
|
89 | * @returns {boolean} True if the rule should be enforced for this node.
|
90 | * @private
|
91 | */
|
92 | function ruleApplies(node) {
|
93 | if (node.type === "JSXElement" || node.type === "JSXFragment") {
|
94 | const isSingleLine = node.loc.start.line === node.loc.end.line;
|
95 |
|
96 | switch (IGNORE_JSX) {
|
97 |
|
98 | // Exclude this JSX element from linting
|
99 | case "all":
|
100 | return false;
|
101 |
|
102 | // Exclude this JSX element if it is multi-line element
|
103 | case "multi-line":
|
104 | return isSingleLine;
|
105 |
|
106 | // Exclude this JSX element if it is single-line element
|
107 | case "single-line":
|
108 | return !isSingleLine;
|
109 |
|
110 | // Nothing special to be done for JSX elements
|
111 | case "none":
|
112 | break;
|
113 |
|
114 | // no default
|
115 | }
|
116 | }
|
117 |
|
118 | return ALL_NODES || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
|
119 | }
|
120 |
|
121 | /**
|
122 | * Determines if a node is surrounded by parentheses twice.
|
123 | * @param {ASTNode} node - The node to be checked.
|
124 | * @returns {boolean} True if the node is doubly parenthesised.
|
125 | * @private
|
126 | */
|
127 | function isParenthesisedTwice(node) {
|
128 | const previousToken = sourceCode.getTokenBefore(node, 1),
|
129 | nextToken = sourceCode.getTokenAfter(node, 1);
|
130 |
|
131 | return isParenthesised(node) && previousToken && nextToken &&
|
132 | astUtils.isOpeningParenToken(previousToken) && previousToken.range[1] <= node.range[0] &&
|
133 | astUtils.isClosingParenToken(nextToken) && nextToken.range[0] >= node.range[1];
|
134 | }
|
135 |
|
136 | /**
|
137 | * Determines if a node is surrounded by (potentially) invalid parentheses.
|
138 | * @param {ASTNode} node - The node to be checked.
|
139 | * @returns {boolean} True if the node is incorrectly parenthesised.
|
140 | * @private
|
141 | */
|
142 | function hasExcessParens(node) {
|
143 | return ruleApplies(node) && isParenthesised(node);
|
144 | }
|
145 |
|
146 | /**
|
147 | * Determines if a node that is expected to be parenthesised is surrounded by
|
148 | * (potentially) invalid extra parentheses.
|
149 | * @param {ASTNode} node - The node to be checked.
|
150 | * @returns {boolean} True if the node is has an unexpected extra pair of parentheses.
|
151 | * @private
|
152 | */
|
153 | function hasDoubleExcessParens(node) {
|
154 | return ruleApplies(node) && isParenthesisedTwice(node);
|
155 | }
|
156 |
|
157 | /**
|
158 | * Determines if a node test expression is allowed to have a parenthesised assignment
|
159 | * @param {ASTNode} node - The node to be checked.
|
160 | * @returns {boolean} True if the assignment can be parenthesised.
|
161 | * @private
|
162 | */
|
163 | function isCondAssignException(node) {
|
164 | return EXCEPT_COND_ASSIGN && node.test.type === "AssignmentExpression";
|
165 | }
|
166 |
|
167 | /**
|
168 | * Determines if a node is in a return statement
|
169 | * @param {ASTNode} node - The node to be checked.
|
170 | * @returns {boolean} True if the node is in a return statement.
|
171 | * @private
|
172 | */
|
173 | function isInReturnStatement(node) {
|
174 | for (let currentNode = node; currentNode; currentNode = currentNode.parent) {
|
175 | if (
|
176 | currentNode.type === "ReturnStatement" ||
|
177 | (currentNode.type === "ArrowFunctionExpression" && currentNode.body.type !== "BlockStatement")
|
178 | ) {
|
179 | return true;
|
180 | }
|
181 | }
|
182 |
|
183 | return false;
|
184 | }
|
185 |
|
186 | /**
|
187 | * Determines if a constructor function is newed-up with parens
|
188 | * @param {ASTNode} newExpression - The NewExpression node to be checked.
|
189 | * @returns {boolean} True if the constructor is called with parens.
|
190 | * @private
|
191 | */
|
192 | function isNewExpressionWithParens(newExpression) {
|
193 | const lastToken = sourceCode.getLastToken(newExpression);
|
194 | const penultimateToken = sourceCode.getTokenBefore(lastToken);
|
195 |
|
196 | return newExpression.arguments.length > 0 || astUtils.isOpeningParenToken(penultimateToken) && astUtils.isClosingParenToken(lastToken);
|
197 | }
|
198 |
|
199 | /**
|
200 | * Determines if a node is or contains an assignment expression
|
201 | * @param {ASTNode} node - The node to be checked.
|
202 | * @returns {boolean} True if the node is or contains an assignment expression.
|
203 | * @private
|
204 | */
|
205 | function containsAssignment(node) {
|
206 | if (node.type === "AssignmentExpression") {
|
207 | return true;
|
208 | }
|
209 | if (node.type === "ConditionalExpression" &&
|
210 | (node.consequent.type === "AssignmentExpression" || node.alternate.type === "AssignmentExpression")) {
|
211 | return true;
|
212 | }
|
213 | if ((node.left && node.left.type === "AssignmentExpression") ||
|
214 | (node.right && node.right.type === "AssignmentExpression")) {
|
215 | return true;
|
216 | }
|
217 |
|
218 | return false;
|
219 | }
|
220 |
|
221 | /**
|
222 | * Determines if a node is contained by or is itself a return statement and is allowed to have a parenthesised assignment
|
223 | * @param {ASTNode} node - The node to be checked.
|
224 | * @returns {boolean} True if the assignment can be parenthesised.
|
225 | * @private
|
226 | */
|
227 | function isReturnAssignException(node) {
|
228 | if (!EXCEPT_RETURN_ASSIGN || !isInReturnStatement(node)) {
|
229 | return false;
|
230 | }
|
231 |
|
232 | if (node.type === "ReturnStatement") {
|
233 | return node.argument && containsAssignment(node.argument);
|
234 | }
|
235 | if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") {
|
236 | return containsAssignment(node.body);
|
237 | }
|
238 | return containsAssignment(node);
|
239 |
|
240 | }
|
241 |
|
242 | /**
|
243 | * Determines if a node following a [no LineTerminator here] restriction is
|
244 | * surrounded by (potentially) invalid extra parentheses.
|
245 | * @param {Token} token - The token preceding the [no LineTerminator here] restriction.
|
246 | * @param {ASTNode} node - The node to be checked.
|
247 | * @returns {boolean} True if the node is incorrectly parenthesised.
|
248 | * @private
|
249 | */
|
250 | function hasExcessParensNoLineTerminator(token, node) {
|
251 | if (token.loc.end.line === node.loc.start.line) {
|
252 | return hasExcessParens(node);
|
253 | }
|
254 |
|
255 | return hasDoubleExcessParens(node);
|
256 | }
|
257 |
|
258 | /**
|
259 | * Determines whether a node should be preceded by an additional space when removing parens
|
260 | * @param {ASTNode} node node to evaluate; must be surrounded by parentheses
|
261 | * @returns {boolean} `true` if a space should be inserted before the node
|
262 | * @private
|
263 | */
|
264 | function requiresLeadingSpace(node) {
|
265 | const leftParenToken = sourceCode.getTokenBefore(node);
|
266 | const tokenBeforeLeftParen = sourceCode.getTokenBefore(node, 1);
|
267 | const firstToken = sourceCode.getFirstToken(node);
|
268 |
|
269 | return tokenBeforeLeftParen &&
|
270 | tokenBeforeLeftParen.range[1] === leftParenToken.range[0] &&
|
271 | leftParenToken.range[1] === firstToken.range[0] &&
|
272 | !astUtils.canTokensBeAdjacent(tokenBeforeLeftParen, firstToken);
|
273 | }
|
274 |
|
275 | /**
|
276 | * Determines whether a node should be followed by an additional space when removing parens
|
277 | * @param {ASTNode} node node to evaluate; must be surrounded by parentheses
|
278 | * @returns {boolean} `true` if a space should be inserted after the node
|
279 | * @private
|
280 | */
|
281 | function requiresTrailingSpace(node) {
|
282 | const nextTwoTokens = sourceCode.getTokensAfter(node, { count: 2 });
|
283 | const rightParenToken = nextTwoTokens[0];
|
284 | const tokenAfterRightParen = nextTwoTokens[1];
|
285 | const tokenBeforeRightParen = sourceCode.getLastToken(node);
|
286 |
|
287 | return rightParenToken && tokenAfterRightParen &&
|
288 | !sourceCode.isSpaceBetweenTokens(rightParenToken, tokenAfterRightParen) &&
|
289 | !astUtils.canTokensBeAdjacent(tokenBeforeRightParen, tokenAfterRightParen);
|
290 | }
|
291 |
|
292 | /**
|
293 | * Determines if a given expression node is an IIFE
|
294 | * @param {ASTNode} node The node to check
|
295 | * @returns {boolean} `true` if the given node is an IIFE
|
296 | */
|
297 | function isIIFE(node) {
|
298 | return node.type === "CallExpression" && node.callee.type === "FunctionExpression";
|
299 | }
|
300 |
|
301 | /**
|
302 | * Report the node
|
303 | * @param {ASTNode} node node to evaluate
|
304 | * @returns {void}
|
305 | * @private
|
306 | */
|
307 | function report(node) {
|
308 | const leftParenToken = sourceCode.getTokenBefore(node);
|
309 | const rightParenToken = sourceCode.getTokenAfter(node);
|
310 |
|
311 | if (!isParenthesisedTwice(node)) {
|
312 | if (tokensToIgnore.has(sourceCode.getFirstToken(node))) {
|
313 | return;
|
314 | }
|
315 |
|
316 | if (isIIFE(node) && !isParenthesised(node.callee)) {
|
317 | return;
|
318 | }
|
319 | }
|
320 |
|
321 | /**
|
322 | * Finishes reporting
|
323 | * @returns {void}
|
324 | * @private
|
325 | */
|
326 | function finishReport() {
|
327 | context.report({
|
328 | node,
|
329 | loc: leftParenToken.loc.start,
|
330 | messageId: "unexpected",
|
331 | fix(fixer) {
|
332 | const parenthesizedSource = sourceCode.text.slice(leftParenToken.range[1], rightParenToken.range[0]);
|
333 |
|
334 | return fixer.replaceTextRange([
|
335 | leftParenToken.range[0],
|
336 | rightParenToken.range[1]
|
337 | ], (requiresLeadingSpace(node) ? " " : "") + parenthesizedSource + (requiresTrailingSpace(node) ? " " : ""));
|
338 | }
|
339 | });
|
340 | }
|
341 |
|
342 | if (reportsBuffer) {
|
343 | reportsBuffer.reports.push({ node, finishReport });
|
344 | return;
|
345 | }
|
346 |
|
347 | finishReport();
|
348 | }
|
349 |
|
350 | /**
|
351 | * Evaluate Unary update
|
352 | * @param {ASTNode} node node to evaluate
|
353 | * @returns {void}
|
354 | * @private
|
355 | */
|
356 | function checkUnaryUpdate(node) {
|
357 | if (node.type === "UnaryExpression" && node.argument.type === "BinaryExpression" && node.argument.operator === "**") {
|
358 | return;
|
359 | }
|
360 |
|
361 | if (hasExcessParens(node.argument) && precedence(node.argument) >= precedence(node)) {
|
362 | report(node.argument);
|
363 | }
|
364 | }
|
365 |
|
366 | /**
|
367 | * Check if a member expression contains a call expression
|
368 | * @param {ASTNode} node MemberExpression node to evaluate
|
369 | * @returns {boolean} true if found, false if not
|
370 | */
|
371 | function doesMemberExpressionContainCallExpression(node) {
|
372 | let currentNode = node.object;
|
373 | let currentNodeType = node.object.type;
|
374 |
|
375 | while (currentNodeType === "MemberExpression") {
|
376 | currentNode = currentNode.object;
|
377 | currentNodeType = currentNode.type;
|
378 | }
|
379 |
|
380 | return currentNodeType === "CallExpression";
|
381 | }
|
382 |
|
383 | /**
|
384 | * Evaluate a new call
|
385 | * @param {ASTNode} node node to evaluate
|
386 | * @returns {void}
|
387 | * @private
|
388 | */
|
389 | function checkCallNew(node) {
|
390 | const callee = node.callee;
|
391 |
|
392 | if (hasExcessParens(callee) && precedence(callee) >= precedence(node)) {
|
393 | const hasNewParensException = callee.type === "NewExpression" && !isNewExpressionWithParens(callee);
|
394 |
|
395 | if (
|
396 | hasDoubleExcessParens(callee) ||
|
397 | !isIIFE(node) && !hasNewParensException && !(
|
398 |
|
399 | /*
|
400 | * Allow extra parens around a new expression if
|
401 | * there are intervening parentheses.
|
402 | */
|
403 | (callee.type === "MemberExpression" && doesMemberExpressionContainCallExpression(callee))
|
404 | )
|
405 | ) {
|
406 | report(node.callee);
|
407 | }
|
408 | }
|
409 | if (node.arguments.length === 1) {
|
410 | if (hasDoubleExcessParens(node.arguments[0]) && precedence(node.arguments[0]) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
|
411 | report(node.arguments[0]);
|
412 | }
|
413 | } else {
|
414 | node.arguments
|
415 | .filter(arg => hasExcessParens(arg) && precedence(arg) >= PRECEDENCE_OF_ASSIGNMENT_EXPR)
|
416 | .forEach(report);
|
417 | }
|
418 | }
|
419 |
|
420 | /**
|
421 | * Evaluate binary logicals
|
422 | * @param {ASTNode} node node to evaluate
|
423 | * @returns {void}
|
424 | * @private
|
425 | */
|
426 | function checkBinaryLogical(node) {
|
427 | const prec = precedence(node);
|
428 | const leftPrecedence = precedence(node.left);
|
429 | const rightPrecedence = precedence(node.right);
|
430 | const isExponentiation = node.operator === "**";
|
431 | const shouldSkipLeft = (NESTED_BINARY && (node.left.type === "BinaryExpression" || node.left.type === "LogicalExpression")) ||
|
432 | node.left.type === "UnaryExpression" && isExponentiation;
|
433 | const shouldSkipRight = NESTED_BINARY && (node.right.type === "BinaryExpression" || node.right.type === "LogicalExpression");
|
434 |
|
435 | if (!shouldSkipLeft && hasExcessParens(node.left) && (leftPrecedence > prec || (leftPrecedence === prec && !isExponentiation))) {
|
436 | report(node.left);
|
437 | }
|
438 | if (!shouldSkipRight && hasExcessParens(node.right) && (rightPrecedence > prec || (rightPrecedence === prec && isExponentiation))) {
|
439 | report(node.right);
|
440 | }
|
441 | }
|
442 |
|
443 | /**
|
444 | * Check the parentheses around the super class of the given class definition.
|
445 | * @param {ASTNode} node The node of class declarations to check.
|
446 | * @returns {void}
|
447 | */
|
448 | function checkClass(node) {
|
449 | if (!node.superClass) {
|
450 | return;
|
451 | }
|
452 |
|
453 | /*
|
454 | * If `node.superClass` is a LeftHandSideExpression, parentheses are extra.
|
455 | * Otherwise, parentheses are needed.
|
456 | */
|
457 | const hasExtraParens = precedence(node.superClass) > PRECEDENCE_OF_UPDATE_EXPR
|
458 | ? hasExcessParens(node.superClass)
|
459 | : hasDoubleExcessParens(node.superClass);
|
460 |
|
461 | if (hasExtraParens) {
|
462 | report(node.superClass);
|
463 | }
|
464 | }
|
465 |
|
466 | /**
|
467 | * Check the parentheses around the argument of the given spread operator.
|
468 | * @param {ASTNode} node The node of spread elements/properties to check.
|
469 | * @returns {void}
|
470 | */
|
471 | function checkSpreadOperator(node) {
|
472 | const hasExtraParens = precedence(node.argument) >= PRECEDENCE_OF_ASSIGNMENT_EXPR
|
473 | ? hasExcessParens(node.argument)
|
474 | : hasDoubleExcessParens(node.argument);
|
475 |
|
476 | if (hasExtraParens) {
|
477 | report(node.argument);
|
478 | }
|
479 | }
|
480 |
|
481 | /**
|
482 | * Checks the parentheses for an ExpressionStatement or ExportDefaultDeclaration
|
483 | * @param {ASTNode} node The ExpressionStatement.expression or ExportDefaultDeclaration.declaration node
|
484 | * @returns {void}
|
485 | */
|
486 | function checkExpressionOrExportStatement(node) {
|
487 | const firstToken = isParenthesised(node) ? sourceCode.getTokenBefore(node) : sourceCode.getFirstToken(node);
|
488 | const secondToken = sourceCode.getTokenAfter(firstToken, astUtils.isNotOpeningParenToken);
|
489 | const thirdToken = secondToken ? sourceCode.getTokenAfter(secondToken) : null;
|
490 | const tokenAfterClosingParens = secondToken ? sourceCode.getTokenAfter(secondToken, astUtils.isNotClosingParenToken) : null;
|
491 |
|
492 | if (
|
493 | astUtils.isOpeningParenToken(firstToken) &&
|
494 | (
|
495 | astUtils.isOpeningBraceToken(secondToken) ||
|
496 | secondToken.type === "Keyword" && (
|
497 | secondToken.value === "function" ||
|
498 | secondToken.value === "class" ||
|
499 | secondToken.value === "let" &&
|
500 | tokenAfterClosingParens &&
|
501 | (
|
502 | astUtils.isOpeningBracketToken(tokenAfterClosingParens) ||
|
503 | tokenAfterClosingParens.type === "Identifier"
|
504 | )
|
505 | ) ||
|
506 | secondToken && secondToken.type === "Identifier" && secondToken.value === "async" && thirdToken && thirdToken.type === "Keyword" && thirdToken.value === "function"
|
507 | )
|
508 | ) {
|
509 | tokensToIgnore.add(secondToken);
|
510 | }
|
511 |
|
512 | if (hasExcessParens(node)) {
|
513 | report(node);
|
514 | }
|
515 | }
|
516 |
|
517 | /**
|
518 | * Finds the path from the given node to the specified ancestor.
|
519 | * @param {ASTNode} node First node in the path.
|
520 | * @param {ASTNode} ancestor Last node in the path.
|
521 | * @returns {ASTNode[]} Path, including both nodes.
|
522 | * @throws {Error} If the given node does not have the specified ancestor.
|
523 | */
|
524 | function pathToAncestor(node, ancestor) {
|
525 | const path = [node];
|
526 | let currentNode = node;
|
527 |
|
528 | while (currentNode !== ancestor) {
|
529 |
|
530 | currentNode = currentNode.parent;
|
531 |
|
532 | /* istanbul ignore if */
|
533 | if (currentNode === null) {
|
534 | throw new Error("Nodes are not in the ancestor-descendant relationship.");
|
535 | }
|
536 |
|
537 | path.push(currentNode);
|
538 | }
|
539 |
|
540 | return path;
|
541 | }
|
542 |
|
543 | /**
|
544 | * Finds the path from the given node to the specified descendant.
|
545 | * @param {ASTNode} node First node in the path.
|
546 | * @param {ASTNode} descendant Last node in the path.
|
547 | * @returns {ASTNode[]} Path, including both nodes.
|
548 | * @throws {Error} If the given node does not have the specified descendant.
|
549 | */
|
550 | function pathToDescendant(node, descendant) {
|
551 | return pathToAncestor(descendant, node).reverse();
|
552 | }
|
553 |
|
554 | /**
|
555 | * Checks whether the syntax of the given ancestor of an 'in' expression inside a for-loop initializer
|
556 | * is preventing the 'in' keyword from being interpreted as a part of an ill-formed for-in loop.
|
557 | *
|
558 | * @param {ASTNode} node Ancestor of an 'in' expression.
|
559 | * @param {ASTNode} child Child of the node, ancestor of the same 'in' expression or the 'in' expression itself.
|
560 | * @returns {boolean} True if the keyword 'in' would be interpreted as the 'in' operator, without any parenthesis.
|
561 | */
|
562 | function isSafelyEnclosingInExpression(node, child) {
|
563 | switch (node.type) {
|
564 | case "ArrayExpression":
|
565 | case "ArrayPattern":
|
566 | case "BlockStatement":
|
567 | case "ObjectExpression":
|
568 | case "ObjectPattern":
|
569 | case "TemplateLiteral":
|
570 | return true;
|
571 | case "ArrowFunctionExpression":
|
572 | case "FunctionExpression":
|
573 | return node.params.includes(child);
|
574 | case "CallExpression":
|
575 | case "NewExpression":
|
576 | return node.arguments.includes(child);
|
577 | case "MemberExpression":
|
578 | return node.computed && node.property === child;
|
579 | case "ConditionalExpression":
|
580 | return node.consequent === child;
|
581 | default:
|
582 | return false;
|
583 | }
|
584 | }
|
585 |
|
586 | /**
|
587 | * Starts a new reports buffering. Warnings will be stored in a buffer instead of being reported immediately.
|
588 | * An additional logic that requires multiple nodes (e.g. a whole subtree) may dismiss some of the stored warnings.
|
589 | *
|
590 | * @returns {void}
|
591 | */
|
592 | function startNewReportsBuffering() {
|
593 | reportsBuffer = {
|
594 | upper: reportsBuffer,
|
595 | inExpressionNodes: [],
|
596 | reports: []
|
597 | };
|
598 | }
|
599 |
|
600 | /**
|
601 | * Ends the current reports buffering.
|
602 | * @returns {void}
|
603 | */
|
604 | function endCurrentReportsBuffering() {
|
605 | const { upper, inExpressionNodes, reports } = reportsBuffer;
|
606 |
|
607 | if (upper) {
|
608 | upper.inExpressionNodes.push(...inExpressionNodes);
|
609 | upper.reports.push(...reports);
|
610 | } else {
|
611 |
|
612 | // flush remaining reports
|
613 | reports.forEach(({ finishReport }) => finishReport());
|
614 | }
|
615 |
|
616 | reportsBuffer = upper;
|
617 | }
|
618 |
|
619 | /**
|
620 | * Checks whether the given node is in the current reports buffer.
|
621 | * @param {ASTNode} node Node to check.
|
622 | * @returns {boolean} True if the node is in the current buffer, false otherwise.
|
623 | */
|
624 | function isInCurrentReportsBuffer(node) {
|
625 | return reportsBuffer.reports.some(r => r.node === node);
|
626 | }
|
627 |
|
628 | /**
|
629 | * Removes the given node from the current reports buffer.
|
630 | * @param {ASTNode} node Node to remove.
|
631 | * @returns {void}
|
632 | */
|
633 | function removeFromCurrentReportsBuffer(node) {
|
634 | reportsBuffer.reports = reportsBuffer.reports.filter(r => r.node !== node);
|
635 | }
|
636 |
|
637 | return {
|
638 | ArrayExpression(node) {
|
639 | node.elements
|
640 | .filter(e => e && hasExcessParens(e) && precedence(e) >= PRECEDENCE_OF_ASSIGNMENT_EXPR)
|
641 | .forEach(report);
|
642 | },
|
643 |
|
644 | ArrowFunctionExpression(node) {
|
645 | if (isReturnAssignException(node)) {
|
646 | return;
|
647 | }
|
648 |
|
649 | if (node.body.type === "ConditionalExpression" &&
|
650 | IGNORE_ARROW_CONDITIONALS &&
|
651 | !isParenthesisedTwice(node.body)
|
652 | ) {
|
653 | return;
|
654 | }
|
655 |
|
656 | if (node.body.type !== "BlockStatement") {
|
657 | const firstBodyToken = sourceCode.getFirstToken(node.body, astUtils.isNotOpeningParenToken);
|
658 | const tokenBeforeFirst = sourceCode.getTokenBefore(firstBodyToken);
|
659 |
|
660 | if (astUtils.isOpeningParenToken(tokenBeforeFirst) && astUtils.isOpeningBraceToken(firstBodyToken)) {
|
661 | tokensToIgnore.add(firstBodyToken);
|
662 | }
|
663 | if (hasExcessParens(node.body) && precedence(node.body) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
|
664 | report(node.body);
|
665 | }
|
666 | }
|
667 | },
|
668 |
|
669 | AssignmentExpression(node) {
|
670 | if (isReturnAssignException(node)) {
|
671 | return;
|
672 | }
|
673 |
|
674 | if (hasExcessParens(node.right) && precedence(node.right) >= precedence(node)) {
|
675 | report(node.right);
|
676 | }
|
677 | },
|
678 |
|
679 | BinaryExpression(node) {
|
680 | if (reportsBuffer && node.operator === "in") {
|
681 | reportsBuffer.inExpressionNodes.push(node);
|
682 | }
|
683 |
|
684 | checkBinaryLogical(node);
|
685 | },
|
686 |
|
687 | CallExpression: checkCallNew,
|
688 |
|
689 | ConditionalExpression(node) {
|
690 | if (isReturnAssignException(node)) {
|
691 | return;
|
692 | }
|
693 |
|
694 | if (hasExcessParens(node.test) && precedence(node.test) >= precedence({ type: "LogicalExpression", operator: "||" })) {
|
695 | report(node.test);
|
696 | }
|
697 |
|
698 | if (hasExcessParens(node.consequent) && precedence(node.consequent) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
|
699 | report(node.consequent);
|
700 | }
|
701 |
|
702 | if (hasExcessParens(node.alternate) && precedence(node.alternate) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
|
703 | report(node.alternate);
|
704 | }
|
705 | },
|
706 |
|
707 | DoWhileStatement(node) {
|
708 | if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) {
|
709 | report(node.test);
|
710 | }
|
711 | },
|
712 |
|
713 | ExportDefaultDeclaration: node => checkExpressionOrExportStatement(node.declaration),
|
714 | ExpressionStatement: node => checkExpressionOrExportStatement(node.expression),
|
715 |
|
716 | "ForInStatement, ForOfStatement"(node) {
|
717 | if (node.left.type !== "VariableDeclarator") {
|
718 | const firstLeftToken = sourceCode.getFirstToken(node.left, astUtils.isNotOpeningParenToken);
|
719 |
|
720 | if (
|
721 | firstLeftToken.value === "let" && (
|
722 |
|
723 | /*
|
724 | * If `let` is the only thing on the left side of the loop, it's the loop variable: `for ((let) of foo);`
|
725 | * Removing it will cause a syntax error, because it will be parsed as the start of a VariableDeclarator.
|
726 | */
|
727 | (firstLeftToken.range[1] === node.left.range[1] || /*
|
728 | * If `let` is followed by a `[` token, it's a property access on the `let` value: `for ((let[foo]) of bar);`
|
729 | * Removing it will cause the property access to be parsed as a destructuring declaration of `foo` instead.
|
730 | */
|
731 | astUtils.isOpeningBracketToken(
|
732 | sourceCode.getTokenAfter(firstLeftToken, astUtils.isNotClosingParenToken)
|
733 | ))
|
734 | )
|
735 | ) {
|
736 | tokensToIgnore.add(firstLeftToken);
|
737 | }
|
738 | }
|
739 | if (!(node.type === "ForOfStatement" && node.right.type === "SequenceExpression") && hasExcessParens(node.right)) {
|
740 | report(node.right);
|
741 | }
|
742 | if (hasExcessParens(node.left)) {
|
743 | report(node.left);
|
744 | }
|
745 | },
|
746 |
|
747 | ForStatement(node) {
|
748 | if (node.test && hasExcessParens(node.test) && !isCondAssignException(node)) {
|
749 | report(node.test);
|
750 | }
|
751 |
|
752 | if (node.update && hasExcessParens(node.update)) {
|
753 | report(node.update);
|
754 | }
|
755 |
|
756 | if (node.init) {
|
757 | startNewReportsBuffering();
|
758 |
|
759 | if (hasExcessParens(node.init)) {
|
760 | report(node.init);
|
761 | }
|
762 | }
|
763 | },
|
764 |
|
765 | "ForStatement > *.init:exit"(node) {
|
766 |
|
767 | /*
|
768 | * Removing parentheses around `in` expressions might change semantics and cause errors.
|
769 | *
|
770 | * For example, this valid for loop:
|
771 | * for (let a = (b in c); ;);
|
772 | * after removing parentheses would be treated as an invalid for-in loop:
|
773 | * for (let a = b in c; ;);
|
774 | */
|
775 |
|
776 | if (reportsBuffer.reports.length) {
|
777 | reportsBuffer.inExpressionNodes.forEach(inExpressionNode => {
|
778 | const path = pathToDescendant(node, inExpressionNode);
|
779 | let nodeToExclude;
|
780 |
|
781 | for (let i = 0; i < path.length; i++) {
|
782 | const pathNode = path[i];
|
783 |
|
784 | if (i < path.length - 1) {
|
785 | const nextPathNode = path[i + 1];
|
786 |
|
787 | if (isSafelyEnclosingInExpression(pathNode, nextPathNode)) {
|
788 |
|
789 | // The 'in' expression in safely enclosed by the syntax of its ancestor nodes (e.g. by '{}' or '[]').
|
790 | return;
|
791 | }
|
792 | }
|
793 |
|
794 | if (isParenthesised(pathNode)) {
|
795 | if (isInCurrentReportsBuffer(pathNode)) {
|
796 |
|
797 | // This node was supposed to be reported, but parentheses might be necessary.
|
798 |
|
799 | if (isParenthesisedTwice(pathNode)) {
|
800 |
|
801 | /*
|
802 | * This node is parenthesised twice, it certainly has at least one pair of `extra` parentheses.
|
803 | * If the --fix option is on, the current fixing iteration will remove only one pair of parentheses.
|
804 | * The remaining pair is safely enclosing the 'in' expression.
|
805 | */
|
806 | return;
|
807 | }
|
808 |
|
809 | // Exclude the outermost node only.
|
810 | if (!nodeToExclude) {
|
811 | nodeToExclude = pathNode;
|
812 | }
|
813 |
|
814 | // Don't break the loop here, there might be some safe nodes or parentheses that will stay inside.
|
815 |
|
816 | } else {
|
817 |
|
818 | // This node will stay parenthesised, the 'in' expression in safely enclosed by '()'.
|
819 | return;
|
820 | }
|
821 | }
|
822 | }
|
823 |
|
824 | // Exclude the node from the list (i.e. treat parentheses as necessary)
|
825 | removeFromCurrentReportsBuffer(nodeToExclude);
|
826 | });
|
827 | }
|
828 |
|
829 | endCurrentReportsBuffering();
|
830 | },
|
831 |
|
832 | IfStatement(node) {
|
833 | if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) {
|
834 | report(node.test);
|
835 | }
|
836 | },
|
837 |
|
838 | LogicalExpression: checkBinaryLogical,
|
839 |
|
840 | MemberExpression(node) {
|
841 | const nodeObjHasExcessParens = hasExcessParens(node.object);
|
842 |
|
843 | if (
|
844 | nodeObjHasExcessParens &&
|
845 | precedence(node.object) >= precedence(node) &&
|
846 | (
|
847 | node.computed ||
|
848 | !(
|
849 | astUtils.isDecimalInteger(node.object) ||
|
850 |
|
851 | // RegExp literal is allowed to have parens (#1589)
|
852 | (node.object.type === "Literal" && node.object.regex)
|
853 | )
|
854 | )
|
855 | ) {
|
856 | report(node.object);
|
857 | }
|
858 |
|
859 | if (nodeObjHasExcessParens &&
|
860 | node.object.type === "CallExpression" &&
|
861 | node.parent.type !== "NewExpression") {
|
862 | report(node.object);
|
863 | }
|
864 |
|
865 | if (node.computed && hasExcessParens(node.property)) {
|
866 | report(node.property);
|
867 | }
|
868 | },
|
869 |
|
870 | NewExpression: checkCallNew,
|
871 |
|
872 | ObjectExpression(node) {
|
873 | node.properties
|
874 | .filter(property => {
|
875 | const value = property.value;
|
876 |
|
877 | return value && hasExcessParens(value) && precedence(value) >= PRECEDENCE_OF_ASSIGNMENT_EXPR;
|
878 | }).forEach(property => report(property.value));
|
879 | },
|
880 |
|
881 | Property(node) {
|
882 | if (node.computed) {
|
883 | const { key } = node;
|
884 |
|
885 | if (key && hasExcessParens(key) && precedence(key) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
|
886 | report(key);
|
887 | }
|
888 | }
|
889 | },
|
890 |
|
891 | ReturnStatement(node) {
|
892 | const returnToken = sourceCode.getFirstToken(node);
|
893 |
|
894 | if (isReturnAssignException(node)) {
|
895 | return;
|
896 | }
|
897 |
|
898 | if (node.argument &&
|
899 | hasExcessParensNoLineTerminator(returnToken, node.argument) &&
|
900 |
|
901 | // RegExp literal is allowed to have parens (#1589)
|
902 | !(node.argument.type === "Literal" && node.argument.regex)) {
|
903 | report(node.argument);
|
904 | }
|
905 | },
|
906 |
|
907 | SequenceExpression(node) {
|
908 | node.expressions
|
909 | .filter(e => hasExcessParens(e) && precedence(e) >= precedence(node))
|
910 | .forEach(report);
|
911 | },
|
912 |
|
913 | SwitchCase(node) {
|
914 | if (node.test && hasExcessParens(node.test)) {
|
915 | report(node.test);
|
916 | }
|
917 | },
|
918 |
|
919 | SwitchStatement(node) {
|
920 | if (hasDoubleExcessParens(node.discriminant)) {
|
921 | report(node.discriminant);
|
922 | }
|
923 | },
|
924 |
|
925 | ThrowStatement(node) {
|
926 | const throwToken = sourceCode.getFirstToken(node);
|
927 |
|
928 | if (hasExcessParensNoLineTerminator(throwToken, node.argument)) {
|
929 | report(node.argument);
|
930 | }
|
931 | },
|
932 |
|
933 | UnaryExpression: checkUnaryUpdate,
|
934 | UpdateExpression: checkUnaryUpdate,
|
935 | AwaitExpression: checkUnaryUpdate,
|
936 |
|
937 | VariableDeclarator(node) {
|
938 | if (node.init && hasExcessParens(node.init) &&
|
939 | precedence(node.init) >= PRECEDENCE_OF_ASSIGNMENT_EXPR &&
|
940 |
|
941 | // RegExp literal is allowed to have parens (#1589)
|
942 | !(node.init.type === "Literal" && node.init.regex)) {
|
943 | report(node.init);
|
944 | }
|
945 | },
|
946 |
|
947 | WhileStatement(node) {
|
948 | if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) {
|
949 | report(node.test);
|
950 | }
|
951 | },
|
952 |
|
953 | WithStatement(node) {
|
954 | if (hasDoubleExcessParens(node.object)) {
|
955 | report(node.object);
|
956 | }
|
957 | },
|
958 |
|
959 | YieldExpression(node) {
|
960 | if (node.argument) {
|
961 | const yieldToken = sourceCode.getFirstToken(node);
|
962 |
|
963 | if ((precedence(node.argument) >= precedence(node) &&
|
964 | hasExcessParensNoLineTerminator(yieldToken, node.argument)) ||
|
965 | hasDoubleExcessParens(node.argument)) {
|
966 | report(node.argument);
|
967 | }
|
968 | }
|
969 | },
|
970 |
|
971 | ClassDeclaration: checkClass,
|
972 | ClassExpression: checkClass,
|
973 |
|
974 | SpreadElement: checkSpreadOperator,
|
975 | SpreadProperty: checkSpreadOperator,
|
976 | ExperimentalSpreadProperty: checkSpreadOperator
|
977 | };
|
978 |
|
979 | }
|
980 | };
|