Skip to content

Commit

Permalink
Fix trailing coma print in type prameters, improve jsx parse (prettie…
Browse files Browse the repository at this point in the history
…r#14688)

Co-authored-by: fisker Cheung <lionkay@gmail.com>
  • Loading branch information
sosukesuzuki and fisker authored Apr 21, 2023
1 parent b5043bc commit cfb3b87
Show file tree
Hide file tree
Showing 18 changed files with 214 additions and 31 deletions.
15 changes: 15 additions & 0 deletions changelog_unreleased/typescript/14688.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#### Fix missing required comma in type parameters (#14688 by @fisker, @sosukesuzuki)

Previously, we only print trailing comma when file extension is `.tsx`, turns out `.mts`, `.cts` files requires it to parse too.

<!-- prettier-ignore -->
```tsx
// Input
export const unsafeCoerce = <T,>(u: unknown): T => u as T

// Prettier stable
export const unsafeCoerce = <T>(u: unknown): T => u as T;

// Prettier main
export const unsafeCoerce = <T,>(u: unknown): T => u as T;
```
2 changes: 1 addition & 1 deletion scripts/build/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ const pluginFiles = [
process: (text) =>
text.replace(
'require("path")',
"{extname: file => file.split('.').pop()}"
'{extname: file => "." + file.split(".").pop()}'
),
},
{
Expand Down
35 changes: 25 additions & 10 deletions src/language-js/parse/typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ import postprocess from "./postprocess/index.js";
import { throwErrorForInvalidNodes } from "./postprocess/typescript.js";

/** @type {import("@typescript-eslint/typescript-estree").TSESTreeOptions} */
const parseOptions = {
const baseParseOptions = {
// `jest@<=26.4.2` rely on `loc`
// https://github.com/facebook/jest/issues/10444
// Set `loc` and `range` to `true` also prevent AST traverse
// https://github.com/typescript-eslint/typescript-eslint/blob/733b3598c17d3a712cf6f043115587f724dbe3ef/packages/typescript-estree/src/ast-converter.ts#L38
loc: true,
range: true,
comment: true,
jsx: true,
tokens: true,
loggerFn: false,
project: [],
Expand All @@ -37,18 +36,34 @@ function createParseError(error) {
});
}

function parse(text) {
// https://typescript-eslint.io/architecture/parser/#jsx
const isKnownFileType = (filepath) =>
/\.(?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$/i.test(filepath);

function getParseOptionsCombinations(text, options) {
const filepath = options?.filepath;
if (filepath && isKnownFileType(filepath)) {
return [{ ...baseParseOptions, filePath: filepath }];
}

const shouldEnableJsx = isProbablyJsx(text);
return [
{ ...baseParseOptions, jsx: shouldEnableJsx },
{ ...baseParseOptions, jsx: !shouldEnableJsx },
];
}

function parse(text, options) {
const textToParse = replaceHashbang(text);
const jsx = isProbablyJsx(text);
const parseOptionsCombinations = getParseOptionsCombinations(text, options);

let result;
try {
result = tryCombinations([
// Try passing with our best guess first.
() => parseWithNodeMaps(textToParse, { ...parseOptions, jsx }),
// But if we get it wrong, try the opposite.
() => parseWithNodeMaps(textToParse, { ...parseOptions, jsx: !jsx }),
]);
result = tryCombinations(
parseOptionsCombinations.map(
(parseOptions) => () => parseWithNodeMaps(textToParse, parseOptions)
)
);
} catch ({
errors: [
// Suppose our guess is correct, throw the first error
Expand Down
23 changes: 15 additions & 8 deletions src/language-js/print/type-parameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
isTestCall,
hasComment,
CommentCheckFlags,
isTSXFile,
shouldPrintComma,
getFunctionParameters,
isObjectType,
Expand All @@ -27,6 +26,20 @@ import { printTypeScriptMappedTypeModifier } from "./mapped-type.js";

const getTypeParametersGroupId = createGroupIdMapper("typeParameters");

// Keep comma if the file extension not `.ts` and
// has one type parameter that isn't extend with any types.
// Because, otherwise formatted result will be invalid as tsx.
function shouldForceTrailingComma(path, options, paramsKey) {
const { node } = path;
return (
getFunctionParameters(node).length === 1 &&
node.type.startsWith("TS") &&
!node[paramsKey][0].constraint &&
path.parent.type === "ArrowFunctionExpression" &&
!(options.filepath && /\.ts$/.test(options.filepath))
);
}

function printTypeParameters(path, options, print, paramsKey) {
const { node } = path;

Expand Down Expand Up @@ -68,16 +81,10 @@ function printTypeParameters(path, options, print, paramsKey) {
];
}

// Keep comma if the file extension is .tsx and
// has one type parameter that isn't extend with any types.
// Because, otherwise formatted result will be invalid as tsx.
const trailingComma =
node.type === "TSTypeParameterInstantiation" // https://github.com/microsoft/TypeScript/issues/21984
? ""
: getFunctionParameters(node).length === 1 &&
isTSXFile(options) &&
!node[paramsKey][0].constraint &&
path.parent.type === "ArrowFunctionExpression"
: shouldForceTrailingComma(path, options, paramsKey)
? ","
: shouldPrintComma(options)
? ifBreak(",")
Expand Down
5 changes: 0 additions & 5 deletions src/language-js/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -730,10 +730,6 @@ function identity(x) {
return x;
}

function isTSXFile(options) {
return options.filepath && /\.tsx$/i.test(options.filepath);
}

/**
* @param {any} options
* @param {("es5" | "all")} [level]
Expand Down Expand Up @@ -1151,7 +1147,6 @@ export {
isStringPropSafeToUnquote,
isTemplateOnItsOwnLine,
isTestCall,
isTSXFile,
isTypeAnnotationAFunction,
isNextLineEmpty,
needsHardlineAfterDanglingComment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ exports[`export-declare.ts [typescript] format 1`] = `
2 |"
`;

exports[`invalid-jsx-1.ts [typescript] format 1`] = `
exports[`invalid-jsx-1.tsx [typescript] format 1`] = `
"Unexpected token. Did you mean \`{'>'}\` or \`&gt;\`? (3:45)
1 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#-and--are-now-invalid-jsx-text-characters
2 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ abstract class AbstractFoo {
================================================================================
`;

exports[`after_jsx_generic.ts format 1`] = `
exports[`after_jsx_generic.tsx format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
Expand Down Expand Up @@ -293,7 +293,7 @@ export type AsyncExecuteOptions = child_process$execFileOpts & {
================================================================================
`;

exports[`jsx.ts format 1`] = `
exports[`jsx.tsx format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default class AddAssetHtmlPlugin {
================================================================================
`;

exports[`decorated-function.ts format 1`] = `
exports[`decorated-function.tsx format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
Expand Down Expand Up @@ -198,7 +198,7 @@ var listener = DOM.listen(
================================================================================
`;
exports[`forward-ref.ts format 1`] = `
exports[`forward-ref.tsx format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
Expand Down
131 changes: 131 additions & 0 deletions tests/format/typescript/tsx/comma/__snapshots__/jsfmt.spec.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`snippet: test.cjs format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
| printWidth
=====================================input======================================
const A = <T,>() => {}
=====================================output=====================================
const A = <T,>() => {};
================================================================================
`;

exports[`snippet: test.cts format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
| printWidth
=====================================input======================================
const A = <T,>() => {}
=====================================output=====================================
const A = <T,>() => {};
================================================================================
`;

exports[`snippet: test.js format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
| printWidth
=====================================input======================================
const A = <T,>() => {}
=====================================output=====================================
const A = <T,>() => {};
================================================================================
`;

exports[`snippet: test.jsx format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
| printWidth
=====================================input======================================
const A = <T,>() => {}
=====================================output=====================================
const A = <T,>() => {};
================================================================================
`;

exports[`snippet: test.mjs format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
| printWidth
=====================================input======================================
const A = <T,>() => {}
=====================================output=====================================
const A = <T,>() => {};
================================================================================
`;

exports[`snippet: test.mts format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
| printWidth
=====================================input======================================
const A = <T,>() => {}
=====================================output=====================================
const A = <T,>() => {};
================================================================================
`;

exports[`snippet: test.ts format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
| printWidth
=====================================input======================================
const A = <T,>() => {}
=====================================output=====================================
const A = <T>() => {};
================================================================================
`;
exports[`snippet: test.tsx format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
| printWidth
=====================================input======================================
const A = <T,>() => {}
=====================================output=====================================
const A = <T,>() => {};
================================================================================
`;
exports[`snippet: test.unknown format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
| printWidth
=====================================input======================================
const A = <T,>() => {}
=====================================output=====================================
const A = <T,>() => {};
================================================================================
`;
exports[`snippet: unnamed format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
| printWidth
=====================================input======================================
const A = <T,>() => {}
=====================================output=====================================
const A = <T,>() => {};
================================================================================
`;
20 changes: 20 additions & 0 deletions tests/format/typescript/tsx/comma/jsfmt.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const code = "const A = <T,>() => {}";

run_spec(
{
importMeta: import.meta,
snippets: [
"test.js",
"test.cjs",
"test.mjs",
"test.ts",
"test.jsx",
"test.mts",
"test.cts",
"test.tsx",
"test.unknown",
undefined,
].map((filename) => ({ code, filename, name: filename ?? "unnamed" })),
},
["typescript"]
);
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`issue-7542.ts - {"printWidth":120} format 1`] = `
exports[`issue-7542.tsx - {"printWidth":120} format 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 120
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/__tests__/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ test("typescript parser should throw the first error when both JSX and non-JSX m
label:
`;
await expect(
prettier.format(input, { parser: "typescript" })
prettier.format(input, { parser: "typescript", filepath: "foo.unknown" })
).rejects.toThrowErrorMatchingSnapshot();
});

Expand Down

0 comments on commit cfb3b87

Please sign in to comment.