Skip to content

Commit a22aaf0

Browse files
authored
Control flow analysis for element access with variable index (#57847)
1 parent 316f180 commit a22aaf0

7 files changed

+367
-14
lines changed

src/compiler/checker.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26622,13 +26622,20 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2662226622
return getFlowCacheKey((node as NonNullExpression | ParenthesizedExpression).expression, declaredType, initialType, flowContainer);
2662326623
case SyntaxKind.QualifiedName:
2662426624
const left = getFlowCacheKey((node as QualifiedName).left, declaredType, initialType, flowContainer);
26625-
return left && left + "." + (node as QualifiedName).right.escapedText;
26625+
return left && `${left}.${(node as QualifiedName).right.escapedText}`;
2662626626
case SyntaxKind.PropertyAccessExpression:
2662726627
case SyntaxKind.ElementAccessExpression:
2662826628
const propName = getAccessedPropertyName(node as AccessExpression);
2662926629
if (propName !== undefined) {
2663026630
const key = getFlowCacheKey((node as AccessExpression).expression, declaredType, initialType, flowContainer);
26631-
return key && key + "." + propName;
26631+
return key && `${key}.${propName}`;
26632+
}
26633+
if (isElementAccessExpression(node) && isIdentifier(node.argumentExpression)) {
26634+
const symbol = getResolvedSymbol(node.argumentExpression);
26635+
if (isConstantVariable(symbol) || isParameterOrMutableLocalVariable(symbol) && !isSymbolAssigned(symbol)) {
26636+
const key = getFlowCacheKey((node as AccessExpression).expression, declaredType, initialType, flowContainer);
26637+
return key && `${key}.@${getSymbolId(symbol)}`;
26638+
}
2663226639
}
2663326640
break;
2663426641
case SyntaxKind.ObjectBindingPattern:
@@ -26674,9 +26681,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2667426681
case SyntaxKind.PropertyAccessExpression:
2667526682
case SyntaxKind.ElementAccessExpression:
2667626683
const sourcePropertyName = getAccessedPropertyName(source as AccessExpression);
26677-
const targetPropertyName = isAccessExpression(target) ? getAccessedPropertyName(target) : undefined;
26678-
return sourcePropertyName !== undefined && targetPropertyName !== undefined && targetPropertyName === sourcePropertyName &&
26679-
isMatchingReference((source as AccessExpression).expression, (target as AccessExpression).expression);
26684+
if (sourcePropertyName !== undefined) {
26685+
const targetPropertyName = isAccessExpression(target) ? getAccessedPropertyName(target) : undefined;
26686+
if (targetPropertyName !== undefined) {
26687+
return targetPropertyName === sourcePropertyName && isMatchingReference((source as AccessExpression).expression, (target as AccessExpression).expression);
26688+
}
26689+
}
26690+
if (isElementAccessExpression(source) && isElementAccessExpression(target) && isIdentifier(source.argumentExpression) && isIdentifier(target.argumentExpression)) {
26691+
const symbol = getResolvedSymbol(source.argumentExpression);
26692+
if (symbol === getResolvedSymbol(target.argumentExpression) && (isConstantVariable(symbol) || isParameterOrMutableLocalVariable(symbol) && !isSymbolAssigned(symbol))) {
26693+
return isMatchingReference(source.expression, target.expression);
26694+
}
26695+
}
26696+
break;
2668026697
case SyntaxKind.QualifiedName:
2668126698
return isAccessExpression(target) &&
2668226699
(source as QualifiedName).right.escapedText === getAccessedPropertyName(target) &&
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//// [tests/cases/conformance/controlFlow/controlFlowComputedPropertyNames.ts] ////
2+
3+
=== controlFlowComputedPropertyNames.ts ===
4+
function f1(obj: Record<string, unknown>, key: string) {
5+
>f1 : Symbol(f1, Decl(controlFlowComputedPropertyNames.ts, 0, 0))
6+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 0, 12))
7+
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
8+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 0, 41))
9+
10+
if (typeof obj[key] === "string") {
11+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 0, 12))
12+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 0, 41))
13+
14+
obj[key].toUpperCase();
15+
>obj[key].toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
16+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 0, 12))
17+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 0, 41))
18+
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
19+
}
20+
}
21+
22+
function f2(obj: Record<string, string | undefined>, key: string) {
23+
>f2 : Symbol(f2, Decl(controlFlowComputedPropertyNames.ts, 4, 1))
24+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 6, 12))
25+
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
26+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 6, 52))
27+
28+
if (obj[key] !== undefined) {
29+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 6, 12))
30+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 6, 52))
31+
>undefined : Symbol(undefined)
32+
33+
obj[key].toUpperCase();
34+
>obj[key].toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
35+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 6, 12))
36+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 6, 52))
37+
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
38+
}
39+
let key2 = key + key;
40+
>key2 : Symbol(key2, Decl(controlFlowComputedPropertyNames.ts, 10, 7))
41+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 6, 52))
42+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 6, 52))
43+
44+
if (obj[key2] !== undefined) {
45+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 6, 12))
46+
>key2 : Symbol(key2, Decl(controlFlowComputedPropertyNames.ts, 10, 7))
47+
>undefined : Symbol(undefined)
48+
49+
obj[key2].toUpperCase();
50+
>obj[key2].toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
51+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 6, 12))
52+
>key2 : Symbol(key2, Decl(controlFlowComputedPropertyNames.ts, 10, 7))
53+
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
54+
}
55+
const key3 = key + key;
56+
>key3 : Symbol(key3, Decl(controlFlowComputedPropertyNames.ts, 14, 9))
57+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 6, 52))
58+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 6, 52))
59+
60+
if (obj[key3] !== undefined) {
61+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 6, 12))
62+
>key3 : Symbol(key3, Decl(controlFlowComputedPropertyNames.ts, 14, 9))
63+
>undefined : Symbol(undefined)
64+
65+
obj[key3].toUpperCase();
66+
>obj[key3].toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
67+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 6, 12))
68+
>key3 : Symbol(key3, Decl(controlFlowComputedPropertyNames.ts, 14, 9))
69+
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
70+
}
71+
}
72+
73+
type Thing = { a?: string, b?: number, c?: number };
74+
>Thing : Symbol(Thing, Decl(controlFlowComputedPropertyNames.ts, 18, 1))
75+
>a : Symbol(a, Decl(controlFlowComputedPropertyNames.ts, 20, 14))
76+
>b : Symbol(b, Decl(controlFlowComputedPropertyNames.ts, 20, 26))
77+
>c : Symbol(c, Decl(controlFlowComputedPropertyNames.ts, 20, 38))
78+
79+
function f3(obj: Thing, key: keyof Thing) {
80+
>f3 : Symbol(f3, Decl(controlFlowComputedPropertyNames.ts, 20, 52))
81+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 22, 12))
82+
>Thing : Symbol(Thing, Decl(controlFlowComputedPropertyNames.ts, 18, 1))
83+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 22, 23))
84+
>Thing : Symbol(Thing, Decl(controlFlowComputedPropertyNames.ts, 18, 1))
85+
86+
if (obj[key] !== undefined) {
87+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 22, 12))
88+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 22, 23))
89+
>undefined : Symbol(undefined)
90+
91+
if (typeof obj[key] === "string") {
92+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 22, 12))
93+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 22, 23))
94+
95+
obj[key].toUpperCase();
96+
>obj[key].toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
97+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 22, 12))
98+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 22, 23))
99+
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
100+
}
101+
if (typeof obj[key] === "number") {
102+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 22, 12))
103+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 22, 23))
104+
105+
obj[key].toFixed();
106+
>obj[key].toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --))
107+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 22, 12))
108+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 22, 23))
109+
>toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --))
110+
}
111+
}
112+
}
113+
114+
function f4<K extends string>(obj: Record<K, string | undefined>, key: K) {
115+
>f4 : Symbol(f4, Decl(controlFlowComputedPropertyNames.ts, 31, 1))
116+
>K : Symbol(K, Decl(controlFlowComputedPropertyNames.ts, 33, 12))
117+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 33, 30))
118+
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
119+
>K : Symbol(K, Decl(controlFlowComputedPropertyNames.ts, 33, 12))
120+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 33, 65))
121+
>K : Symbol(K, Decl(controlFlowComputedPropertyNames.ts, 33, 12))
122+
123+
if (obj[key]) {
124+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 33, 30))
125+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 33, 65))
126+
127+
obj[key].toUpperCase();
128+
>obj[key].toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
129+
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 33, 30))
130+
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 33, 65))
131+
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
132+
}
133+
}
134+
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//// [tests/cases/conformance/controlFlow/controlFlowComputedPropertyNames.ts] ////
2+
3+
=== controlFlowComputedPropertyNames.ts ===
4+
function f1(obj: Record<string, unknown>, key: string) {
5+
>f1 : (obj: Record<string, unknown>, key: string) => void
6+
>obj : Record<string, unknown>
7+
>key : string
8+
9+
if (typeof obj[key] === "string") {
10+
>typeof obj[key] === "string" : boolean
11+
>typeof obj[key] : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
12+
>obj[key] : unknown
13+
>obj : Record<string, unknown>
14+
>key : string
15+
>"string" : "string"
16+
17+
obj[key].toUpperCase();
18+
>obj[key].toUpperCase() : string
19+
>obj[key].toUpperCase : () => string
20+
>obj[key] : string
21+
>obj : Record<string, unknown>
22+
>key : string
23+
>toUpperCase : () => string
24+
}
25+
}
26+
27+
function f2(obj: Record<string, string | undefined>, key: string) {
28+
>f2 : (obj: Record<string, string | undefined>, key: string) => void
29+
>obj : Record<string, string | undefined>
30+
>key : string
31+
32+
if (obj[key] !== undefined) {
33+
>obj[key] !== undefined : boolean
34+
>obj[key] : string | undefined
35+
>obj : Record<string, string | undefined>
36+
>key : string
37+
>undefined : undefined
38+
39+
obj[key].toUpperCase();
40+
>obj[key].toUpperCase() : string
41+
>obj[key].toUpperCase : () => string
42+
>obj[key] : string
43+
>obj : Record<string, string | undefined>
44+
>key : string
45+
>toUpperCase : () => string
46+
}
47+
let key2 = key + key;
48+
>key2 : string
49+
>key + key : string
50+
>key : string
51+
>key : string
52+
53+
if (obj[key2] !== undefined) {
54+
>obj[key2] !== undefined : boolean
55+
>obj[key2] : string | undefined
56+
>obj : Record<string, string | undefined>
57+
>key2 : string
58+
>undefined : undefined
59+
60+
obj[key2].toUpperCase();
61+
>obj[key2].toUpperCase() : string
62+
>obj[key2].toUpperCase : () => string
63+
>obj[key2] : string
64+
>obj : Record<string, string | undefined>
65+
>key2 : string
66+
>toUpperCase : () => string
67+
}
68+
const key3 = key + key;
69+
>key3 : string
70+
>key + key : string
71+
>key : string
72+
>key : string
73+
74+
if (obj[key3] !== undefined) {
75+
>obj[key3] !== undefined : boolean
76+
>obj[key3] : string | undefined
77+
>obj : Record<string, string | undefined>
78+
>key3 : string
79+
>undefined : undefined
80+
81+
obj[key3].toUpperCase();
82+
>obj[key3].toUpperCase() : string
83+
>obj[key3].toUpperCase : () => string
84+
>obj[key3] : string
85+
>obj : Record<string, string | undefined>
86+
>key3 : string
87+
>toUpperCase : () => string
88+
}
89+
}
90+
91+
type Thing = { a?: string, b?: number, c?: number };
92+
>Thing : { a?: string | undefined; b?: number | undefined; c?: number | undefined; }
93+
>a : string | undefined
94+
>b : number | undefined
95+
>c : number | undefined
96+
97+
function f3(obj: Thing, key: keyof Thing) {
98+
>f3 : (obj: Thing, key: keyof Thing) => void
99+
>obj : Thing
100+
>key : keyof Thing
101+
102+
if (obj[key] !== undefined) {
103+
>obj[key] !== undefined : boolean
104+
>obj[key] : string | number | undefined
105+
>obj : Thing
106+
>key : keyof Thing
107+
>undefined : undefined
108+
109+
if (typeof obj[key] === "string") {
110+
>typeof obj[key] === "string" : boolean
111+
>typeof obj[key] : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
112+
>obj[key] : string | number
113+
>obj : Thing
114+
>key : keyof Thing
115+
>"string" : "string"
116+
117+
obj[key].toUpperCase();
118+
>obj[key].toUpperCase() : string
119+
>obj[key].toUpperCase : () => string
120+
>obj[key] : string
121+
>obj : Thing
122+
>key : keyof Thing
123+
>toUpperCase : () => string
124+
}
125+
if (typeof obj[key] === "number") {
126+
>typeof obj[key] === "number" : boolean
127+
>typeof obj[key] : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
128+
>obj[key] : string | number
129+
>obj : Thing
130+
>key : keyof Thing
131+
>"number" : "number"
132+
133+
obj[key].toFixed();
134+
>obj[key].toFixed() : string
135+
>obj[key].toFixed : (fractionDigits?: number | undefined) => string
136+
>obj[key] : number
137+
>obj : Thing
138+
>key : keyof Thing
139+
>toFixed : (fractionDigits?: number | undefined) => string
140+
}
141+
}
142+
}
143+
144+
function f4<K extends string>(obj: Record<K, string | undefined>, key: K) {
145+
>f4 : <K extends string>(obj: Record<K, string | undefined>, key: K) => void
146+
>obj : Record<K, string | undefined>
147+
>key : K
148+
149+
if (obj[key]) {
150+
>obj[key] : Record<K, string | undefined>[K]
151+
>obj : Record<K, string | undefined>
152+
>key : K
153+
154+
obj[key].toUpperCase();
155+
>obj[key].toUpperCase() : string
156+
>obj[key].toUpperCase : () => string
157+
>obj[key] : string
158+
>obj : Record<K, string | undefined>
159+
>key : K
160+
>toUpperCase : () => string
161+
}
162+
}
163+

tests/baselines/reference/mappedTypeGenericIndexedAccess.types

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ class Test {
6060
>[] : never[]
6161
}
6262
this.entries[name]?.push(entry);
63-
>this.entries[name]?.push(entry) : number | undefined
64-
>this.entries[name]?.push : ((...items: Types[T][]) => number) | undefined
65-
>this.entries[name] : Types[T][] | undefined
63+
>this.entries[name]?.push(entry) : number
64+
>this.entries[name]?.push : (...items: Types[T][]) => number
65+
>this.entries[name] : Types[T][]
6666
>this.entries : { first?: { a1: true; }[] | undefined; second?: { a2: true; }[] | undefined; third?: { a3: true; }[] | undefined; }
6767
>this : this
6868
>entries : { first?: { a1: true; }[] | undefined; second?: { a2: true; }[] | undefined; third?: { a3: true; }[] | undefined; }
6969
>name : T
70-
>push : ((...items: Types[T][]) => number) | undefined
70+
>push : (...items: Types[T][]) => number
7171
>entry : Types[T]
7272
}
7373
}

tests/baselines/reference/noUncheckedIndexedAccess.errors.txt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ noUncheckedIndexedAccess.ts(90,7): error TS2322: Type 'string | undefined' is no
3333
Type 'undefined' is not assignable to type 'string'.
3434
noUncheckedIndexedAccess.ts(98,5): error TS2322: Type 'undefined' is not assignable to type '{ [key: string]: string; a: string; b: string; }[Key]'.
3535
Type 'undefined' is not assignable to type 'string'.
36-
noUncheckedIndexedAccess.ts(99,11): error TS2322: Type 'string | undefined' is not assignable to type 'string'.
37-
Type 'undefined' is not assignable to type 'string'.
36+
noUncheckedIndexedAccess.ts(99,11): error TS2322: Type 'undefined' is not assignable to type 'string'.
3837

3938

4039
==== noUncheckedIndexedAccess.ts (31 errors) ====
@@ -203,8 +202,7 @@ noUncheckedIndexedAccess.ts(99,11): error TS2322: Type 'string | undefined' is n
203202
!!! error TS2322: Type 'undefined' is not assignable to type 'string'.
204203
const v: string = myRecord2[key]; // Should error
205204
~
206-
!!! error TS2322: Type 'string | undefined' is not assignable to type 'string'.
207-
!!! error TS2322: Type 'undefined' is not assignable to type 'string'.
205+
!!! error TS2322: Type 'undefined' is not assignable to type 'string'.
208206
};
209207

210208

tests/baselines/reference/noUncheckedIndexedAccess.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ const fn3 = <Key extends keyof typeof myRecord2>(key: Key) => {
412412

413413
const v: string = myRecord2[key]; // Should error
414414
>v : string
415-
>myRecord2[key] : string | undefined
415+
>myRecord2[key] : undefined
416416
>myRecord2 : { [key: string]: string; a: string; b: string; }
417417
>key : Key
418418

0 commit comments

Comments
 (0)