Extract shouldAddParenthesesToIdentifier (#18432)

This commit is contained in:
fisker Cheung authored and GitHub committed 2025-12-06 17:46:42 +08:00
1 parent b6cf29f3d0
commit 1f6ba74b87
4 files changed
+117 -99

No files matched your search

@@ -53,7 +53,7 @@ export default {
fixable: "code",
},
create(context) {
if (path.basename(context.getFilename()) !== "needs-parens.js") {
if (path.basename(path.dirname(context.getFilename())) !== "parentheses") {
return {};
}
const sourceCode = context.getSourceCode();
@@ -33,7 +33,7 @@ test("await-cli-tests", {
],
});
test("better-parent-property-check-in-needs-parens", {
test("better-parent-property-check-in-parentheses-check", {
valid: ["function needsParens() {return parent.test === node;}"],
invalid: [
{
@@ -70,7 +70,7 @@ test("better-parent-property-check-in-needs-parens", {
output: testCase.output
? `function needsParens() {${testCase.output}}`
: null,
filename: "needs-parens.js",
filename: "parentheses/foo.js",
})),
});
+112
View File
@@ -0,0 +1,112 @@
import {
isBinaryCastExpression,
startsWithNoLookaheadToken,
} from "../utilities/index.js";
function shouldAddParenthesesToIdentifier(path) {
const { node } = path;
// Identifiers never need parentheses.
if (node.type !== "Identifier") {
return false;
}
// ...unless those identifiers are embed placeholders. They might be substituted by complex
// expressions, so the parens around them should not be dropped. Example (JS-in-HTML-in-JS):
// let tpl = html`<script> f((${expr}) / 2); </script>`;
// If the inner JS formatter removes the parens, the expression might change its meaning:
// f((a + b) / 2) vs f(a + b / 2)
if (
node.extra?.parenthesized &&
/^PRETTIER_HTML_PLACEHOLDER_\d+_\d+_IN_JS$/u.test(node.name)
) {
return true;
}
const { key, parent } = path;
// `for ((async) of []);` and `for ((let) of []);`
if (
key === "left" &&
((node.name === "async" && !parent.await) || node.name === "let") &&
parent.type === "ForOfStatement"
) {
return true;
}
// `for ((let.a) of []);`
if (node.name === "let") {
const expression = path.findAncestor(
(node) => node.type === "ForOfStatement",
)?.left;
if (
expression &&
startsWithNoLookaheadToken(
expression,
(leftmostNode) => leftmostNode === node,
)
) {
return true;
}
}
// `(let)[a] = 1`
if (
key === "object" &&
node.name === "let" &&
parent.type === "MemberExpression" &&
parent.computed &&
!parent.optional
) {
const statement = path.findAncestor(
(node) =>
node.type === "ExpressionStatement" ||
node.type === "ForStatement" ||
node.type === "ForInStatement",
);
const expression = !statement
? undefined
: statement.type === "ExpressionStatement"
? statement.expression
: statement.type === "ForStatement"
? statement.init
: statement.left;
if (
expression &&
startsWithNoLookaheadToken(
expression,
(leftmostNode) => leftmostNode === node,
)
) {
return true;
}
}
// `(type) satisfies never;` and similar cases
if (key === "expression") {
switch (node.name) {
case "await":
case "interface":
case "module":
case "using":
case "yield":
case "let":
case "component":
case "hook":
case "type": {
const ancestorNeitherAsNorSatisfies = path.findAncestor(
(node) => !isBinaryCastExpression(node),
);
if (
ancestorNeitherAsNorSatisfies !== parent &&
ancestorNeitherAsNorSatisfies.type === "ExpressionStatement"
) {
return true;
}
}
}
}
return false;
}
export { shouldAddParenthesesToIdentifier };
@@ -24,6 +24,7 @@ import {
startsWithNoLookaheadToken,
} from "../utilities/index.js";
import { returnArgumentHasLeadingComment } from "../utilities/return-statement-has-leading-comment.js";
import { shouldAddParenthesesToIdentifier } from "./identifier.js";
/**
* @import AstPath from "../../common/ast-path.js"
@@ -55,103 +56,8 @@ function needsParentheses(path, options) {
return false;
}
// Identifiers never need parentheses.
if (node.type === "Identifier") {
// ...unless those identifiers are embed placeholders. They might be substituted by complex
// expressions, so the parens around them should not be dropped. Example (JS-in-HTML-in-JS):
// let tpl = html`<script> f((${expr}) / 2); </script>`;
// If the inner JS formatter removes the parens, the expression might change its meaning:
// f((a + b) / 2) vs f(a + b / 2)
if (
node.extra?.parenthesized &&
/^PRETTIER_HTML_PLACEHOLDER_\d+_\d+_IN_JS$/u.test(node.name)
) {
return true;
}
// `for ((async) of []);` and `for ((let) of []);`
if (
key === "left" &&
((node.name === "async" && !parent.await) || node.name === "let") &&
parent.type === "ForOfStatement"
) {
return true;
}
// `for ((let.a) of []);`
if (node.name === "let") {
const expression = path.findAncestor(
(node) => node.type === "ForOfStatement",
)?.left;
if (
expression &&
startsWithNoLookaheadToken(
expression,
(leftmostNode) => leftmostNode === node,
)
) {
return true;
}
}
// `(let)[a] = 1`
if (
key === "object" &&
node.name === "let" &&
parent.type === "MemberExpression" &&
parent.computed &&
!parent.optional
) {
const statement = path.findAncestor(
(node) =>
node.type === "ExpressionStatement" ||
node.type === "ForStatement" ||
node.type === "ForInStatement",
);
const expression = !statement
? undefined
: statement.type === "ExpressionStatement"
? statement.expression
: statement.type === "ForStatement"
? statement.init
: statement.left;
if (
expression &&
startsWithNoLookaheadToken(
expression,
(leftmostNode) => leftmostNode === node,
)
) {
return true;
}
}
// `(type) satisfies never;` and similar cases
if (key === "expression") {
switch (node.name) {
case "await":
case "interface":
case "module":
case "using":
case "yield":
case "let":
case "component":
case "hook":
case "type": {
const ancestorNeitherAsNorSatisfies = path.findAncestor(
(node) => !isBinaryCastExpression(node),
);
if (
ancestorNeitherAsNorSatisfies !== parent &&
ancestorNeitherAsNorSatisfies.type === "ExpressionStatement"
) {
return true;
}
}
}
}
return false;
return shouldAddParenthesesToIdentifier(path);
}
if (