Skip to content

Commit

Permalink
Control flow analysis for element access with variable index (#57847)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahejlsberg committed Mar 26, 2024
1 parent 316f180 commit a22aaf0
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 14 deletions.
27 changes: 22 additions & 5 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26622,13 +26622,20 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return getFlowCacheKey((node as NonNullExpression | ParenthesizedExpression).expression, declaredType, initialType, flowContainer);
case SyntaxKind.QualifiedName:
const left = getFlowCacheKey((node as QualifiedName).left, declaredType, initialType, flowContainer);
return left && left + "." + (node as QualifiedName).right.escapedText;
return left && `${left}.${(node as QualifiedName).right.escapedText}`;
case SyntaxKind.PropertyAccessExpression:
case SyntaxKind.ElementAccessExpression:
const propName = getAccessedPropertyName(node as AccessExpression);
if (propName !== undefined) {
const key = getFlowCacheKey((node as AccessExpression).expression, declaredType, initialType, flowContainer);
return key && key + "." + propName;
return key && `${key}.${propName}`;
}
if (isElementAccessExpression(node) && isIdentifier(node.argumentExpression)) {
const symbol = getResolvedSymbol(node.argumentExpression);
if (isConstantVariable(symbol) || isParameterOrMutableLocalVariable(symbol) && !isSymbolAssigned(symbol)) {
const key = getFlowCacheKey((node as AccessExpression).expression, declaredType, initialType, flowContainer);
return key && `${key}.@${getSymbolId(symbol)}`;
}
}
break;
case SyntaxKind.ObjectBindingPattern:
Expand Down Expand Up @@ -26674,9 +26681,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.PropertyAccessExpression:
case SyntaxKind.ElementAccessExpression:
const sourcePropertyName = getAccessedPropertyName(source as AccessExpression);
const targetPropertyName = isAccessExpression(target) ? getAccessedPropertyName(target) : undefined;
return sourcePropertyName !== undefined && targetPropertyName !== undefined && targetPropertyName === sourcePropertyName &&
isMatchingReference((source as AccessExpression).expression, (target as AccessExpression).expression);
if (sourcePropertyName !== undefined) {
const targetPropertyName = isAccessExpression(target) ? getAccessedPropertyName(target) : undefined;
if (targetPropertyName !== undefined) {
return targetPropertyName === sourcePropertyName && isMatchingReference((source as AccessExpression).expression, (target as AccessExpression).expression);
}
}
if (isElementAccessExpression(source) && isElementAccessExpression(target) && isIdentifier(source.argumentExpression) && isIdentifier(target.argumentExpression)) {
const symbol = getResolvedSymbol(source.argumentExpression);
if (symbol === getResolvedSymbol(target.argumentExpression) && (isConstantVariable(symbol) || isParameterOrMutableLocalVariable(symbol) && !isSymbolAssigned(symbol))) {
return isMatchingReference(source.expression, target.expression);
}
}
break;
case SyntaxKind.QualifiedName:
return isAccessExpression(target) &&
(source as QualifiedName).right.escapedText === getAccessedPropertyName(target) &&
Expand Down
134 changes: 134 additions & 0 deletions tests/baselines/reference/controlFlowComputedPropertyNames.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//// [tests/cases/conformance/controlFlow/controlFlowComputedPropertyNames.ts] ////

=== controlFlowComputedPropertyNames.ts ===
function f1(obj: Record<string, unknown>, key: string) {
>f1 : Symbol(f1, Decl(controlFlowComputedPropertyNames.ts, 0, 0))
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 0, 12))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 0, 41))

if (typeof obj[key] === "string") {
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 0, 12))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 0, 41))

obj[key].toUpperCase();
>obj[key].toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 0, 12))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 0, 41))
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
}
}

function f2(obj: Record<string, string | undefined>, key: string) {
>f2 : Symbol(f2, Decl(controlFlowComputedPropertyNames.ts, 4, 1))
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 6, 12))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 6, 52))

if (obj[key] !== undefined) {
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 6, 12))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 6, 52))
>undefined : Symbol(undefined)

obj[key].toUpperCase();
>obj[key].toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 6, 12))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 6, 52))
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
}
let key2 = key + key;
>key2 : Symbol(key2, Decl(controlFlowComputedPropertyNames.ts, 10, 7))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 6, 52))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 6, 52))

if (obj[key2] !== undefined) {
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 6, 12))
>key2 : Symbol(key2, Decl(controlFlowComputedPropertyNames.ts, 10, 7))
>undefined : Symbol(undefined)

obj[key2].toUpperCase();
>obj[key2].toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 6, 12))
>key2 : Symbol(key2, Decl(controlFlowComputedPropertyNames.ts, 10, 7))
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
}
const key3 = key + key;
>key3 : Symbol(key3, Decl(controlFlowComputedPropertyNames.ts, 14, 9))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 6, 52))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 6, 52))

if (obj[key3] !== undefined) {
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 6, 12))
>key3 : Symbol(key3, Decl(controlFlowComputedPropertyNames.ts, 14, 9))
>undefined : Symbol(undefined)

obj[key3].toUpperCase();
>obj[key3].toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 6, 12))
>key3 : Symbol(key3, Decl(controlFlowComputedPropertyNames.ts, 14, 9))
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
}
}

type Thing = { a?: string, b?: number, c?: number };
>Thing : Symbol(Thing, Decl(controlFlowComputedPropertyNames.ts, 18, 1))
>a : Symbol(a, Decl(controlFlowComputedPropertyNames.ts, 20, 14))
>b : Symbol(b, Decl(controlFlowComputedPropertyNames.ts, 20, 26))
>c : Symbol(c, Decl(controlFlowComputedPropertyNames.ts, 20, 38))

function f3(obj: Thing, key: keyof Thing) {
>f3 : Symbol(f3, Decl(controlFlowComputedPropertyNames.ts, 20, 52))
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 22, 12))
>Thing : Symbol(Thing, Decl(controlFlowComputedPropertyNames.ts, 18, 1))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 22, 23))
>Thing : Symbol(Thing, Decl(controlFlowComputedPropertyNames.ts, 18, 1))

if (obj[key] !== undefined) {
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 22, 12))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 22, 23))
>undefined : Symbol(undefined)

if (typeof obj[key] === "string") {
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 22, 12))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 22, 23))

obj[key].toUpperCase();
>obj[key].toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 22, 12))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 22, 23))
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
}
if (typeof obj[key] === "number") {
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 22, 12))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 22, 23))

obj[key].toFixed();
>obj[key].toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --))
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 22, 12))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 22, 23))
>toFixed : Symbol(Number.toFixed, Decl(lib.es5.d.ts, --, --))
}
}
}

function f4<K extends string>(obj: Record<K, string | undefined>, key: K) {
>f4 : Symbol(f4, Decl(controlFlowComputedPropertyNames.ts, 31, 1))
>K : Symbol(K, Decl(controlFlowComputedPropertyNames.ts, 33, 12))
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 33, 30))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
>K : Symbol(K, Decl(controlFlowComputedPropertyNames.ts, 33, 12))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 33, 65))
>K : Symbol(K, Decl(controlFlowComputedPropertyNames.ts, 33, 12))

if (obj[key]) {
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 33, 30))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 33, 65))

obj[key].toUpperCase();
>obj[key].toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
>obj : Symbol(obj, Decl(controlFlowComputedPropertyNames.ts, 33, 30))
>key : Symbol(key, Decl(controlFlowComputedPropertyNames.ts, 33, 65))
>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --))
}
}

163 changes: 163 additions & 0 deletions tests/baselines/reference/controlFlowComputedPropertyNames.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//// [tests/cases/conformance/controlFlow/controlFlowComputedPropertyNames.ts] ////

=== controlFlowComputedPropertyNames.ts ===
function f1(obj: Record<string, unknown>, key: string) {
>f1 : (obj: Record<string, unknown>, key: string) => void
>obj : Record<string, unknown>
>key : string

if (typeof obj[key] === "string") {
>typeof obj[key] === "string" : boolean
>typeof obj[key] : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>obj[key] : unknown
>obj : Record<string, unknown>
>key : string
>"string" : "string"

obj[key].toUpperCase();
>obj[key].toUpperCase() : string
>obj[key].toUpperCase : () => string
>obj[key] : string
>obj : Record<string, unknown>
>key : string
>toUpperCase : () => string
}
}

function f2(obj: Record<string, string | undefined>, key: string) {
>f2 : (obj: Record<string, string | undefined>, key: string) => void
>obj : Record<string, string | undefined>
>key : string

if (obj[key] !== undefined) {
>obj[key] !== undefined : boolean
>obj[key] : string | undefined
>obj : Record<string, string | undefined>
>key : string
>undefined : undefined

obj[key].toUpperCase();
>obj[key].toUpperCase() : string
>obj[key].toUpperCase : () => string
>obj[key] : string
>obj : Record<string, string | undefined>
>key : string
>toUpperCase : () => string
}
let key2 = key + key;
>key2 : string
>key + key : string
>key : string
>key : string

if (obj[key2] !== undefined) {
>obj[key2] !== undefined : boolean
>obj[key2] : string | undefined
>obj : Record<string, string | undefined>
>key2 : string
>undefined : undefined

obj[key2].toUpperCase();
>obj[key2].toUpperCase() : string
>obj[key2].toUpperCase : () => string
>obj[key2] : string
>obj : Record<string, string | undefined>
>key2 : string
>toUpperCase : () => string
}
const key3 = key + key;
>key3 : string
>key + key : string
>key : string
>key : string

if (obj[key3] !== undefined) {
>obj[key3] !== undefined : boolean
>obj[key3] : string | undefined
>obj : Record<string, string | undefined>
>key3 : string
>undefined : undefined

obj[key3].toUpperCase();
>obj[key3].toUpperCase() : string
>obj[key3].toUpperCase : () => string
>obj[key3] : string
>obj : Record<string, string | undefined>
>key3 : string
>toUpperCase : () => string
}
}

type Thing = { a?: string, b?: number, c?: number };
>Thing : { a?: string | undefined; b?: number | undefined; c?: number | undefined; }
>a : string | undefined
>b : number | undefined
>c : number | undefined

function f3(obj: Thing, key: keyof Thing) {
>f3 : (obj: Thing, key: keyof Thing) => void
>obj : Thing
>key : keyof Thing

if (obj[key] !== undefined) {
>obj[key] !== undefined : boolean
>obj[key] : string | number | undefined
>obj : Thing
>key : keyof Thing
>undefined : undefined

if (typeof obj[key] === "string") {
>typeof obj[key] === "string" : boolean
>typeof obj[key] : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>obj[key] : string | number
>obj : Thing
>key : keyof Thing
>"string" : "string"

obj[key].toUpperCase();
>obj[key].toUpperCase() : string
>obj[key].toUpperCase : () => string
>obj[key] : string
>obj : Thing
>key : keyof Thing
>toUpperCase : () => string
}
if (typeof obj[key] === "number") {
>typeof obj[key] === "number" : boolean
>typeof obj[key] : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>obj[key] : string | number
>obj : Thing
>key : keyof Thing
>"number" : "number"

obj[key].toFixed();
>obj[key].toFixed() : string
>obj[key].toFixed : (fractionDigits?: number | undefined) => string
>obj[key] : number
>obj : Thing
>key : keyof Thing
>toFixed : (fractionDigits?: number | undefined) => string
}
}
}

function f4<K extends string>(obj: Record<K, string | undefined>, key: K) {
>f4 : <K extends string>(obj: Record<K, string | undefined>, key: K) => void
>obj : Record<K, string | undefined>
>key : K

if (obj[key]) {
>obj[key] : Record<K, string | undefined>[K]
>obj : Record<K, string | undefined>
>key : K

obj[key].toUpperCase();
>obj[key].toUpperCase() : string
>obj[key].toUpperCase : () => string
>obj[key] : string
>obj : Record<K, string | undefined>
>key : K
>toUpperCase : () => string
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ class Test {
>[] : never[]
}
this.entries[name]?.push(entry);
>this.entries[name]?.push(entry) : number | undefined
>this.entries[name]?.push : ((...items: Types[T][]) => number) | undefined
>this.entries[name] : Types[T][] | undefined
>this.entries[name]?.push(entry) : number
>this.entries[name]?.push : (...items: Types[T][]) => number
>this.entries[name] : Types[T][]
>this.entries : { first?: { a1: true; }[] | undefined; second?: { a2: true; }[] | undefined; third?: { a3: true; }[] | undefined; }
>this : this
>entries : { first?: { a1: true; }[] | undefined; second?: { a2: true; }[] | undefined; third?: { a3: true; }[] | undefined; }
>name : T
>push : ((...items: Types[T][]) => number) | undefined
>push : (...items: Types[T][]) => number
>entry : Types[T]
}
}
Expand Down
6 changes: 2 additions & 4 deletions tests/baselines/reference/noUncheckedIndexedAccess.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ noUncheckedIndexedAccess.ts(90,7): error TS2322: Type 'string | undefined' is no
Type 'undefined' is not assignable to type 'string'.
noUncheckedIndexedAccess.ts(98,5): error TS2322: Type 'undefined' is not assignable to type '{ [key: string]: string; a: string; b: string; }[Key]'.
Type 'undefined' is not assignable to type 'string'.
noUncheckedIndexedAccess.ts(99,11): error TS2322: Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
noUncheckedIndexedAccess.ts(99,11): error TS2322: Type 'undefined' is not assignable to type 'string'.


==== noUncheckedIndexedAccess.ts (31 errors) ====
Expand Down Expand Up @@ -203,8 +202,7 @@ noUncheckedIndexedAccess.ts(99,11): error TS2322: Type 'string | undefined' is n
!!! error TS2322: Type 'undefined' is not assignable to type 'string'.
const v: string = myRecord2[key]; // Should error
~
!!! error TS2322: Type 'string | undefined' is not assignable to type 'string'.
!!! error TS2322: Type 'undefined' is not assignable to type 'string'.
!!! error TS2322: Type 'undefined' is not assignable to type 'string'.
};


2 changes: 1 addition & 1 deletion tests/baselines/reference/noUncheckedIndexedAccess.types
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ const fn3 = <Key extends keyof typeof myRecord2>(key: Key) => {

const v: string = myRecord2[key]; // Should error
>v : string
>myRecord2[key] : string | undefined
>myRecord2[key] : undefined
>myRecord2 : { [key: string]: string; a: string; b: string; }
>key : Key

Expand Down
Loading

0 comments on commit a22aaf0

Please sign in to comment.