summaryrefslogtreecommitdiff
path: root/tools/node_modules/eslint/lib/rules/no-extra-parens.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/node_modules/eslint/lib/rules/no-extra-parens.js')
-rw-r--r--tools/node_modules/eslint/lib/rules/no-extra-parens.js258
1 files changed, 241 insertions, 17 deletions
diff --git a/tools/node_modules/eslint/lib/rules/no-extra-parens.js b/tools/node_modules/eslint/lib/rules/no-extra-parens.js
index 98fc49654c..6c3198b5f0 100644
--- a/tools/node_modules/eslint/lib/rules/no-extra-parens.js
+++ b/tools/node_modules/eslint/lib/rules/no-extra-parens.js
@@ -81,6 +81,8 @@ module.exports = {
const PRECEDENCE_OF_ASSIGNMENT_EXPR = precedence({ type: "AssignmentExpression" });
const PRECEDENCE_OF_UPDATE_EXPR = precedence({ type: "UpdateExpression" });
+ let reportsBuffer;
+
/**
* Determines if this rule should be enforced for a node given the current configuration.
* @param {ASTNode} node - The node to be checked.
@@ -316,19 +318,33 @@ module.exports = {
}
}
- context.report({
- node,
- loc: leftParenToken.loc.start,
- messageId: "unexpected",
- fix(fixer) {
- const parenthesizedSource = sourceCode.text.slice(leftParenToken.range[1], rightParenToken.range[0]);
+ /**
+ * Finishes reporting
+ * @returns {void}
+ * @private
+ */
+ function finishReport() {
+ context.report({
+ node,
+ loc: leftParenToken.loc.start,
+ messageId: "unexpected",
+ fix(fixer) {
+ const parenthesizedSource = sourceCode.text.slice(leftParenToken.range[1], rightParenToken.range[0]);
+
+ return fixer.replaceTextRange([
+ leftParenToken.range[0],
+ rightParenToken.range[1]
+ ], (requiresLeadingSpace(node) ? " " : "") + parenthesizedSource + (requiresTrailingSpace(node) ? " " : ""));
+ }
+ });
+ }
- return fixer.replaceTextRange([
- leftParenToken.range[0],
- rightParenToken.range[1]
- ], (requiresLeadingSpace(node) ? " " : "") + parenthesizedSource + (requiresTrailingSpace(node) ? " " : ""));
- }
- });
+ if (reportsBuffer) {
+ reportsBuffer.reports.push({ node, finishReport });
+ return;
+ }
+
+ finishReport();
}
/**
@@ -498,6 +514,126 @@ module.exports = {
}
}
+ /**
+ * Finds the path from the given node to the specified ancestor.
+ * @param {ASTNode} node First node in the path.
+ * @param {ASTNode} ancestor Last node in the path.
+ * @returns {ASTNode[]} Path, including both nodes.
+ * @throws {Error} If the given node does not have the specified ancestor.
+ */
+ function pathToAncestor(node, ancestor) {
+ const path = [node];
+ let currentNode = node;
+
+ while (currentNode !== ancestor) {
+
+ currentNode = currentNode.parent;
+
+ /* istanbul ignore if */
+ if (currentNode === null) {
+ throw new Error("Nodes are not in the ancestor-descendant relationship.");
+ }
+
+ path.push(currentNode);
+ }
+
+ return path;
+ }
+
+ /**
+ * Finds the path from the given node to the specified descendant.
+ * @param {ASTNode} node First node in the path.
+ * @param {ASTNode} descendant Last node in the path.
+ * @returns {ASTNode[]} Path, including both nodes.
+ * @throws {Error} If the given node does not have the specified descendant.
+ */
+ function pathToDescendant(node, descendant) {
+ return pathToAncestor(descendant, node).reverse();
+ }
+
+ /**
+ * Checks whether the syntax of the given ancestor of an 'in' expression inside a for-loop initializer
+ * is preventing the 'in' keyword from being interpreted as a part of an ill-formed for-in loop.
+ *
+ * @param {ASTNode} node Ancestor of an 'in' expression.
+ * @param {ASTNode} child Child of the node, ancestor of the same 'in' expression or the 'in' expression itself.
+ * @returns {boolean} True if the keyword 'in' would be interpreted as the 'in' operator, without any parenthesis.
+ */
+ function isSafelyEnclosingInExpression(node, child) {
+ switch (node.type) {
+ case "ArrayExpression":
+ case "ArrayPattern":
+ case "BlockStatement":
+ case "ObjectExpression":
+ case "ObjectPattern":
+ case "TemplateLiteral":
+ return true;
+ case "ArrowFunctionExpression":
+ case "FunctionExpression":
+ return node.params.includes(child);
+ case "CallExpression":
+ case "NewExpression":
+ return node.arguments.includes(child);
+ case "MemberExpression":
+ return node.computed && node.property === child;
+ case "ConditionalExpression":
+ return node.consequent === child;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Starts a new reports buffering. Warnings will be stored in a buffer instead of being reported immediately.
+ * An additional logic that requires multiple nodes (e.g. a whole subtree) may dismiss some of the stored warnings.
+ *
+ * @returns {void}
+ */
+ function startNewReportsBuffering() {
+ reportsBuffer = {
+ upper: reportsBuffer,
+ inExpressionNodes: [],
+ reports: []
+ };
+ }
+
+ /**
+ * Ends the current reports buffering.
+ * @returns {void}
+ */
+ function endCurrentReportsBuffering() {
+ const { upper, inExpressionNodes, reports } = reportsBuffer;
+
+ if (upper) {
+ upper.inExpressionNodes.push(...inExpressionNodes);
+ upper.reports.push(...reports);
+ } else {
+
+ // flush remaining reports
+ reports.forEach(({ finishReport }) => finishReport());
+ }
+
+ reportsBuffer = upper;
+ }
+
+ /**
+ * Checks whether the given node is in the current reports buffer.
+ * @param {ASTNode} node Node to check.
+ * @returns {boolean} True if the node is in the current buffer, false otherwise.
+ */
+ function isInCurrentReportsBuffer(node) {
+ return reportsBuffer.reports.some(r => r.node === node);
+ }
+
+ /**
+ * Removes the given node from the current reports buffer.
+ * @param {ASTNode} node Node to remove.
+ * @returns {void}
+ */
+ function removeFromCurrentReportsBuffer(node) {
+ reportsBuffer.reports = reportsBuffer.reports.filter(r => r.node !== node);
+ }
+
return {
ArrayExpression(node) {
node.elements
@@ -540,7 +676,14 @@ module.exports = {
}
},
- BinaryExpression: checkBinaryLogical,
+ BinaryExpression(node) {
+ if (reportsBuffer && node.operator === "in") {
+ reportsBuffer.inExpressionNodes.push(node);
+ }
+
+ checkBinaryLogical(node);
+ },
+
CallExpression: checkCallNew,
ConditionalExpression(node) {
@@ -602,10 +745,6 @@ module.exports = {
},
ForStatement(node) {
- if (node.init && hasExcessParens(node.init)) {
- report(node.init);
- }
-
if (node.test && hasExcessParens(node.test) && !isCondAssignException(node)) {
report(node.test);
}
@@ -613,6 +752,81 @@ module.exports = {
if (node.update && hasExcessParens(node.update)) {
report(node.update);
}
+
+ if (node.init) {
+ startNewReportsBuffering();
+
+ if (hasExcessParens(node.init)) {
+ report(node.init);
+ }
+ }
+ },
+
+ "ForStatement > *.init:exit"(node) {
+
+ /*
+ * Removing parentheses around `in` expressions might change semantics and cause errors.
+ *
+ * For example, this valid for loop:
+ * for (let a = (b in c); ;);
+ * after removing parentheses would be treated as an invalid for-in loop:
+ * for (let a = b in c; ;);
+ */
+
+ if (reportsBuffer.reports.length) {
+ reportsBuffer.inExpressionNodes.forEach(inExpressionNode => {
+ const path = pathToDescendant(node, inExpressionNode);
+ let nodeToExclude;
+
+ for (let i = 0; i < path.length; i++) {
+ const pathNode = path[i];
+
+ if (i < path.length - 1) {
+ const nextPathNode = path[i + 1];
+
+ if (isSafelyEnclosingInExpression(pathNode, nextPathNode)) {
+
+ // The 'in' expression in safely enclosed by the syntax of its ancestor nodes (e.g. by '{}' or '[]').
+ return;
+ }
+ }
+
+ if (isParenthesised(pathNode)) {
+ if (isInCurrentReportsBuffer(pathNode)) {
+
+ // This node was supposed to be reported, but parentheses might be necessary.
+
+ if (isParenthesisedTwice(pathNode)) {
+
+ /*
+ * This node is parenthesised twice, it certainly has at least one pair of `extra` parentheses.
+ * If the --fix option is on, the current fixing iteration will remove only one pair of parentheses.
+ * The remaining pair is safely enclosing the 'in' expression.
+ */
+ return;
+ }
+
+ // Exclude the outermost node only.
+ if (!nodeToExclude) {
+ nodeToExclude = pathNode;
+ }
+
+ // Don't break the loop here, there might be some safe nodes or parentheses that will stay inside.
+
+ } else {
+
+ // This node will stay parenthesised, the 'in' expression in safely enclosed by '()'.
+ return;
+ }
+ }
+ }
+
+ // Exclude the node from the list (i.e. treat parentheses as necessary)
+ removeFromCurrentReportsBuffer(nodeToExclude);
+ });
+ }
+
+ endCurrentReportsBuffering();
},
IfStatement(node) {
@@ -664,6 +878,16 @@ module.exports = {
}).forEach(property => report(property.value));
},
+ Property(node) {
+ if (node.computed) {
+ const { key } = node;
+
+ if (key && hasExcessParens(key) && precedence(key) >= PRECEDENCE_OF_ASSIGNMENT_EXPR) {
+ report(key);
+ }
+ }
+ },
+
ReturnStatement(node) {
const returnToken = sourceCode.getFirstToken(node);