fix(formatter): keep parens around a generic function type in a type assertion (#28173)

A generic function type right after the `<` of a type assertion is
printed without parens, so the output starts with `<<` and no longer
parses:

```ts
// input (parses)
var a = < <T>(x: T) => T>f;
var g = < <T>() => T>< <U>() => U>h;

// main (tsc: "Expression expected")
var a = <<T>(x: T) => T>f;
var g = <<T>() => T>(<<U>() => U>h);
```

In an expression, `<<` is read as a left shift, so the type assertion is
gone. Prettier 3.9.10 prints the same, so this follows Prettier into
invalid code. `TSFunctionType::needs_parentheses` now returns true for a
function type with type parameters whose parent is a `TSTypeAssertion`:

```ts
var a = <(<T>(x: T) => T)>f;
var g = <(<T>() => T)>(<(<U>() => U)>h);
```

Only that position needs it. In type arguments TypeScript re-scans `<<`
as two tokens, so `Array<<T>() => T>`, `f<<T>() => T>()`, `new C<<T>()
=> T>()` and an instantiation expression all parse and keep their
output. A function type without type parameters, a constructor type
(`<new <T>() => T>f`) and a function type nested inside another type
don't start with `<` and don't change.

This is a divergence from the pin, so it has a
`DIVERGENCES.md#type-assertion-generic-function-parens` entry, under
`semantics (Prettier's output no longer parses)`, and a pin fixture.

Tests:
- New fixture
`ts/parenthesis/type-assertion-generic-function-parens.ts`. It fails on
`main`. Its output matches Prettier 3.9.10 at both option sets except
the three statements the fixture marks as deviating, and every output
parses with tsc 6.0.3.
- On a 16,157-file corpus (Biome's JS/TS specs, the TypeScript compiler
tests, node_modules sources), only `genericTypeAssertions3.ts` and
`typeAssertionToGenericFunctionType.ts` format differently, and their
output now parses (2 fewer parse failures). Nothing else changes.
- `tasks/coverage/snapshots/formatter_typescript.snap`:
`genericTypeAssertions3.ts` and `typeAssertionToGenericFunctionType.ts`
now pass (9850/9862, was 9848); they failed with "Unexpected token"
because the formatted output no longer parsed.
- `cargo test -p oxc_formatter` (fixtures and both Prettier conformance
reports unchanged), `cargo c -p oxc_formatter --features
detect_code_removal`, `just lint -p oxc_formatter`, `cargo fmt --check`
and `typos` pass.

AI disclosure: this PR (code, tests and description) was written with
Claude Code (Claude Opus 5.5); the verification above is what was run.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: leaysgur <6259812+leaysgur@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-27 08:23:45 +00:00
1 parent e2be72dfea
commit f00862bbf3
9 files changed
+162 -6

No files matched your search

+32
View File
@@ -897,6 +897,38 @@ Prettier also drops the parens around an operand that binds looser than the type
The type arguments then attach to its last piece only (`c ? a : b<T>`, `a as B<T>`), or the output no longer parses (`y++<T>`).
Fixed in Prettier `main` by prettier/prettier#20345, which we follow.
## generic-function-type-after-lt-parens
- Why: semantics (Prettier's output no longer parses)
- Pin: `tests/fixtures/ts/parenthesis/generic-function-type-after-lt-parens.ts`, `tests/fixtures/ts/parenthesis/generic-function-type-after-lt-parens-jsx.tsx`
```ts
// input
var a = < <T>(x: T) => T>f;
var b = <(<T>() => void)>f;
class A extends B< <T>() => T> {}
type G = typeof g< <T>() => T>;
// ours
var a = <(<T>(x: T) => T)>f;
var b = <(<T>() => void)>f;
class A extends B<(<T>() => T)> {}
type G = typeof g<(<T>() => T)>;
// prettier
var a = <<T>(x: T) => T>f;
var b = <<T>() => void>f;
class A extends B<<T>() => T> {}
type G = typeof g<<T>() => T>;
```
A generic function type printed right after a `<` starts with `<<`, which is read as a left shift.
tsc splits it back in most type arguments (`Array<<T>() => T>`, `f<<T>() => T>()`),
but not after the `<` of a type assertion or of the first type argument of a heritage clause (`extends`, `implements`), `typeof` or a JSX element,
so it rejects Prettier's output there.
We keep the type in parens in those positions, as Prettier does for a function type in other ambiguous positions (a union member, an arrow function's return type).
A function type without type parameters, a constructor type (`<new <T>() => T>f`) and a function type nested inside another type don't start with `<` and print as before.
## export-default-leftmost-parens
- Why: uniform-rule (same construct, same output: expression statement)
@@ -58,6 +58,29 @@ impl NeedsParentheses<'_> for AstNode<'_, '_, TSFunctionType<'_>> {
return true;
}
// A generic function type after these `<`s would print `<<`, which TypeScript reads as a left shift;
// only these owners' type arguments are parsed without splitting it back (`tryParseTypeArguments`).
// Containment, since `parent` looks through a single-member union.
// (DIVERGENCES.md#generic-function-type-after-lt-parens)
if self.type_parameters.is_some()
&& match parent {
AstNodes::TSTypeAssertion(_) => true,
AstNodes::TSTypeParameterInstantiation(args) => {
matches!(
args.parent(),
AstNodes::Class(_)
| AstNodes::TSClassImplements(_)
| AstNodes::TSInterfaceHeritage(_)
| AstNodes::TSTypeQuery(_)
| AstNodes::JSXOpeningElement(_)
) && args.params[0].span().contains_inclusive(self.span)
}
_ => false,
}
{
return true;
}
function_like_type_needs_parentheses(self.span(), parent, Some(&self.return_type))
}
}
@@ -99,6 +99,9 @@ const IGNORE: &[&str] = &[
"js/call/invalid",
// Trailing comma after rest element
"typescript/trailing-comma/invalid.ts",
// `<<` right after a heritage clause's or JSX element's type argument `<`
"typescript/type-arguments-bit-shift-left-like/3.ts",
"typescript/type-arguments-bit-shift-left-like/5.tsx",
// Invalid modifier combos (`readonly accessor`, optional accessor, ambient initializer)
"typescript/decorator-auto-accessors/decorator-auto-accessors-abstract-class.ts",
"typescript/decorator-auto-accessors/decorator-auto-accessors-declare-class.ts",
@@ -0,0 +1,2 @@
// Deviates from Prettier (DIVERGENCES.md#generic-function-type-after-lt-parens).
<C< <T>() => T> />;
@@ -0,0 +1,21 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
// Deviates from Prettier (DIVERGENCES.md#generic-function-type-after-lt-parens).
<C< <T>() => T> />;
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// Deviates from Prettier (DIVERGENCES.md#generic-function-type-after-lt-parens).
<C<(<T>() => T)> />;
-------------------
{ printWidth: 100 }
-------------------
// Deviates from Prettier (DIVERGENCES.md#generic-function-type-after-lt-parens).
<C<(<T>() => T)> />;
===================== End =====================
@@ -0,0 +1,16 @@
// The first seven statements deviate from Prettier (DIVERGENCES.md#generic-function-type-after-lt-parens).
var a = < <T>(x: T) => T>f;
var b = <(<T>() => void)>f;
var c = < <T>(x: T) => T>((x) => {
return null;
});
class A extends B< <T>() => T> {}
class C implements D< <T>() => T> {}
interface E extends F< <T>() => T> {}
type G = typeof g< <T>() => T>;
// No parens: no type parameters, a `new` type, a nested function type, not the first type argument, or a `<` TypeScript splits
var d = <() => void>f;
var e = <new <T>() => T>f;
var g = <Map<string, <T>() => T>>f;
class H extends B<X, <T>() => T> {}
type I = Array< <T>() => T>;
@@ -0,0 +1,63 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
// The first seven statements deviate from Prettier (DIVERGENCES.md#generic-function-type-after-lt-parens).
var a = < <T>(x: T) => T>f;
var b = <(<T>() => void)>f;
var c = < <T>(x: T) => T>((x) => {
return null;
});
class A extends B< <T>() => T> {}
class C implements D< <T>() => T> {}
interface E extends F< <T>() => T> {}
type G = typeof g< <T>() => T>;
// No parens: no type parameters, a `new` type, a nested function type, not the first type argument, or a `<` TypeScript splits
var d = <() => void>f;
var e = <new <T>() => T>f;
var g = <Map<string, <T>() => T>>f;
class H extends B<X, <T>() => T> {}
type I = Array< <T>() => T>;
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// The first seven statements deviate from Prettier (DIVERGENCES.md#generic-function-type-after-lt-parens).
var a = <(<T>(x: T) => T)>f;
var b = <(<T>() => void)>f;
var c = <(<T>(x: T) => T)>((x) => {
return null;
});
class A extends B<(<T>() => T)> {}
class C implements D<(<T>() => T)> {}
interface E extends F<(<T>() => T)> {}
type G = typeof g<(<T>() => T)>;
// No parens: no type parameters, a `new` type, a nested function type, not the first type argument, or a `<` TypeScript splits
var d = <() => void>f;
var e = <new <T>() => T>f;
var g = <Map<string, <T>() => T>>f;
class H extends B<X, <T>() => T> {}
type I = Array<<T>() => T>;
-------------------
{ printWidth: 100 }
-------------------
// The first seven statements deviate from Prettier (DIVERGENCES.md#generic-function-type-after-lt-parens).
var a = <(<T>(x: T) => T)>f;
var b = <(<T>() => void)>f;
var c = <(<T>(x: T) => T)>((x) => {
return null;
});
class A extends B<(<T>() => T)> {}
class C implements D<(<T>() => T)> {}
interface E extends F<(<T>() => T)> {}
type G = typeof g<(<T>() => T)>;
// No parens: no type parameters, a `new` type, a nested function type, not the first type argument, or a `<` TypeScript splits
var d = <() => void>f;
var e = <new <T>() => T>f;
var g = <Map<string, <T>() => T>>f;
class H extends B<X, <T>() => T> {}
type I = Array<<T>() => T>;
===================== End =====================
@@ -2,7 +2,7 @@
source: crates/oxc_formatter/tests/conformance.rs
expression: report
---
ts compatibility: 754/819 (92.06%), 16 files skipped
ts compatibility: 752/817 (92.04%), 16 files skipped
# Failed
@@ -2,23 +2,19 @@ commit: 5237fd46
formatter_typescript Summary:
AST Parsed : 9862/9862 (100.00%)
Positive Passed: 9848/9862 (99.86%)
Positive Passed: 9850/9862 (99.88%)
Mismatch: tasks/coverage/typescript/tsc/testdata/tests/cases/compiler/amdLikeInputDeclarationEmit.ts
Mismatch: tasks/coverage/typescript/tsc/testdata/tests/cases/compiler/commentsAfterSpread.ts
Expect to Parse: tasks/coverage/typescript/tsc/testdata/tests/cases/compiler/controlFlowAliasedDiscriminants.ts
An implementation cannot be declared in ambient contexts.
Expect to Parse: tasks/coverage/typescript/tsc/testdata/tests/cases/compiler/genericTypeAssertions3.ts
Unexpected token
Expect to Parse: tasks/coverage/typescript/tsc/testdata/tests/cases/compiler/letDeclarations-scopes2.ts
Expected `{` but found `Identifier`
Mismatch: tasks/coverage/typescript/tsc/testdata/tests/cases/compiler/parenthesizedExpressionInternalComments.ts
Expect to Parse: tasks/coverage/typescript/tsc/testdata/tests/cases/compiler/reverseMappedTypeInferenceSameSource1.ts
Initializers are not allowed in ambient contexts.
Expect to Parse: tasks/coverage/typescript/tsc/testdata/tests/cases/compiler/typeAssertionToGenericFunctionType.ts
Unexpected token
Expect to Parse: tasks/coverage/typescript/tsc/testdata/tests/cases/conformance/controlFlow/controlFlowAssignmentPatternOrder.ts
Initializers are not allowed in ambient contexts.
Mismatch: tasks/coverage/typescript/tsc/testdata/tests/cases/conformance/esDecorators/classExpression/esDecorators-classExpression-commentPreservation.ts