mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
9 files changed
+162
-6
No files matched your search
@@ -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",
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
// Deviates from Prettier (DIVERGENCES.md#generic-function-type-after-lt-parens).
|
||||
<C< <T>() => T> />;
|
||||
+21
@@ -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 =====================
|
||||
Vendored
+16
@@ -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>;
|
||||
Vendored
+63
@@ -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
|
||||
|
||||
Reference in new issue
Block a user