Skip to content

Commit 15dee00

Browse files
authored
Detect cycles while serializing array and tuple types (#64556)
1 parent e8fd69f commit 15dee00

15 files changed

Lines changed: 1628 additions & 360 deletions

‎tsc/internal/checker/nodebuilderimpl.go‎

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ type SerializedTypeEntry struct {
4141
}
4242

4343
type CompositeTypeCacheIdentity struct {
44-
typeId TypeId
45-
flags nodebuilder.Flags
46-
internalFlags nodebuilder.InternalFlags
44+
typeId TypeId
45+
flags nodebuilder.Flags
46+
internalFlags nodebuilder.InternalFlags
47+
inferTypeParameters CacheHashKey
4748
}
4849

4950
type NodeBuilderLinks struct {
@@ -3084,7 +3085,7 @@ func (b *NodeBuilderImpl) getParentSymbolOfTypeParameter(typeParameter *TypePara
30843085
return b.ch.getSymbolOfNode(host)
30853086
}
30863087

3087-
func (b *NodeBuilderImpl) typeReferenceToTypeNode(t *Type) *ast.TypeNode {
3088+
func (b *NodeBuilderImpl) arrayOrTupleTypeToNode(t *Type) *ast.TypeNode {
30883089
var typeArguments []*Type = b.ch.getTypeArguments(t)
30893090
if t.Target() == b.ch.globalArrayType || t.Target() == b.ch.globalReadonlyArrayType {
30903091
if b.ctx.flags&nodebuilder.FlagsWriteArrayAsGenericType != 0 {
@@ -3101,7 +3102,8 @@ func (b *NodeBuilderImpl) typeReferenceToTypeNode(t *Type) *ast.TypeNode {
31013102
} else {
31023103
return b.f.NewTypeOperatorNode(ast.KindReadonlyKeyword, arrayType)
31033104
}
3104-
} else if t.Target().objectFlags&ObjectFlagsTuple != 0 {
3105+
} else {
3106+
debug.Assert(t.Target().objectFlags&ObjectFlagsTuple != 0)
31053107
typeArguments = core.SameMapIndex(typeArguments, func(arg *Type, i int) *Type {
31063108
isOptional := false
31073109
if i < len(t.Target().AsTupleType().elementInfos) {
@@ -3154,7 +3156,12 @@ func (b *NodeBuilderImpl) typeReferenceToTypeNode(t *Type) *ast.TypeNode {
31543156
b.ctx.encounteredError = true
31553157
return nil
31563158
// TODO: GH#18217
3157-
} else if b.ctx.flags&nodebuilder.FlagsWriteClassExpressionAsTypeLiteral != 0 && t.symbol.ValueDeclaration != nil && ast.IsClassLike(t.symbol.ValueDeclaration) && !b.ch.IsValueSymbolAccessible(t.symbol, b.ctx.enclosingDeclaration) {
3159+
}
3160+
}
3161+
3162+
func (b *NodeBuilderImpl) typeReferenceToTypeNode(t *Type) *ast.TypeNode {
3163+
var typeArguments []*Type = b.ch.getTypeArguments(t)
3164+
if b.ctx.flags&nodebuilder.FlagsWriteClassExpressionAsTypeLiteral != 0 && t.symbol.ValueDeclaration != nil && ast.IsClassLike(t.symbol.ValueDeclaration) && !b.ch.IsValueSymbolAccessible(t.symbol, b.ctx.enclosingDeclaration) {
31583165
return b.createAnonymousTypeNode(t)
31593166
} else {
31603167
outerTypeParameters := t.Target().AsInterfaceType().OuterTypeParameters()
@@ -3232,9 +3239,21 @@ func (b *NodeBuilderImpl) visitAndTransformType(t *Type, transform func(b *NodeB
32323239
}
32333240

32343241
typeId := t.id
3242+
isArrayOrTuple := b.ch.isArrayOrTupleType(t)
3243+
if isArrayOrTuple {
3244+
// Deferred and regular references share a cycle identity.
3245+
typeId = b.ch.createTypeReference(t.Target(), b.ch.getTypeArguments(t)).id
3246+
}
3247+
if b.ctx.visitedTypes.Has(typeId) {
3248+
return b.createCyclicStructurePlaceholder()
3249+
}
3250+
32353251
isConstructorObject := t.objectFlags&ObjectFlagsAnonymous != 0 && t.symbol != nil && t.symbol.Flags&ast.SymbolFlagsClass != 0
32363252
var id *CompositeSymbolIdentity
32373253
switch {
3254+
case isArrayOrTuple:
3255+
// Do not bound finite container nesting by the shared Array symbol or tuple origin.
3256+
id = nil
32383257
case t.objectFlags&ObjectFlagsReference != 0 && t.AsTypeReference().node != nil:
32393258
id = &CompositeSymbolIdentity{false, 0, ast.GetNodeId(t.AsTypeReference().node)}
32403259
case t.flags&TypeFlagsConditional != 0:
@@ -3247,7 +3266,14 @@ func (b *NodeBuilderImpl) visitAndTransformType(t *Type, transform func(b *NodeB
32473266
// Since instantiations of the same anonymous type have the same symbol, tracking symbols instead
32483267
// of types allows us to catch circular references to instantiations of the same anonymous type
32493268

3250-
key := CompositeTypeCacheIdentity{typeId, b.ctx.flags, b.ctx.internalFlags}
3269+
key := CompositeTypeCacheIdentity{
3270+
typeId: typeId,
3271+
flags: b.ctx.flags,
3272+
internalFlags: b.ctx.internalFlags,
3273+
}
3274+
if len(b.ctx.inferTypeParameters) != 0 {
3275+
key.inferTypeParameters = getTypeListKey(b.ctx.inferTypeParameters)
3276+
}
32513277
// Don't rely on type cache if we're expanding a type, because we need to compute `canIncreaseExpansionDepth`.
32523278
canUseCache := b.ctx.maxExpansionDepth < 0
32533279
if canUseCache && b.ctx.enclosingDeclaration != nil && b.links.Has(b.ctx.enclosingDeclaration) {
@@ -3518,7 +3544,9 @@ func (b *NodeBuilderImpl) typeToTypeNode(t *Type) *ast.TypeNode {
35183544
b.ctx.depth--
35193545
return result
35203546
}
3521-
if t.AsTypeReference().node != nil {
3547+
if b.ch.isArrayOrTupleType(t) {
3548+
return b.visitAndTransformType(t, (*NodeBuilderImpl).arrayOrTupleTypeToNode)
3549+
} else if t.AsTypeReference().node != nil {
35223550
return b.visitAndTransformType(t, (*NodeBuilderImpl).typeReferenceToTypeNode)
35233551
} else {
35243552
return b.typeReferenceToTypeNode(t)

‎tsc/internal/execute/tsctests/tsc_test.go‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2429,6 +2429,58 @@ func TestTscIncremental(t *testing.T) {
24292429
noChange,
24302430
},
24312431
},
2432+
{
2433+
subScenario: "recursive tagged tuple after incremental edits",
2434+
files: FileMap{
2435+
"/home/src/workspaces/project/tsconfig.json": `{"compilerOptions": {"strict": true, "incremental": true, "noEmit": true, "module": "esnext", "moduleResolution": "bundler"}}`,
2436+
tscLibPath + "/lib.es2026.full.d.ts": libWithReadonlyArray,
2437+
"/home/src/workspaces/project/doc.ts": stringtestutil.Dedent(`
2438+
type Doc =
2439+
| string
2440+
| { [k: string]: Doc }
2441+
| readonly ["array", Doc]
2442+
| readonly ["array", Doc, { length: number }]
2443+
| readonly ["array", Doc, { min?: number; max?: number }]
2444+
| readonly ["union", Doc, ...Doc[]];
2445+
export declare const doc: Doc;
2446+
`),
2447+
"/home/src/workspaces/project/consumer.ts": stringtestutil.Dedent(`
2448+
import { doc } from "./doc";
2449+
export const value = doc;
2450+
`),
2451+
},
2452+
edits: []*tscEdit{
2453+
noChange,
2454+
{
2455+
caption: "add a comment to the recursive type",
2456+
edit: func(sys *TestSys) {
2457+
sys.appendFile("/home/src/workspaces/project/doc.ts", "\n// comment-only edit\n")
2458+
},
2459+
},
2460+
noChange,
2461+
{
2462+
caption: "add a union constituent",
2463+
edit: func(sys *TestSys) {
2464+
sys.replaceFileText("/home/src/workspaces/project/doc.ts", "| string", "| number\n | string")
2465+
},
2466+
},
2467+
noChange,
2468+
{
2469+
caption: "verify the consumer type was not weakened",
2470+
edit: func(sys *TestSys) {
2471+
sys.appendFile("/home/src/workspaces/project/consumer.ts", "\nexport const invalid: number = value;\n")
2472+
},
2473+
},
2474+
noChange,
2475+
{
2476+
caption: "delete build info and check the edited source afresh",
2477+
edit: func(sys *TestSys) {
2478+
sys.removeNoError("/home/src/workspaces/project/tsconfig.tsbuildinfo")
2479+
},
2480+
},
2481+
noChange,
2482+
},
2483+
},
24322484
{
24332485
subScenario: "json module diagnostics are cleared after fixing the json file",
24342486
files: FileMap{

‎tsc/internal/fourslash/tests/quickInfoCanBeTruncated_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,5 +536,5 @@ type DeeplyMapped/*6*/ = {[K in keyof Foo]: {[K2 in keyof Foo]: [K, K2, Foo[K],
536536
f.GoToMarker(t, "5")
537537
f.VerifyQuickInfoIs(t, "type LongTuple = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17.18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70]", "")
538538
f.GoToMarker(t, "6")
539-
f.VerifyQuickInfoIs(t, "type DeeplyMapped = {\n _0: {\n _0: [\"_0\", \"_0\", 0, 0];\n _1: [\"_0\", \"_1\", 0, 1];\n _2: [\"_0\", \"_2\", 0, 2];\n _3: [\"_0\", \"_3\", 0, 3];\n _4: [\"_0\", \"_4\", 0, 4];\n _5: [\"_0\", \"_5\", 0, 5];\n _6: [\"_0\", \"_6\", 0, 6];\n _7: [\"_0\", \"_7\", 0, 7];\n _8: [\"_0\", \"_8\", 0, 8];\n _9: [\"_0\", \"_9\", 0, 9];\n _10: [\"_0\", \"_10\", 0, 10];\n _11: [\"_0\", \"_11\", 0, 11];\n _12: [\"_0\", \"_12\", 0, 12];\n _13: [\"_0\", \"_13\", 0, 13];\n _14: [\"_0\", \"_14\", 0, 14];\n _15: [\"_0\", \"_15\", 0, 15];\n _16: [\"_0\", \"_16\", 0, 16];\n _17: [\"_0\", \"_17\", 0, 17];\n _18: [\"_0\", \"_18\", 0, 18];\n _19: [\"_0\", \"_19\", 0, 19];\n _20: [\"_0\", \"_20\", 0, 20];\n _21: [\"_0\", \"_21\", 0, 21];\n ... 477 more ...;\n _499: [...];\n };\n ... 498 more ...;\n _499: ...;\n}", "")
539+
f.VerifyQuickInfoIs(t, "type DeeplyMapped = {\n _0: {\n _0: [\"_0\", \"_0\", 0, 0];\n _1: [\"_0\", \"_1\", 0, 1];\n _2: [\"_0\", \"_2\", 0, 2];\n _3: [\"_0\", \"_3\", 0, 3];\n _4: [\"_0\", \"_4\", 0, 4];\n _5: [\"_0\", \"_5\", 0, 5];\n _6: [\"_0\", \"_6\", 0, 6];\n _7: [\"_0\", \"_7\", 0, 7];\n _8: [\"_0\", \"_8\", 0, 8];\n _9: [\"_0\", \"_9\", 0, 9];\n _10: [\"_0\", \"_10\", 0, 10];\n _11: [\"_0\", \"_11\", 0, 11];\n _12: [\"_0\", \"_12\", 0, 12];\n _13: [\"_0\", \"_13\", 0, 13];\n _14: [\"_0\", \"_14\", 0, 14];\n _15: [\"_0\", \"_15\", 0, 15];\n _16: [\"_0\", \"_16\", 0, 16];\n _17: [\"_0\", \"_17\", 0, 17];\n _18: [\"_0\", \"_18\", 0, 18];\n _19: [\"_0\", \"_19\", 0, 19];\n _20: [\"_0\", \"_20\", 0, 20];\n _21: [\"_0\", \"_21\", 0, 21];\n ... 477 more ...;\n _499: ...;\n };\n ... 498 more ...;\n _499: ...;\n}", "")
540540
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
local.ts(1,17): error TS5088: The inferred type of 'array' references a type with a cyclic structure which cannot be trivially serialized. A type annotation is necessary.
2+
local.ts(5,17): error TS5088: The inferred type of 'tuple' references a type with a cyclic structure which cannot be trivially serialized. A type annotation is necessary.
3+
local.ts(9,17): error TS5088: The inferred type of 'readonlyTuple' references a type with a cyclic structure which cannot be trivially serialized. A type annotation is necessary.
4+
local.ts(13,17): error TS5088: The inferred type of 'union' references a type with a cyclic structure which cannot be trivially serialized. A type annotation is necessary.
5+
6+
7+
==== local.ts (4 errors) ====
8+
export function array() {
9+
~~~~~
10+
!!! error TS5088: The inferred type of 'array' references a type with a cyclic structure which cannot be trivially serialized. A type annotation is necessary.
11+
type Recursive = Recursive[];
12+
return null as unknown as Recursive;
13+
}
14+
export function tuple() {
15+
~~~~~
16+
!!! error TS5088: The inferred type of 'tuple' references a type with a cyclic structure which cannot be trivially serialized. A type annotation is necessary.
17+
type Recursive = [Recursive];
18+
return null as unknown as Recursive;
19+
}
20+
export function readonlyTuple() {
21+
~~~~~~~~~~~~~
22+
!!! error TS5088: The inferred type of 'readonlyTuple' references a type with a cyclic structure which cannot be trivially serialized. A type annotation is necessary.
23+
type Recursive = readonly [Recursive];
24+
return null as unknown as Recursive;
25+
}
26+
export function union() {
27+
~~~~~
28+
!!! error TS5088: The inferred type of 'union' references a type with a cyclic structure which cannot be trivially serialized. A type annotation is necessary.
29+
type Recursive = string | Recursive[];
30+
return null as unknown as Recursive;
31+
}
32+
33+
==== nameable.ts (0 errors) ====
34+
export type RecursiveArray = RecursiveArray[];
35+
export type RecursiveTuple = readonly [RecursiveTuple];
36+
declare const array: RecursiveArray;
37+
declare const tuple: RecursiveTuple;
38+
export const namedArray = array;
39+
export const namedTuple = tuple;
40+
export function finite() {
41+
type Nested = [[[[[[[[[[[[[number]]]]]]]]]]]]];
42+
return null as unknown as Nested;
43+
}
44+
export function finiteArray() {
45+
type Nested = number[][][][][][][][][][][][][];
46+
return null as unknown as Nested;
47+
}
48+
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//// [tests/cases/compiler/recursiveContainerDeclarationSerialization.ts] ////
2+
3+
//// [local.ts]
4+
export function array() {
5+
type Recursive = Recursive[];
6+
return null as unknown as Recursive;
7+
}
8+
export function tuple() {
9+
type Recursive = [Recursive];
10+
return null as unknown as Recursive;
11+
}
12+
export function readonlyTuple() {
13+
type Recursive = readonly [Recursive];
14+
return null as unknown as Recursive;
15+
}
16+
export function union() {
17+
type Recursive = string | Recursive[];
18+
return null as unknown as Recursive;
19+
}
20+
21+
//// [nameable.ts]
22+
export type RecursiveArray = RecursiveArray[];
23+
export type RecursiveTuple = readonly [RecursiveTuple];
24+
declare const array: RecursiveArray;
25+
declare const tuple: RecursiveTuple;
26+
export const namedArray = array;
27+
export const namedTuple = tuple;
28+
export function finite() {
29+
type Nested = [[[[[[[[[[[[[number]]]]]]]]]]]]];
30+
return null as unknown as Nested;
31+
}
32+
export function finiteArray() {
33+
type Nested = number[][][][][][][][][][][][][];
34+
return null as unknown as Nested;
35+
}
36+
37+
38+
//// [local.js]
39+
export function array() {
40+
return null;
41+
}
42+
export function tuple() {
43+
return null;
44+
}
45+
export function readonlyTuple() {
46+
return null;
47+
}
48+
export function union() {
49+
return null;
50+
}
51+
//// [nameable.js]
52+
export const namedArray = array;
53+
export const namedTuple = tuple;
54+
export function finite() {
55+
return null;
56+
}
57+
export function finiteArray() {
58+
return null;
59+
}
60+
61+
62+
//// [nameable.d.ts]
63+
export type RecursiveArray = RecursiveArray[];
64+
export type RecursiveTuple = readonly [RecursiveTuple];
65+
export declare const namedArray: RecursiveArray;
66+
export declare const namedTuple: RecursiveTuple;
67+
export declare function finite(): [[[[[[[[[[[[[number]]]]]]]]]]]]];
68+
export declare function finiteArray(): number[][][][][][][][][][][][][];
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//// [tests/cases/compiler/recursiveContainerDeclarationSerialization.ts] ////
2+
3+
=== local.ts ===
4+
export function array() {
5+
>array : Symbol(array, Decl(local.ts, 0, 0))
6+
7+
type Recursive = Recursive[];
8+
>Recursive : Symbol(Recursive, Decl(local.ts, 0, 25))
9+
>Recursive : Symbol(Recursive, Decl(local.ts, 0, 25))
10+
11+
return null as unknown as Recursive;
12+
>Recursive : Symbol(Recursive, Decl(local.ts, 0, 25))
13+
}
14+
export function tuple() {
15+
>tuple : Symbol(tuple, Decl(local.ts, 3, 1))
16+
17+
type Recursive = [Recursive];
18+
>Recursive : Symbol(Recursive, Decl(local.ts, 4, 25))
19+
>Recursive : Symbol(Recursive, Decl(local.ts, 4, 25))
20+
21+
return null as unknown as Recursive;
22+
>Recursive : Symbol(Recursive, Decl(local.ts, 4, 25))
23+
}
24+
export function readonlyTuple() {
25+
>readonlyTuple : Symbol(readonlyTuple, Decl(local.ts, 7, 1))
26+
27+
type Recursive = readonly [Recursive];
28+
>Recursive : Symbol(Recursive, Decl(local.ts, 8, 33))
29+
>Recursive : Symbol(Recursive, Decl(local.ts, 8, 33))
30+
31+
return null as unknown as Recursive;
32+
>Recursive : Symbol(Recursive, Decl(local.ts, 8, 33))
33+
}
34+
export function union() {
35+
>union : Symbol(union, Decl(local.ts, 11, 1))
36+
37+
type Recursive = string | Recursive[];
38+
>Recursive : Symbol(Recursive, Decl(local.ts, 12, 25))
39+
>Recursive : Symbol(Recursive, Decl(local.ts, 12, 25))
40+
41+
return null as unknown as Recursive;
42+
>Recursive : Symbol(Recursive, Decl(local.ts, 12, 25))
43+
}
44+
45+
=== nameable.ts ===
46+
export type RecursiveArray = RecursiveArray[];
47+
>RecursiveArray : Symbol(RecursiveArray, Decl(nameable.ts, 0, 0))
48+
>RecursiveArray : Symbol(RecursiveArray, Decl(nameable.ts, 0, 0))
49+
50+
export type RecursiveTuple = readonly [RecursiveTuple];
51+
>RecursiveTuple : Symbol(RecursiveTuple, Decl(nameable.ts, 0, 46))
52+
>RecursiveTuple : Symbol(RecursiveTuple, Decl(nameable.ts, 0, 46))
53+
54+
declare const array: RecursiveArray;
55+
>array : Symbol(array, Decl(nameable.ts, 2, 13))
56+
>RecursiveArray : Symbol(RecursiveArray, Decl(nameable.ts, 0, 0))
57+
58+
declare const tuple: RecursiveTuple;
59+
>tuple : Symbol(tuple, Decl(nameable.ts, 3, 13))
60+
>RecursiveTuple : Symbol(RecursiveTuple, Decl(nameable.ts, 0, 46))
61+
62+
export const namedArray = array;
63+
>namedArray : Symbol(namedArray, Decl(nameable.ts, 4, 12))
64+
>array : Symbol(array, Decl(nameable.ts, 2, 13))
65+
66+
export const namedTuple = tuple;
67+
>namedTuple : Symbol(namedTuple, Decl(nameable.ts, 5, 12))
68+
>tuple : Symbol(tuple, Decl(nameable.ts, 3, 13))
69+
70+
export function finite() {
71+
>finite : Symbol(finite, Decl(nameable.ts, 5, 32))
72+
73+
type Nested = [[[[[[[[[[[[[number]]]]]]]]]]]]];
74+
>Nested : Symbol(Nested, Decl(nameable.ts, 6, 26))
75+
76+
return null as unknown as Nested;
77+
>Nested : Symbol(Nested, Decl(nameable.ts, 6, 26))
78+
}
79+
export function finiteArray() {
80+
>finiteArray : Symbol(finiteArray, Decl(nameable.ts, 9, 1))
81+
82+
type Nested = number[][][][][][][][][][][][][];
83+
>Nested : Symbol(Nested, Decl(nameable.ts, 10, 31))
84+
85+
return null as unknown as Nested;
86+
>Nested : Symbol(Nested, Decl(nameable.ts, 10, 31))
87+
}
88+

0 commit comments

Comments
 (0)