Skip to content

Commit 1de76cd

Browse files
authored
Control flow for element access expressions (#31478)
* Control flow for element access expressions Draft version, just want to see how performance is * Add baselines * Fix cast lint * Cleanup to share code path * Fix errant diffs
1 parent c7b8b2a commit 1de76cd

File tree

5 files changed

+147
-4
lines changed

5 files changed

+147
-4
lines changed

src/compiler/checker.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20573,10 +20573,15 @@ namespace ts {
2057320573
}
2057420574
propType = getConstraintForLocation(getTypeOfSymbol(prop), node);
2057520575
}
20576+
return getFlowTypeOfAccessExpression(node, prop, propType, right);
20577+
}
20578+
20579+
function getFlowTypeOfAccessExpression(node: ElementAccessExpression | PropertyAccessExpression | QualifiedName, prop: Symbol | undefined, propType: Type, errorNode: Node) {
2057620580
// Only compute control flow type if this is a property access expression that isn't an
2057720581
// assignment target, and the referenced property was declared as a variable, property,
2057820582
// accessor, or optional method.
20579-
if (node.kind !== SyntaxKind.PropertyAccessExpression ||
20583+
const assignmentKind = getAssignmentTargetKind(node);
20584+
if (node.kind !== SyntaxKind.ElementAccessExpression && node.kind !== SyntaxKind.PropertyAccessExpression ||
2058020585
assignmentKind === AssignmentKind.Definite ||
2058120586
prop && !(prop.flags & (SymbolFlags.Variable | SymbolFlags.Property | SymbolFlags.Accessor)) && !(prop.flags & SymbolFlags.Method && propType.flags & TypeFlags.Union)) {
2058220587
return propType;
@@ -20586,7 +20591,7 @@ namespace ts {
2058620591
// and if we are in a constructor of the same class as the property declaration, assume that
2058720592
// the property is uninitialized at the top of the control flow.
2058820593
let assumeUninitialized = false;
20589-
if (strictNullChecks && strictPropertyInitialization && left.kind === SyntaxKind.ThisKeyword) {
20594+
if (strictNullChecks && strictPropertyInitialization && node.expression.kind === SyntaxKind.ThisKeyword) {
2059020595
const declaration = prop && prop.valueDeclaration;
2059120596
if (declaration && isInstancePropertyWithoutInitializer(declaration)) {
2059220597
const flowContainer = getControlFlowContainer(node);
@@ -20603,7 +20608,7 @@ namespace ts {
2060320608
}
2060420609
const flowType = getFlowTypeOfReference(node, propType, assumeUninitialized ? getOptionalType(propType) : propType);
2060520610
if (assumeUninitialized && !(getFalsyFlags(propType) & TypeFlags.Undefined) && getFalsyFlags(flowType) & TypeFlags.Undefined) {
20606-
error(right, Diagnostics.Property_0_is_used_before_being_assigned, symbolToString(prop!)); // TODO: GH#18217
20611+
error(errorNode, Diagnostics.Property_0_is_used_before_being_assigned, symbolToString(prop!)); // TODO: GH#18217
2060720612
// Return the declared type to reduce follow-on errors
2060820613
return propType;
2060920614
}
@@ -20960,7 +20965,7 @@ namespace ts {
2096020965
AccessFlags.Writing | (isGenericObjectType(objectType) && !isThisTypeParameter(objectType) ? AccessFlags.NoIndexSignatures : 0) :
2096120966
AccessFlags.None;
2096220967
const indexedAccessType = getIndexedAccessTypeOrUndefined(objectType, effectiveIndexType, node, accessFlags) || errorType;
20963-
return checkIndexedAccessIndexType(indexedAccessType, node);
20968+
return checkIndexedAccessIndexType(getFlowTypeOfAccessExpression(node, indexedAccessType.symbol, indexedAccessType, indexExpression), node);
2096420969
}
2096520970

2096620971
function checkThatExpressionIsProperSymbolReference(expression: Expression, expressionType: Type, reportError: boolean): boolean {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//// [controlFlowElementAccess2.ts]
2+
declare const config: {
3+
[key: string]: boolean | { prop: string };
4+
};
5+
6+
if (typeof config['works'] !== 'boolean') {
7+
config.works.prop = 'test'; // ok
8+
config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string }
9+
}
10+
if (typeof config.works !== 'boolean') {
11+
config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string }
12+
config.works.prop = 'test'; // ok
13+
}
14+
15+
16+
//// [controlFlowElementAccess2.js]
17+
"use strict";
18+
if (typeof config['works'] !== 'boolean') {
19+
config.works.prop = 'test'; // ok
20+
config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string }
21+
}
22+
if (typeof config.works !== 'boolean') {
23+
config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string }
24+
config.works.prop = 'test'; // ok
25+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
=== tests/cases/conformance/controlFlow/controlFlowElementAccess2.ts ===
2+
declare const config: {
3+
>config : Symbol(config, Decl(controlFlowElementAccess2.ts, 0, 13))
4+
5+
[key: string]: boolean | { prop: string };
6+
>key : Symbol(key, Decl(controlFlowElementAccess2.ts, 1, 5))
7+
>prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30))
8+
9+
};
10+
11+
if (typeof config['works'] !== 'boolean') {
12+
>config : Symbol(config, Decl(controlFlowElementAccess2.ts, 0, 13))
13+
14+
config.works.prop = 'test'; // ok
15+
>config.works.prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30))
16+
>config : Symbol(config, Decl(controlFlowElementAccess2.ts, 0, 13))
17+
>prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30))
18+
19+
config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string }
20+
>config['works'].prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30))
21+
>config : Symbol(config, Decl(controlFlowElementAccess2.ts, 0, 13))
22+
>prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30))
23+
}
24+
if (typeof config.works !== 'boolean') {
25+
>config : Symbol(config, Decl(controlFlowElementAccess2.ts, 0, 13))
26+
27+
config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string }
28+
>config['works'].prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30))
29+
>config : Symbol(config, Decl(controlFlowElementAccess2.ts, 0, 13))
30+
>prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30))
31+
32+
config.works.prop = 'test'; // ok
33+
>config.works.prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30))
34+
>config : Symbol(config, Decl(controlFlowElementAccess2.ts, 0, 13))
35+
>prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30))
36+
}
37+
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
=== tests/cases/conformance/controlFlow/controlFlowElementAccess2.ts ===
2+
declare const config: {
3+
>config : { [key: string]: boolean | { prop: string; }; }
4+
5+
[key: string]: boolean | { prop: string };
6+
>key : string
7+
>prop : string
8+
9+
};
10+
11+
if (typeof config['works'] !== 'boolean') {
12+
>typeof config['works'] !== 'boolean' : boolean
13+
>typeof config['works'] : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
14+
>config['works'] : boolean | { prop: string; }
15+
>config : { [key: string]: boolean | { prop: string; }; }
16+
>'works' : "works"
17+
>'boolean' : "boolean"
18+
19+
config.works.prop = 'test'; // ok
20+
>config.works.prop = 'test' : "test"
21+
>config.works.prop : string
22+
>config.works : { prop: string; }
23+
>config : { [key: string]: boolean | { prop: string; }; }
24+
>works : { prop: string; }
25+
>prop : string
26+
>'test' : "test"
27+
28+
config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string }
29+
>config['works'].prop = 'test' : "test"
30+
>config['works'].prop : string
31+
>config['works'] : { prop: string; }
32+
>config : { [key: string]: boolean | { prop: string; }; }
33+
>'works' : "works"
34+
>prop : string
35+
>'test' : "test"
36+
}
37+
if (typeof config.works !== 'boolean') {
38+
>typeof config.works !== 'boolean' : boolean
39+
>typeof config.works : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
40+
>config.works : boolean | { prop: string; }
41+
>config : { [key: string]: boolean | { prop: string; }; }
42+
>works : boolean | { prop: string; }
43+
>'boolean' : "boolean"
44+
45+
config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string }
46+
>config['works'].prop = 'test' : "test"
47+
>config['works'].prop : string
48+
>config['works'] : { prop: string; }
49+
>config : { [key: string]: boolean | { prop: string; }; }
50+
>'works' : "works"
51+
>prop : string
52+
>'test' : "test"
53+
54+
config.works.prop = 'test'; // ok
55+
>config.works.prop = 'test' : "test"
56+
>config.works.prop : string
57+
>config.works : { prop: string; }
58+
>config : { [key: string]: boolean | { prop: string; }; }
59+
>works : { prop: string; }
60+
>prop : string
61+
>'test' : "test"
62+
}
63+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// @strict: true
2+
declare const config: {
3+
[key: string]: boolean | { prop: string };
4+
};
5+
6+
if (typeof config['works'] !== 'boolean') {
7+
config.works.prop = 'test'; // ok
8+
config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string }
9+
}
10+
if (typeof config.works !== 'boolean') {
11+
config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string }
12+
config.works.prop = 'test'; // ok
13+
}

0 commit comments

Comments
 (0)