Skip to content

Commit 5186ee3

Browse files
authored
Track non-null unknown types in control flow analysis (#45575)
* Track non-null unknown types in CFA * Add tests
1 parent 630012a commit 5186ee3

File tree

6 files changed

+620
-20
lines changed

6 files changed

+620
-20
lines changed

src/compiler/checker.ts

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,7 @@ namespace ts {
755755
const nonInferrableAnyType = createIntrinsicType(TypeFlags.Any, "any", ObjectFlags.ContainsWideningType);
756756
const intrinsicMarkerType = createIntrinsicType(TypeFlags.Any, "intrinsic");
757757
const unknownType = createIntrinsicType(TypeFlags.Unknown, "unknown");
758+
const nonNullUnknownType = createIntrinsicType(TypeFlags.Unknown, "unknown");
758759
const undefinedType = createIntrinsicType(TypeFlags.Undefined, "undefined");
759760
const undefinedWideningType = strictNullChecks ? undefinedType : createIntrinsicType(TypeFlags.Undefined, "undefined", ObjectFlags.ContainsWideningType);
760761
const optionalType = createIntrinsicType(TypeFlags.Undefined, "undefined");
@@ -14048,7 +14049,9 @@ namespace ts {
1404814049
const includes = addTypesToUnion(typeSet, 0, types);
1404914050
if (unionReduction !== UnionReduction.None) {
1405014051
if (includes & TypeFlags.AnyOrUnknown) {
14051-
return includes & TypeFlags.Any ? includes & TypeFlags.IncludesWildcard ? wildcardType : anyType : unknownType;
14052+
return includes & TypeFlags.Any ?
14053+
includes & TypeFlags.IncludesWildcard ? wildcardType : anyType :
14054+
includes & TypeFlags.Null || containsType(typeSet, unknownType) ? unknownType : nonNullUnknownType;
1405214055
}
1405314056
if (exactOptionalPropertyTypes && includes & TypeFlags.Undefined) {
1405414057
const missingIndex = binarySearch(typeSet, missingType, getTypeId, compareValues);
@@ -22220,13 +22223,6 @@ namespace ts {
2222022223
return false;
2222122224
}
2222222225

22223-
// Given a source x, check if target matches x or is an && operation with an operand that matches x.
22224-
function containsTruthyCheck(source: Node, target: Node): boolean {
22225-
return isMatchingReference(source, target) ||
22226-
(target.kind === SyntaxKind.BinaryExpression && (target as BinaryExpression).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken &&
22227-
(containsTruthyCheck(source, (target as BinaryExpression).left) || containsTruthyCheck(source, (target as BinaryExpression).right)));
22228-
}
22229-
2223022226
function getPropertyAccess(expr: Expression) {
2223122227
if (isAccessExpression(expr)) {
2223222228
return expr;
@@ -23239,7 +23235,8 @@ namespace ts {
2323923235
if (resultType === unreachableNeverType || reference.parent && reference.parent.kind === SyntaxKind.NonNullExpression && !(resultType.flags & TypeFlags.Never) && getTypeWithFacts(resultType, TypeFacts.NEUndefinedOrNull).flags & TypeFlags.Never) {
2324023236
return declaredType;
2324123237
}
23242-
return resultType;
23238+
// The non-null unknown type should never escape control flow analysis.
23239+
return resultType === nonNullUnknownType ? unknownType : resultType;
2324323240

2324423241
function getOrSetCacheKey() {
2324523242
if (isKeySet) {
@@ -23727,7 +23724,8 @@ namespace ts {
2372723724

2372823725
function narrowTypeByTruthiness(type: Type, expr: Expression, assumeTrue: boolean): Type {
2372923726
if (isMatchingReference(reference, expr)) {
23730-
return getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy);
23727+
return type.flags & TypeFlags.Unknown && assumeTrue ? nonNullUnknownType :
23728+
getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy);
2373123729
}
2373223730
if (strictNullChecks && assumeTrue && optionalChainContainsReference(expr, reference)) {
2373323731
type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
@@ -23885,7 +23883,7 @@ namespace ts {
2388523883
valueType.flags & TypeFlags.Null ?
2388623884
assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull :
2388723885
assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined;
23888-
return getTypeWithFacts(type, facts);
23886+
return type.flags & TypeFlags.Unknown && facts & (TypeFacts.NENull | TypeFacts.NEUndefinedOrNull) ? nonNullUnknownType : getTypeWithFacts(type, facts);
2388923887
}
2389023888
if (assumeTrue) {
2389123889
const filterFn: (t: Type) => boolean = operator === SyntaxKind.EqualsEqualsToken ?
@@ -23915,15 +23913,10 @@ namespace ts {
2391523913
return type;
2391623914
}
2391723915
if (assumeTrue && type.flags & TypeFlags.Unknown && literal.text === "object") {
23918-
// The pattern x && typeof x === 'object', where x is of type unknown, narrows x to type object. We don't
23919-
// need to check for the reverse typeof x === 'object' && x since that already narrows correctly.
23920-
if (typeOfExpr.parent.parent.kind === SyntaxKind.BinaryExpression) {
23921-
const expr = typeOfExpr.parent.parent as BinaryExpression;
23922-
if (expr.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken && expr.right === typeOfExpr.parent && containsTruthyCheck(reference, expr.left)) {
23923-
return nonPrimitiveType;
23924-
}
23925-
}
23926-
return getUnionType([nonPrimitiveType, nullType]);
23916+
// The non-null unknown type is used to track whether a previous narrowing operation has removed the null type
23917+
// from the unknown type. For example, the expression `x && typeof x === 'object'` first narrows x to the non-null
23918+
// unknown type, and then narrows that to the non-primitive type.
23919+
return type === nonNullUnknownType ? nonPrimitiveType : getUnionType([nonPrimitiveType, nullType]);
2392723920
}
2392823921
const facts = assumeTrue ?
2392923922
typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject :
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
tests/cases/conformance/controlFlow/controlFlowTypeofObject.ts(66,13): error TS2345: Argument of type 'object | null' is not assignable to parameter of type 'object'.
2+
Type 'null' is not assignable to type 'object'.
3+
4+
5+
==== tests/cases/conformance/controlFlow/controlFlowTypeofObject.ts (1 errors) ====
6+
declare function obj(x: object): void;
7+
8+
function f1(x: unknown) {
9+
if (!x) {
10+
return;
11+
}
12+
if (typeof x === 'object') {
13+
obj(x);
14+
}
15+
}
16+
17+
function f2(x: unknown) {
18+
if (x === null) {
19+
return;
20+
}
21+
if (typeof x === 'object') {
22+
obj(x);
23+
}
24+
}
25+
26+
function f3(x: unknown) {
27+
if (x == null) {
28+
return;
29+
}
30+
if (typeof x === 'object') {
31+
obj(x);
32+
}
33+
}
34+
35+
function f4(x: unknown) {
36+
if (x == undefined) {
37+
return;
38+
}
39+
if (typeof x === 'object') {
40+
obj(x);
41+
}
42+
}
43+
44+
function f5(x: unknown) {
45+
if (!!true) {
46+
if (!x) {
47+
return;
48+
}
49+
}
50+
else {
51+
if (x === null) {
52+
return;
53+
}
54+
}
55+
if (typeof x === 'object') {
56+
obj(x);
57+
}
58+
}
59+
60+
function f6(x: unknown) {
61+
if (x === null) {
62+
x;
63+
}
64+
else {
65+
x;
66+
if (typeof x === 'object') {
67+
obj(x);
68+
}
69+
}
70+
if (typeof x === 'object') {
71+
obj(x); // Error
72+
~
73+
!!! error TS2345: Argument of type 'object | null' is not assignable to parameter of type 'object'.
74+
!!! error TS2345: Type 'null' is not assignable to type 'object'.
75+
}
76+
}
77+
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
//// [controlFlowTypeofObject.ts]
2+
declare function obj(x: object): void;
3+
4+
function f1(x: unknown) {
5+
if (!x) {
6+
return;
7+
}
8+
if (typeof x === 'object') {
9+
obj(x);
10+
}
11+
}
12+
13+
function f2(x: unknown) {
14+
if (x === null) {
15+
return;
16+
}
17+
if (typeof x === 'object') {
18+
obj(x);
19+
}
20+
}
21+
22+
function f3(x: unknown) {
23+
if (x == null) {
24+
return;
25+
}
26+
if (typeof x === 'object') {
27+
obj(x);
28+
}
29+
}
30+
31+
function f4(x: unknown) {
32+
if (x == undefined) {
33+
return;
34+
}
35+
if (typeof x === 'object') {
36+
obj(x);
37+
}
38+
}
39+
40+
function f5(x: unknown) {
41+
if (!!true) {
42+
if (!x) {
43+
return;
44+
}
45+
}
46+
else {
47+
if (x === null) {
48+
return;
49+
}
50+
}
51+
if (typeof x === 'object') {
52+
obj(x);
53+
}
54+
}
55+
56+
function f6(x: unknown) {
57+
if (x === null) {
58+
x;
59+
}
60+
else {
61+
x;
62+
if (typeof x === 'object') {
63+
obj(x);
64+
}
65+
}
66+
if (typeof x === 'object') {
67+
obj(x); // Error
68+
}
69+
}
70+
71+
72+
//// [controlFlowTypeofObject.js]
73+
"use strict";
74+
function f1(x) {
75+
if (!x) {
76+
return;
77+
}
78+
if (typeof x === 'object') {
79+
obj(x);
80+
}
81+
}
82+
function f2(x) {
83+
if (x === null) {
84+
return;
85+
}
86+
if (typeof x === 'object') {
87+
obj(x);
88+
}
89+
}
90+
function f3(x) {
91+
if (x == null) {
92+
return;
93+
}
94+
if (typeof x === 'object') {
95+
obj(x);
96+
}
97+
}
98+
function f4(x) {
99+
if (x == undefined) {
100+
return;
101+
}
102+
if (typeof x === 'object') {
103+
obj(x);
104+
}
105+
}
106+
function f5(x) {
107+
if (!!true) {
108+
if (!x) {
109+
return;
110+
}
111+
}
112+
else {
113+
if (x === null) {
114+
return;
115+
}
116+
}
117+
if (typeof x === 'object') {
118+
obj(x);
119+
}
120+
}
121+
function f6(x) {
122+
if (x === null) {
123+
x;
124+
}
125+
else {
126+
x;
127+
if (typeof x === 'object') {
128+
obj(x);
129+
}
130+
}
131+
if (typeof x === 'object') {
132+
obj(x); // Error
133+
}
134+
}
135+
136+
137+
//// [controlFlowTypeofObject.d.ts]
138+
declare function obj(x: object): void;
139+
declare function f1(x: unknown): void;
140+
declare function f2(x: unknown): void;
141+
declare function f3(x: unknown): void;
142+
declare function f4(x: unknown): void;
143+
declare function f5(x: unknown): void;
144+
declare function f6(x: unknown): void;

0 commit comments

Comments
 (0)