Keep ChainExpression AST unchanged (#18661)

This commit is contained in:
fisker Cheung authored and GitHub committed 2026-01-21 08:26:02 +08:00
1 parent 9ebc4a7142
commit 29c3f97dec
14 files changed
+163 -237

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
#### Preserve non-null assertion around optional chaining (#18661 by @fisker)
<!-- prettier-ignore -->
```tsx
// Input
(a?.b!).c;
(a?.b)!.c;
(a?.b!)!.c;
// Prettier stable
(a?.b)!.c;
(a?.b)!.c;
a?.b!!.c;
// Prettier main
(a?.b!).c;
(a?.b)!.c;
(a?.b!)!.c;
```
-10
View File
@@ -176,16 +176,6 @@ function clean(original, cloned, parent) {
if (original.type === "TemplateLiteral") {
removeTemplateElementsValue(cloned);
}
// We print `(a?.b!).c` as `(a?.b)!.c`, but `typescript` parse them differently
if (
original.type === "ChainExpression" &&
original.expression.type === "TSNonNullExpression"
) {
// Ideally, we should swap these two nodes, but `type` is the only difference
cloned.type = "TSNonNullExpression";
cloned.expression.type = "ChainExpression";
}
}
clean.ignoredProperties = ignoredProperties;
@@ -0,0 +1,70 @@
import { createTypeCheckFunction } from "../utilities/index.js";
/**
@import AstPath from "../../common/ast-path.js"
*/
function shouldAddParenthesesToChainExpression(path) {
const { key, parent } = path;
return (
(key === "expression" && parent.type === "TSNonNullExpression") ||
(key === "object" &&
parent.type === "MemberExpression" &&
!parent.optional) ||
(key === "callee" &&
parent.type === "CallExpression" &&
!parent.optional) ||
(key === "callee" && parent.type === "NewExpression") ||
(key === "tag" && parent.type === "TaggedTemplateExpression")
);
}
const isParenthesized = (node) => node.extra?.parenthesized;
const isBabelOptionalChainElement = createTypeCheckFunction([
"OptionalCallExpression",
"OptionalMemberExpression",
]);
/**
@param {AstPath} path
*/
function isBabelOptionalChainRoot(path) {
const { node } = path;
let child = node;
while (child.type === "TSNonNullExpression") {
child = child.expression;
if (isParenthesized(child)) {
return false;
}
}
if (!isBabelOptionalChainElement(child)) {
return false;
}
if (isParenthesized(node)) {
return true;
}
return !(
(path.key === "expression" && path.parent.type === "TSNonNullExpression")
// We should exclude if it's in `OptionalCallExpression` or `OptionalMemberExpression`
// But in these cases it should not matter, since we don't need parentheses anyway
);
}
/**
@param {AstPath} path
@returns {boolean}
*/
function shouldAddParenthesesToChainElement(path) {
return (
(path.node.type === "ChainExpression" || isBabelOptionalChainRoot(path)) &&
shouldAddParenthesesToChainExpression(path)
);
}
export { shouldAddParenthesesToChainElement };
@@ -26,6 +26,7 @@ import {
stripChainElementWrappers,
} from "../utilities/index.js";
import { returnArgumentHasLeadingComment } from "../utilities/return-statement-has-leading-comment.js";
import { shouldAddParenthesesToChainElement } from "./chain-expression.js";
import { shouldAddParenthesesToIdentifier } from "./identifier.js";
/**
@@ -829,15 +830,16 @@ function needsParentheses(path, options) {
}
case "OptionalMemberExpression":
case "OptionalCallExpression":
case "CallExpression":
case "MemberExpression":
case "ChainExpression":
case "TSNonNullExpression":
if (shouldAddParenthesesToChainElement(path)) {
return true;
}
// fallthrough
case "CallExpression":
case "MemberExpression":
case "TaggedTemplateExpression":
case "TSNonNullExpression":
if (
key === "callee" &&
(parent.type === "BindExpression" || parent.type === "NewExpression")
@@ -1086,158 +1088,6 @@ function shouldWrapFunctionForExportDefault(path, options) {
);
}
/*
Matches following cases:
```js
(a?.b).c;
(a?.()).b;
(a?.b!).c;
(a?.()!).b;
(a?.b)!.c;
(a?.())!.b;
(a?.b)();
(a?.())();
new (a?.b)();
new (a?.())();
(a?.b)``;
(a?.())``;
```
*/
/**
* @param {AstPath} path
* @returns {boolean}
*/
function shouldAddParenthesesToChainElement(path) {
if (
// ESTree
path.match(
undefined,
(node, name) => name === "expression" && node.type === "ChainExpression",
(node, name) =>
name === "tag" && node.type === "TaggedTemplateExpression",
) ||
// Babel
path.match(
(node) =>
node.type === "OptionalCallExpression" ||
node.type === "OptionalMemberExpression",
(node, name) =>
name === "tag" && node.type === "TaggedTemplateExpression",
) ||
// Babel-ts
// (a?.b)!``;
// (a?.b!)``;
path.match(
(node) =>
node.type === "OptionalCallExpression" ||
node.type === "OptionalMemberExpression",
(node, name) =>
name === "expression" && node.type === "TSNonNullExpression",
(node, name) =>
name === "tag" && node.type === "TaggedTemplateExpression",
) ||
// case (a?.b)!``; in Typescript
path.match(
undefined,
(node, name) => name === "expression" && node.type === "ChainExpression",
(node, name) =>
name === "expression" && node.type === "TSNonNullExpression",
(node, name) =>
name === "tag" && node.type === "TaggedTemplateExpression",
) ||
// case (a?.b!)``; in Typescript
path.match(
undefined,
(node, name) =>
name === "expression" && node.type === "TSNonNullExpression",
(node, name) => name === "expression" && node.type === "ChainExpression",
(node, name) =>
name === "tag" && node.type === "TaggedTemplateExpression",
)
) {
return true;
}
if (
path.match(
(node) =>
node.type === "OptionalMemberExpression" ||
node.type === "OptionalCallExpression",
(node, name) =>
(name === "object" && node.type === "MemberExpression") ||
(name === "callee" &&
(node.type === "CallExpression" || node.type === "NewExpression")),
) ||
path.match(
(node) =>
node.type === "OptionalMemberExpression" ||
node.type === "OptionalCallExpression",
(node, name) =>
name === "expression" && node.type === "TSNonNullExpression",
(node, name) =>
(name === "object" && node.type === "MemberExpression") ||
(name === "callee" && node.type === "CallExpression"),
)
) {
return true;
}
// ESTree, same logic as babel
if (
path.match(
(node) =>
node.type === "CallExpression" || node.type === "MemberExpression",
(node, name) => name === "expression" && node.type === "ChainExpression",
) &&
(path.match(
undefined,
undefined,
(node, name) =>
(name === "callee" &&
((node.type === "CallExpression" && !node.optional) ||
node.type === "NewExpression")) ||
(name === "object" &&
node.type === "MemberExpression" &&
!node.optional),
) ||
path.match(
undefined,
undefined,
(node, name) =>
name === "expression" && node.type === "TSNonNullExpression",
(node, name) =>
(name === "object" && node.type === "MemberExpression") ||
(name === "callee" && node.type === "CallExpression"),
))
) {
return true;
}
// Babel treat `(a?.b!).c` and `(a?.b)!.c` the same, https://github.com/babel/babel/discussions/15077
// Use this to align with babel
if (
path.match(
(node) =>
node.type === "CallExpression" || node.type === "MemberExpression",
(node, name) =>
name === "expression" && node.type === "TSNonNullExpression",
(node, name) => name === "expression" && node.type === "ChainExpression",
(node, name) =>
(name === "object" && node.type === "MemberExpression") ||
(name === "callee" && node.type === "CallExpression"),
)
) {
return true;
}
// This function only handle cases above
return false;
}
function isDecoratorMemberExpression(node) {
if (node.type === "Identifier") {
return true;
+6 -13
View File
@@ -46,16 +46,12 @@ import { printOptionalToken } from "./miscellaneous.js";
// The way it is structured in the AST is via a nested sequence of
// MemberExpression and CallExpression. We need to traverse the AST
// and make groups out of it to print it in the desired way.
/*
- `BindExpression`
- `MemberExpression`
- `OptionalMemberExpression`
*/
function printMemberChain(path, options, print) {
// TODO[@fisker]: skip `TSNonNullExpression`
/* c8 ignore next 6 */
if (path.node.type === "ChainExpression") {
return path.call(
() => printMemberChain(path, options, print),
"expression",
);
}
const isExpressionStatement =
(path.parent.type === "ChainExpression" ? path.grandparent : path.parent)
.type === "ExpressionStatement";
@@ -95,10 +91,6 @@ function printMemberChain(path, options, print) {
function rec() {
const { node } = path;
if (node.type === "ChainExpression") {
return path.call(rec, "expression");
}
if (
isCallExpression(node) &&
(isMemberish(node.callee) || isCallExpression(node.callee))
@@ -137,6 +129,7 @@ function printMemberChain(path, options, print) {
} else if (node.type === "TSNonNullExpression") {
printedNodes.unshift({
node,
needsParens: needsParentheses(path, options),
printed: printComments(path, "!", options),
});
path.call(rec, "expression");
@@ -68,7 +68,7 @@ printWidth: 80
=====================================output=====================================
{{ foo?.bar!.bam }}
{{ (foo?.bar)!.bam }}
{{ (foo?.bar)!.bam }}
{{ (foo?.bar!).bam }}
================================================================================
`;
@@ -506,9 +506,8 @@ if (true) {
.type;
const secondType3 = (sourceCode?.getNodeByRangeIndex1234(second.range[0])!)
.type;
// TODO[@fisker]: Fix
// const secondType4 = (sourceCode?.getNodeByRangeIndex1234(second.range[0])!)!
// .type;
const secondType4 = (sourceCode?.getNodeByRangeIndex1234(second.range[0])!)!
.type;
const secondType5 = sourceCode.getNodeByRangeIndex1234(second.range[0])!!
.type;
}
@@ -525,10 +524,10 @@ if (true) {
)).type;
const secondType3 = (sourceCode?.getNodeByRangeIndex1234(
second.range[0],
))!.type;
// TODO[@fisker]: Fix
// const secondType4 = (sourceCode?.getNodeByRangeIndex1234(second.range[0])!)!
// .type;
)!).type;
const secondType4 = (sourceCode?.getNodeByRangeIndex1234(
second.range[0],
)!)!.type;
const secondType5 = sourceCode.getNodeByRangeIndex1234(
second.range[0],
)!!.type;
@@ -6,9 +6,8 @@ if (true) {
.type;
const secondType3 = (sourceCode?.getNodeByRangeIndex1234(second.range[0])!)
.type;
// TODO[@fisker]: Fix
// const secondType4 = (sourceCode?.getNodeByRangeIndex1234(second.range[0])!)!
// .type;
const secondType4 = (sourceCode?.getNodeByRangeIndex1234(second.range[0])!)!
.type;
const secondType5 = sourceCode.getNodeByRangeIndex1234(second.range[0])!!
.type;
}
@@ -74,14 +74,14 @@ foo((a?.b)!)
=====================================output=====================================
// Member expressions
(a?.b)!();
(a?.b)!();
(a?.b!)();
(a!?.b)();
(a.b?.c)!();
(a.b?.c)!();
(a.b?.c!)();
(a.b!?.c)();
(a!.b?.c)();
(a?.b.c)!();
(a?.b.c)!();
(a?.b.c!)();
(a?.b!.c)();
(a!?.b.c)();
a[b?.c]!();
@@ -105,21 +105,21 @@ a![b?.()]();
(a?.()).b!();
(a?.()).b!();
(a?.())!.b();
(a?.())!.b();
(a?.()!).b();
(a!?.()).b();
// Call expressions
(a?.())!();
(a?.())!();
(a?.()!)();
(a!?.())();
(a.b.c?.())!();
(a.b.c?.())!();
(a.b.c?.()!)();
(a.b.c!?.())();
(a.b?.c())!();
(a.b?.c())!();
(a.b?.c()!)();
(a.b!?.c())();
(a?.b.c())!();
(a?.b.c())!();
(a?.b.c()!)();
(a?.b!.c())();
a(b?.c)!();
a(b?.c)!();
@@ -127,15 +127,15 @@ a(b?.c!)();
(a?.b)()!();
(a?.b)()!();
(a?.b)!()();
(a?.b)!()();
(a?.b!)()();
(a?.())()!();
(a?.())()!();
(a?.())!()();
(a?.())!()();
(a?.()!)()();
(a!?.())()();
// Not \`.callee\`
foo(a?.b!);
foo((a?.b)!);
================================================================================
`;
@@ -285,14 +285,14 @@ _[a?.b!]
=====================================output=====================================
// Member expressions
(a?.b)!.foo;
(a?.b)!.foo;
(a?.b!).foo;
(a!?.b).foo;
(a.b?.c)!.foo;
(a.b?.c)!.foo;
(a.b?.c!).foo;
(a.b!?.c).foo;
(a!.b?.c).foo;
(a?.b.c)!.foo;
(a?.b.c)!.foo;
(a?.b.c!).foo;
(a?.b!.c).foo;
(a!?.b.c).foo;
a[b?.c]!.foo;
@@ -301,7 +301,7 @@ a[b?.c!].foo;
a[b!?.c].foo;
(a?.b).c!.foo;
(a?.b).c!.foo;
(a?.b)!.c.foo;
(a?.b!).c.foo;
(a!?.b).c.foo;
a[b?.()]!.foo;
a[b?.()]!.foo;
@@ -311,26 +311,26 @@ a![b?.()].foo;
(a?.b).c!.foo;
(a?.b).c!.foo;
(a?.b)!.c.foo;
(a?.b)!.c.foo;
(a?.b!).c.foo;
(a!?.b).c.foo;
(a?.()).b!.foo;
(a?.()).b!.foo;
(a?.())!.b.foo;
(a?.())!.b.foo;
(a?.()!).b.foo;
(a!?.()).b.foo;
// Call expressions
(a?.())!.foo;
(a?.())!.foo;
(a?.()!).foo;
(a!?.()).foo;
(a.b.c?.())!.foo;
(a.b.c?.())!.foo;
(a.b.c?.()!).foo;
(a.b.c!?.()).foo;
(a.b?.c())!.foo;
(a.b?.c())!.foo;
(a.b?.c()!).foo;
(a.b!?.c()).foo;
(a?.b.c())!.foo;
(a?.b.c())!.foo;
(a?.b.c()!).foo;
(a?.b!.c()).foo;
a(b?.c)!.foo;
a(b?.c)!.foo;
@@ -338,11 +338,11 @@ a(b?.c!).foo;
(a?.b)()!.foo;
(a?.b)()!.foo;
(a?.b)!().foo;
(a?.b)!().foo;
(a?.b!)().foo;
(a?.())()!.foo;
(a?.())()!.foo;
(a?.())!().foo;
(a?.())!().foo;
(a?.()!)().foo;
(a!?.())().foo;
// Not \`.object\`
@@ -444,7 +444,7 @@ new a[b?.c!]();
new a[b!?.c]();
new (a?.b).c!();
new (a?.b).c!();
new (a?.b)!.c();
new (a?.b!).c();
new (a!?.b).c();
new a[b?.()]!();
new a[b?.()]!();
@@ -454,7 +454,7 @@ new a![b?.()]();
new (a?.b).c!();
new (a?.b).c!();
new (a?.b)!.c();
new (a?.b)!.c();
new (a?.b!).c();
new (a!?.b).c();
// FIXME: new ((a?.()).b)! ();
// FIXME: new ((a?.()).b!) ();
@@ -481,11 +481,11 @@ new (a(b?.c!))();
new ((a?.b)()!)();
new ((a?.b)()!)();
new ((a?.b)!())();
new ((a?.b)!())();
new ((a?.b!)())();
new ((a?.())()!)();
new ((a?.())()!)();
new ((a?.())!())();
new ((a?.())!())();
new ((a?.()!)())();
new ((a!?.())())();
// Not \`.callee\`
@@ -565,14 +565,14 @@ printWidth: 80
=====================================output=====================================
// Member expressions
(a?.b)!\`\`;
(a?.b)!\`\`;
(a?.b!)\`\`;
(a!?.b)\`\`;
(a.b?.c)!\`\`;
(a.b?.c)!\`\`;
(a.b?.c!)\`\`;
(a.b!?.c)\`\`;
(a!.b?.c)\`\`;
(a?.b.c)!\`\`;
(a?.b.c)!\`\`;
(a?.b.c!)\`\`;
(a?.b!.c)\`\`;
(a!?.b.c)\`\`;
a[b?.c]!\`\`;
@@ -581,7 +581,7 @@ a[b?.c!]\`\`;
a[b!?.c]\`\`;
(a?.b).c!\`\`;
(a?.b).c!\`\`;
(a?.b)!.c\`\`;
(a?.b!).c\`\`;
(a!?.b).c\`\`;
a[b?.()]!\`\`;
a[b?.()]!\`\`;
@@ -591,26 +591,26 @@ a![b?.()]\`\`;
(a?.b).c!\`\`;
(a?.b).c!\`\`;
(a?.b)!.c\`\`;
(a?.b)!.c\`\`;
(a?.b!).c\`\`;
(a!?.b).c\`\`;
(a?.()).b!\`\`;
(a?.()).b!\`\`;
(a?.())!.b\`\`;
(a?.())!.b\`\`;
(a?.()!).b\`\`;
(a!?.()).b\`\`;
// Call expressions
(a?.())!\`\`;
(a?.())!\`\`;
(a?.()!)\`\`;
(a!?.())\`\`;
(a.b.c?.())!\`\`;
(a.b.c?.())!\`\`;
(a.b.c?.()!)\`\`;
(a.b.c!?.())\`\`;
(a.b?.c())!\`\`;
(a.b?.c())!\`\`;
(a.b?.c()!)\`\`;
(a.b!?.c())\`\`;
(a?.b.c())!\`\`;
(a?.b.c())!\`\`;
(a?.b.c()!)\`\`;
(a?.b!.c())\`\`;
a(b?.c)!\`\`;
a(b?.c)!\`\`;
@@ -618,11 +618,11 @@ a(b?.c!)\`\`;
(a?.b)()!\`\`;
(a?.b)()!\`\`;
(a?.b)!()\`\`;
(a?.b)!()\`\`;
(a?.b!)()\`\`;
(a?.())()!\`\`;
(a?.())()!\`\`;
(a?.())!()\`\`;
(a?.())!()\`\`;
(a?.()!)()\`\`;
(a!?.())()\`\`;
================================================================================
@@ -640,8 +640,8 @@ printWidth: 80
(a?.())!.b;
=====================================output=====================================
(a?.b)!.c;
(a?.())!.b;
(a?.b!).c;
(a?.()!).b;
(a?.b)!.c;
(a?.())!.b;
@@ -105,7 +105,9 @@ a?.().b!.c?.c;
(a?.().b!).c;
(a?.().b)!.c;
(a?.b)![c?.d!]
(a?.b)![c?.d!];
(a?.b!!)!!.c;
=====================================output=====================================
a?.b!.c;
@@ -114,7 +116,7 @@ a?.b.c!.d;
a!.b?.c;
a?.b!?.c;
a?.b!.c?.c;
(a?.b)!.c;
(a?.b!).c;
(a?.b)!.c;
a?.().b!.c;
@@ -122,11 +124,13 @@ a?.().b!.c.d;
a?.().b.c!.d;
a?.().b!?.c;
a?.().b!.c?.c;
(a?.().b)!.c;
(a?.().b!).c;
(a?.().b)!.c;
(a?.b)![c?.d!];
(a?.b!!)!!.c;
================================================================================
`;
@@ -15,4 +15,6 @@ a?.().b!.c?.c;
(a?.().b!).c;
(a?.().b)!.c;
(a?.b)![c?.d!]
(a?.b)![c?.d!];
(a?.b!!)!!.c;
@@ -78,8 +78,8 @@ long1 = \`long \${
} long longlong \${a.b.c.d.e} long longlong \${a.b.c.d.e} long longlong \${a.b.c.d.e} long long\`;
long1 = \`long \${
a //comment
?.b!
(a //comment
?.b)!
} long longlong \${a.b.c.d.e} long longlong \${a.b.c.d.e} long longlong \${a.b.c.d.e} long long\`;
================================================================================
@@ -126,7 +126,7 @@ const footer = "<footer></footer>";
================================================================================
`;
exports[`optional-chainning.tsx format 1`] = `
exports[`optional-chaining.tsx format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80