Skip to content

Commit fd23885

Browse files
authored
Avoid infinite recursion with inferReverseMappedType (#57837)
1 parent ce21314 commit fd23885

7 files changed

+207
-25
lines changed

src/compiler/checker.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2159,7 +2159,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
21592159
/** Key is "/path/to/a.ts|/path/to/b.ts". */
21602160
var amalgamatedDuplicates: Map<string, DuplicateInfoForFiles> | undefined;
21612161
var reverseMappedCache = new Map<string, Type | undefined>();
2162-
var homomorphicMappedTypeInferenceStack: string[] = [];
21632162
var ambientModulesCache: Symbol[] | undefined;
21642163
/**
21652164
* List of every ambient module with a "*" wildcard.
@@ -2277,6 +2276,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
22772276
var potentialReflectCollisions: Node[] = [];
22782277
var potentialUnusedRenamedBindingElementsInTypes: BindingElement[] = [];
22792278
var awaitedTypeStack: number[] = [];
2279+
var reverseMappedSourceStack: Type[] = [];
2280+
var reverseMappedTargetStack: Type[] = [];
2281+
var reverseExpandingFlags = ExpandingFlags.None;
22802282

22812283
var diagnostics = createDiagnosticCollection();
22822284
var suggestionDiagnostics = createDiagnosticCollection();
@@ -13583,7 +13585,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1358313585
const modifiers = getMappedTypeModifiers(type.mappedType);
1358413586
const readonlyMask = modifiers & MappedTypeModifiers.IncludeReadonly ? false : true;
1358513587
const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : SymbolFlags.Optional;
13586-
const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType), readonlyMask && indexInfo.isReadonly)] : emptyArray;
13588+
const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType) || unknownType, readonlyMask && indexInfo.isReadonly)] : emptyArray;
1358713589
const members = createSymbolTable();
1358813590
const limitedConstraint = getLimitedConstraint(type);
1358913591
for (const prop of getPropertiesOfType(type.source)) {
@@ -25102,13 +25104,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2510225104
if (reverseMappedCache.has(cacheKey)) {
2510325105
return reverseMappedCache.get(cacheKey);
2510425106
}
25105-
const recursionKey = source.id + "," + (target.target || target).id;
25106-
if (contains(homomorphicMappedTypeInferenceStack, recursionKey)) {
25107-
return undefined;
25108-
}
25109-
homomorphicMappedTypeInferenceStack.push(recursionKey);
2511025107
const type = createReverseMappedType(source, target, constraint);
25111-
homomorphicMappedTypeInferenceStack.pop();
2511225108
reverseMappedCache.set(cacheKey, type);
2511325109
return type;
2511425110
}
@@ -25132,10 +25128,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2513225128
// For arrays and tuples we infer new arrays and tuples where the reverse mapping has been
2513325129
// applied to the element type(s).
2513425130
if (isArrayType(source)) {
25135-
return createArrayType(inferReverseMappedType(getTypeArguments(source)[0], target, constraint), isReadonlyArrayType(source));
25131+
const elementType = inferReverseMappedType(getTypeArguments(source)[0], target, constraint);
25132+
if (!elementType) {
25133+
return undefined;
25134+
}
25135+
return createArrayType(elementType, isReadonlyArrayType(source));
2513625136
}
2513725137
if (isTupleType(source)) {
2513825138
const elementTypes = map(getElementTypes(source), t => inferReverseMappedType(t, target, constraint));
25139+
if (!every(elementTypes, (t): t is Type => !!t)) {
25140+
return undefined;
25141+
}
2513925142
const elementFlags = getMappedTypeModifiers(target) & MappedTypeModifiers.IncludeOptional ?
2514025143
sameMap(source.target.elementFlags, f => f & ElementFlags.Optional ? ElementFlags.Required : f) :
2514125144
source.target.elementFlags;
@@ -25150,22 +25153,43 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2515025153
return reversed;
2515125154
}
2515225155

25153-
function getTypeOfReverseMappedSymbol(symbol: ReverseMappedSymbol) {
25156+
function getTypeOfReverseMappedSymbol(symbol: ReverseMappedSymbol): Type {
2515425157
const links = getSymbolLinks(symbol);
2515525158
if (!links.type) {
25156-
links.type = inferReverseMappedType(symbol.links.propertyType, symbol.links.mappedType, symbol.links.constraintType);
25159+
links.type = inferReverseMappedType(symbol.links.propertyType, symbol.links.mappedType, symbol.links.constraintType) || unknownType;
2515725160
}
2515825161
return links.type;
2515925162
}
2516025163

25161-
function inferReverseMappedType(sourceType: Type, target: MappedType, constraint: IndexType): Type {
25164+
function inferReverseMappedTypeWorker(sourceType: Type, target: MappedType, constraint: IndexType): Type {
2516225165
const typeParameter = getIndexedAccessType(constraint.type, getTypeParameterFromMappedType(target)) as TypeParameter;
2516325166
const templateType = getTemplateTypeFromMappedType(target);
2516425167
const inference = createInferenceInfo(typeParameter);
2516525168
inferTypes([inference], sourceType, templateType);
2516625169
return getTypeFromInference(inference) || unknownType;
2516725170
}
2516825171

25172+
function inferReverseMappedType(source: Type, target: MappedType, constraint: IndexType): Type | undefined {
25173+
const cacheKey = source.id + "," + target.id + "," + constraint.id;
25174+
if (reverseMappedCache.has(cacheKey)) {
25175+
return reverseMappedCache.get(cacheKey) || unknownType;
25176+
}
25177+
reverseMappedSourceStack.push(source);
25178+
reverseMappedTargetStack.push(target);
25179+
const saveExpandingFlags = reverseExpandingFlags;
25180+
if (isDeeplyNestedType(source, reverseMappedSourceStack, reverseMappedSourceStack.length, 2)) reverseExpandingFlags |= ExpandingFlags.Source;
25181+
if (isDeeplyNestedType(target, reverseMappedTargetStack, reverseMappedTargetStack.length, 2)) reverseExpandingFlags |= ExpandingFlags.Target;
25182+
let type;
25183+
if (reverseExpandingFlags !== ExpandingFlags.Both) {
25184+
type = inferReverseMappedTypeWorker(source, target, constraint);
25185+
}
25186+
reverseMappedSourceStack.pop();
25187+
reverseMappedTargetStack.pop();
25188+
reverseExpandingFlags = saveExpandingFlags;
25189+
reverseMappedCache.set(cacheKey, type);
25190+
return type;
25191+
}
25192+
2516925193
function* getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): IterableIterator<Symbol> {
2517025194
const properties = getPropertiesOfType(target);
2517125195
for (const targetProp of properties) {

tests/baselines/reference/mappedTypeRecursiveInference.errors.txt

Lines changed: 10 additions & 8 deletions
Large diffs are not rendered by default.

tests/baselines/reference/mappedTypeRecursiveInference.types

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
//// [tests/cases/compiler/mappedTypeRecursiveInference.ts] ////
22

33
=== Performance Stats ===
4-
Strict subtype cache: 300 / 300 (nearest 100)
5-
Assignability cache: 5,000 / 5,000 (nearest 100)
6-
Type Count: 15,000 / 15,000 (nearest 100)
7-
Instantiation count: 486,000 / 503,500 (nearest 500)
8-
Symbol count: 174,500 / 177,000 (nearest 500)
4+
Strict subtype cache: 100 / 100 (nearest 100)
5+
Assignability cache: 4,100 / 4,100 (nearest 100)
6+
Type Count: 12,200 / 12,200 (nearest 100)
7+
Instantiation count: 341,000 / 358,500 (nearest 500)
8+
Symbol count: 117,500 / 120,000 (nearest 500)
99

1010
=== mappedTypeRecursiveInference.ts ===
1111
interface A { a: A }
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//// [tests/cases/compiler/reverseMappedTypeRecursiveInference.ts] ////
2+
3+
//// [reverseMappedTypeRecursiveInference.ts]
4+
type Foo<V> = {
5+
[K in keyof V]: Foo<V[K]>;
6+
}
7+
8+
type Bar<V> = {
9+
[K in keyof V]: V[K] extends object ? Bar<V[K]> : string;
10+
}
11+
12+
function test<V>(value: Foo<V>): V {
13+
console.log(value);
14+
return undefined as any;
15+
}
16+
17+
const bar: Bar<any> = {};
18+
19+
test(bar);
20+
21+
//// [reverseMappedTypeRecursiveInference.js]
22+
"use strict";
23+
function test(value) {
24+
console.log(value);
25+
return undefined;
26+
}
27+
var bar = {};
28+
test(bar);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//// [tests/cases/compiler/reverseMappedTypeRecursiveInference.ts] ////
2+
3+
=== reverseMappedTypeRecursiveInference.ts ===
4+
type Foo<V> = {
5+
>Foo : Symbol(Foo, Decl(reverseMappedTypeRecursiveInference.ts, 0, 0))
6+
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 0, 9))
7+
8+
[K in keyof V]: Foo<V[K]>;
9+
>K : Symbol(K, Decl(reverseMappedTypeRecursiveInference.ts, 1, 5))
10+
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 0, 9))
11+
>Foo : Symbol(Foo, Decl(reverseMappedTypeRecursiveInference.ts, 0, 0))
12+
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 0, 9))
13+
>K : Symbol(K, Decl(reverseMappedTypeRecursiveInference.ts, 1, 5))
14+
}
15+
16+
type Bar<V> = {
17+
>Bar : Symbol(Bar, Decl(reverseMappedTypeRecursiveInference.ts, 2, 1))
18+
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 4, 9))
19+
20+
[K in keyof V]: V[K] extends object ? Bar<V[K]> : string;
21+
>K : Symbol(K, Decl(reverseMappedTypeRecursiveInference.ts, 5, 5))
22+
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 4, 9))
23+
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 4, 9))
24+
>K : Symbol(K, Decl(reverseMappedTypeRecursiveInference.ts, 5, 5))
25+
>Bar : Symbol(Bar, Decl(reverseMappedTypeRecursiveInference.ts, 2, 1))
26+
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 4, 9))
27+
>K : Symbol(K, Decl(reverseMappedTypeRecursiveInference.ts, 5, 5))
28+
}
29+
30+
function test<V>(value: Foo<V>): V {
31+
>test : Symbol(test, Decl(reverseMappedTypeRecursiveInference.ts, 6, 1))
32+
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 8, 14))
33+
>value : Symbol(value, Decl(reverseMappedTypeRecursiveInference.ts, 8, 17))
34+
>Foo : Symbol(Foo, Decl(reverseMappedTypeRecursiveInference.ts, 0, 0))
35+
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 8, 14))
36+
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 8, 14))
37+
38+
console.log(value);
39+
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
40+
>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
41+
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
42+
>value : Symbol(value, Decl(reverseMappedTypeRecursiveInference.ts, 8, 17))
43+
44+
return undefined as any;
45+
>undefined : Symbol(undefined)
46+
}
47+
48+
const bar: Bar<any> = {};
49+
>bar : Symbol(bar, Decl(reverseMappedTypeRecursiveInference.ts, 13, 5))
50+
>Bar : Symbol(Bar, Decl(reverseMappedTypeRecursiveInference.ts, 2, 1))
51+
52+
test(bar);
53+
>test : Symbol(test, Decl(reverseMappedTypeRecursiveInference.ts, 6, 1))
54+
>bar : Symbol(bar, Decl(reverseMappedTypeRecursiveInference.ts, 13, 5))
55+
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//// [tests/cases/compiler/reverseMappedTypeRecursiveInference.ts] ////
2+
3+
=== reverseMappedTypeRecursiveInference.ts ===
4+
type Foo<V> = {
5+
>Foo : Foo<V>
6+
> : ^^^^^^
7+
8+
[K in keyof V]: Foo<V[K]>;
9+
}
10+
11+
type Bar<V> = {
12+
>Bar : Bar<V>
13+
> : ^^^^^^
14+
15+
[K in keyof V]: V[K] extends object ? Bar<V[K]> : string;
16+
}
17+
18+
function test<V>(value: Foo<V>): V {
19+
>test : <V>(value: Foo<V>) => V
20+
> : ^ ^^ ^^ ^^^^^
21+
>value : Foo<V>
22+
> : ^^^^^^
23+
24+
console.log(value);
25+
>console.log(value) : void
26+
> : ^^^^
27+
>console.log : (...data: any[]) => void
28+
> : ^^^^ ^^ ^^^^^^^^^
29+
>console : Console
30+
> : ^^^^^^^
31+
>log : (...data: any[]) => void
32+
> : ^^^^ ^^ ^^^^^^^^^
33+
>value : Foo<V>
34+
> : ^^^^^^
35+
36+
return undefined as any;
37+
>undefined as any : any
38+
>undefined : undefined
39+
> : ^^^^^^^^^
40+
}
41+
42+
const bar: Bar<any> = {};
43+
>bar : Bar<any>
44+
> : ^^^^^^^^
45+
>{} : {}
46+
> : ^^
47+
48+
test(bar);
49+
>test(bar) : { [x: string]: any; }
50+
> : ^^^^^^^^^^^^^^^^^^^^^
51+
>test : <V>(value: Foo<V>) => V
52+
> : ^ ^^ ^^ ^^^^^^
53+
>bar : Bar<any>
54+
> : ^^^^^^^^
55+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// @strict: true
2+
3+
type Foo<V> = {
4+
[K in keyof V]: Foo<V[K]>;
5+
}
6+
7+
type Bar<V> = {
8+
[K in keyof V]: V[K] extends object ? Bar<V[K]> : string;
9+
}
10+
11+
function test<V>(value: Foo<V>): V {
12+
console.log(value);
13+
return undefined as any;
14+
}
15+
16+
const bar: Bar<any> = {};
17+
18+
test(bar);

0 commit comments

Comments
 (0)