Fix TSMappedType format with babel-ts parser (#17785)

This commit is contained in:
fisker Cheung authored and GitHub committed 2025-09-24 05:49:53 +08:00
1 parent b35ef6a549
commit e48f0eaf78
5 files changed
+183 -148

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
#### Fix `TSMappedType` format (#17785 by @fisker)
<!-- prettier-ignore -->
```tsx
// Input (--parser=babel-ts)
export type A = B extends { C?: { [D in infer E]?: F } } ? G : H
// Prettier stable
TypeError: Cannot read properties of undefined (reading 'startsWith')
// Prettier main
export type A = B extends { C?: { [D in infer E]?: F } } ? G : H;
```
+149 -144
View File
@@ -46,159 +46,164 @@ function postprocess(ast, options) {
let typeCastCommentsEnds;
ast = visitNode(ast, (node) => {
switch (node.type) {
case "ParenthesizedExpression": {
const { expression } = node;
const start = locStart(node);
ast = visitNode(ast, {
onEnter(node) {
switch (node.type) {
// Remove this when update `@babel/parser` to v8
// https://github.com/typescript-eslint/typescript-eslint/pull/7065
case "TSMappedType":
if (!node.constraint && !node.key) {
const { name: key, constraint } = fixBabelTSTypeParameter(
node.typeParameter,
);
node.constraint = constraint;
node.key = key;
delete node.typeParameter;
}
break;
}
},
onLeave(node) {
switch (node.type) {
case "ParenthesizedExpression": {
const { expression } = node;
const start = locStart(node);
// Align range with `flow`
if (expression.type === "TypeCastExpression") {
expression.range = [start, locEnd(node)];
return expression;
}
let keepTypeCast = false;
if (!isOxcTs) {
if (!typeCastCommentsEnds) {
typeCastCommentsEnds = [];
for (const comment of comments) {
if (isTypeCastComment(comment)) {
typeCastCommentsEnds.push(locEnd(comment));
}
}
// Align range with `flow`
if (expression.type === "TypeCastExpression") {
expression.range = [start, locEnd(node)];
return expression;
}
// Keep ParenthesizedExpression nodes only if they have Closure-style type cast comments.
const previousCommentEnd = typeCastCommentsEnds.findLast(
(end) => end <= start,
);
keepTypeCast =
previousCommentEnd &&
// check that there are only white spaces between the comment and the parenthesis
text.slice(previousCommentEnd, start).trim().length === 0;
let keepTypeCast = false;
if (!isOxcTs) {
if (!typeCastCommentsEnds) {
typeCastCommentsEnds = [];
for (const comment of comments) {
if (isTypeCastComment(comment)) {
typeCastCommentsEnds.push(locEnd(comment));
}
}
}
// Keep ParenthesizedExpression nodes only if they have Closure-style type cast comments.
const previousCommentEnd = typeCastCommentsEnds.findLast(
(end) => end <= start,
);
keepTypeCast =
previousCommentEnd &&
// check that there are only white spaces between the comment and the parenthesis
text.slice(previousCommentEnd, start).trim().length === 0;
}
if (!keepTypeCast) {
expression.extra = { ...expression.extra, parenthesized: true };
return expression;
}
break;
}
if (!keepTypeCast) {
expression.extra = { ...expression.extra, parenthesized: true };
return expression;
case "LogicalExpression":
// We remove unneeded parens around same-operator LogicalExpressions
if (isUnbalancedLogicalTree(node)) {
return rebalanceLogicalTree(node);
}
break;
// This happens when use `oxc-parser` to parse `` `${foo satisfies bar}`; ``
// https://github.com/oxc-project/oxc/issues/11313
case "TemplateLiteral":
/* c8 ignore next 3 */
if (node.expressions.length !== node.quasis.length - 1) {
throw new Error("Malformed template literal.");
}
break;
case "TemplateElement":
// `flow`, `hermes`, `typescript`, and `oxc`(with `{astType: 'ts'}`) follows the `espree` style positions
// https://github.com/eslint/js/blob/5826877f7b33548e5ba984878dd4a8eac8448f87/packages/espree/lib/espree.js#L213
if (
parser === "flow" ||
parser === "hermes" ||
parser === "espree" ||
parser === "typescript" ||
isOxcTs
) {
const start = locStart(node) + 1;
const end = locEnd(node) - (node.tail ? 1 : 2);
node.range = [start, end];
}
break;
// fix unexpected locEnd caused by --no-semi style
case "VariableDeclaration": {
const lastDeclaration = node.declarations.at(-1);
if (lastDeclaration?.init && text[locEnd(lastDeclaration)] !== ";") {
node.range = [locStart(node), locEnd(lastDeclaration)];
}
break;
}
break;
// remove redundant TypeScript nodes
case "TSParenthesizedType":
return node.typeAnnotation;
case "TSTypeParameter":
// babel-ts
fixBabelTSTypeParameter(node);
break;
// For hack-style pipeline
case "TopicReference":
ast.extra = { ...ast.extra, __isUsingHackPipeline: true };
break;
// In Flow parser, it doesn't generate union/intersection types for single type
case "TSUnionType":
case "TSIntersectionType":
if (node.types.length === 1) {
return node.types[0];
}
break;
// Remove this when update `@babel/parser` to v8
// https://github.com/typescript-eslint/typescript-eslint/pull/8920
case "TSEnumDeclaration":
if (!node.body) {
const idEnd = locEnd(node.id);
const { members } = node;
const textWithoutComments = getTextWithoutComments(
{
originalText: text,
[Symbol.for("comments")]: comments,
},
idEnd,
members[0] ? locStart(members[0]) : locEnd(node),
);
const start = idEnd + textWithoutComments.indexOf("{");
node.body = {
type: "TSEnumBody",
members,
range: [start, locEnd(node)],
};
delete node.members;
}
break;
// https://github.com/facebook/hermes/issues/1712
case "ImportExpression":
if (parser === "hermes" && node.attributes && !node.options) {
node.options = node.attributes;
}
break;
}
case "LogicalExpression":
// We remove unneeded parens around same-operator LogicalExpressions
if (isUnbalancedLogicalTree(node)) {
return rebalanceLogicalTree(node);
}
break;
// This happens when use `oxc-parser` to parse `` `${foo satisfies bar}`; ``
// https://github.com/oxc-project/oxc/issues/11313
case "TemplateLiteral":
/* c8 ignore next 3 */
if (node.expressions.length !== node.quasis.length - 1) {
throw new Error("Malformed template literal.");
}
break;
case "TemplateElement":
// `flow`, `hermes`, `typescript`, and `oxc`(with `{astType: 'ts'}`) follows the `espree` style positions
// https://github.com/eslint/js/blob/5826877f7b33548e5ba984878dd4a8eac8448f87/packages/espree/lib/espree.js#L213
if (
parser === "flow" ||
parser === "hermes" ||
parser === "espree" ||
parser === "typescript" ||
isOxcTs
) {
const start = locStart(node) + 1;
const end = locEnd(node) - (node.tail ? 1 : 2);
node.range = [start, end];
}
break;
// fix unexpected locEnd caused by --no-semi style
case "VariableDeclaration": {
const lastDeclaration = node.declarations.at(-1);
if (lastDeclaration?.init && text[locEnd(lastDeclaration)] !== ";") {
node.range = [locStart(node), locEnd(lastDeclaration)];
}
break;
/* c8 ignore next 3 */
if (process.env.NODE_ENV !== "production") {
assertRaw(node, text);
}
// remove redundant TypeScript nodes
case "TSParenthesizedType":
return node.typeAnnotation;
case "TSTypeParameter":
// babel-ts
fixBabelTSTypeParameter(node);
break;
// For hack-style pipeline
case "TopicReference":
ast.extra = { ...ast.extra, __isUsingHackPipeline: true };
break;
// In Flow parser, it doesn't generate union/intersection types for single type
case "TSUnionType":
case "TSIntersectionType":
if (node.types.length === 1) {
return node.types[0];
}
break;
// Remove this when update `@babel/parser` to v8
// https://github.com/typescript-eslint/typescript-eslint/pull/7065
case "TSMappedType":
if (!node.constraint && !node.key) {
const { name: key, constraint } = fixBabelTSTypeParameter(
node.typeParameter,
);
node.constraint = constraint;
node.key = key;
delete node.typeParameter;
}
break;
// Remove this when update `@babel/parser` to v8
// https://github.com/typescript-eslint/typescript-eslint/pull/8920
case "TSEnumDeclaration":
if (!node.body) {
const idEnd = locEnd(node.id);
const { members } = node;
const textWithoutComments = getTextWithoutComments(
{
originalText: text,
[Symbol.for("comments")]: comments,
},
idEnd,
members[0] ? locStart(members[0]) : locEnd(node),
);
const start = idEnd + textWithoutComments.indexOf("{");
node.body = {
type: "TSEnumBody",
members,
range: [start, locEnd(node)],
};
delete node.members;
}
break;
// https://github.com/facebook/hermes/issues/1712
case "ImportExpression":
if (parser === "hermes" && node.attributes && !node.options) {
node.options = node.attributes;
}
break;
}
/* c8 ignore next 3 */
if (process.env.NODE_ENV !== "production") {
assertRaw(node, text);
}
},
});
// `InterpreterDirective` from babel parser and flow parser
@@ -1,6 +1,6 @@
import getVisitorKeys from "../../traverse/get-visitor-keys.js";
function visitNode(node, fn) {
function visitNode(node, options) {
if (!(node !== null && typeof node === "object")) {
return node;
}
@@ -10,17 +10,19 @@ function visitNode(node, fn) {
// measurable difference in performance. Array.entries returns an iterator
// of arrays.
for (let i = 0; i < node.length; i++) {
node[i] = visitNode(node[i], fn);
node[i] = visitNode(node[i], options);
}
return node;
}
node = options.onEnter(node) || node;
const keys = getVisitorKeys(node);
for (let i = 0; i < keys.length; i++) {
node[keys[i]] = visitNode(node[keys[i]], fn);
node[keys[i]] = visitNode(node[keys[i]], options);
}
return fn(node) || node;
return options.onLeave(node) || node;
}
export default visitNode;
@@ -136,6 +136,20 @@ type Type = {
================================================================================
`;
exports[`issue-17784.ts format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
| printWidth
=====================================input======================================
export type A = B extends { C?: { [D in infer E]?: F } } ? G : H
=====================================output=====================================
export type A = B extends { C?: { [D in infer E]?: F } } ? G : H;
================================================================================
`;
exports[`mapped-type.ts format 1`] = `
====================================options=====================================
parsers: ["typescript"]
@@ -0,0 +1 @@
export type A = B extends { C?: { [D in infer E]?: F } } ? G : H