Track non-null unknown types in control flow analysis (#45575)

* Track non-null unknown types in CFA

* Add tests
This commit is contained in:
Anders Hejlsberg 2021-09-09 09:20:44 -07:00 committed by GitHub
parent 630012a6a7
commit 5186ee3d1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 620 additions and 20 deletions

View file

@ -755,6 +755,7 @@ namespace ts {
const nonInferrableAnyType = createIntrinsicType(TypeFlags.Any, "any", ObjectFlags.ContainsWideningType);
const intrinsicMarkerType = createIntrinsicType(TypeFlags.Any, "intrinsic");
const unknownType = createIntrinsicType(TypeFlags.Unknown, "unknown");
const nonNullUnknownType = createIntrinsicType(TypeFlags.Unknown, "unknown");
const undefinedType = createIntrinsicType(TypeFlags.Undefined, "undefined");
const undefinedWideningType = strictNullChecks ? undefinedType : createIntrinsicType(TypeFlags.Undefined, "undefined", ObjectFlags.ContainsWideningType);
const optionalType = createIntrinsicType(TypeFlags.Undefined, "undefined");
@ -14048,7 +14049,9 @@ namespace ts {
const includes = addTypesToUnion(typeSet, 0, types);
if (unionReduction !== UnionReduction.None) {
if (includes & TypeFlags.AnyOrUnknown) {
return includes & TypeFlags.Any ? includes & TypeFlags.IncludesWildcard ? wildcardType : anyType : unknownType;
return includes & TypeFlags.Any ?
includes & TypeFlags.IncludesWildcard ? wildcardType : anyType :
includes & TypeFlags.Null || containsType(typeSet, unknownType) ? unknownType : nonNullUnknownType;
}
if (exactOptionalPropertyTypes && includes & TypeFlags.Undefined) {
const missingIndex = binarySearch(typeSet, missingType, getTypeId, compareValues);
@ -22220,13 +22223,6 @@ namespace ts {
return false;
}
// Given a source x, check if target matches x or is an && operation with an operand that matches x.
function containsTruthyCheck(source: Node, target: Node): boolean {
return isMatchingReference(source, target) ||
(target.kind === SyntaxKind.BinaryExpression && (target as BinaryExpression).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken &&
(containsTruthyCheck(source, (target as BinaryExpression).left) || containsTruthyCheck(source, (target as BinaryExpression).right)));
}
function getPropertyAccess(expr: Expression) {
if (isAccessExpression(expr)) {
return expr;
@ -23239,7 +23235,8 @@ namespace ts {
if (resultType === unreachableNeverType || reference.parent && reference.parent.kind === SyntaxKind.NonNullExpression && !(resultType.flags & TypeFlags.Never) && getTypeWithFacts(resultType, TypeFacts.NEUndefinedOrNull).flags & TypeFlags.Never) {
return declaredType;
}
return resultType;
// The non-null unknown type should never escape control flow analysis.
return resultType === nonNullUnknownType ? unknownType : resultType;
function getOrSetCacheKey() {
if (isKeySet) {
@ -23727,7 +23724,8 @@ namespace ts {
function narrowTypeByTruthiness(type: Type, expr: Expression, assumeTrue: boolean): Type {
if (isMatchingReference(reference, expr)) {
return getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy);
return type.flags & TypeFlags.Unknown && assumeTrue ? nonNullUnknownType :
getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy);
}
if (strictNullChecks && assumeTrue && optionalChainContainsReference(expr, reference)) {
type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
@ -23885,7 +23883,7 @@ namespace ts {
valueType.flags & TypeFlags.Null ?
assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull :
assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined;
return getTypeWithFacts(type, facts);
return type.flags & TypeFlags.Unknown && facts & (TypeFacts.NENull | TypeFacts.NEUndefinedOrNull) ? nonNullUnknownType : getTypeWithFacts(type, facts);
}
if (assumeTrue) {
const filterFn: (t: Type) => boolean = operator === SyntaxKind.EqualsEqualsToken ?
@ -23915,15 +23913,10 @@ namespace ts {
return type;
}
if (assumeTrue && type.flags & TypeFlags.Unknown && literal.text === "object") {
// The pattern x && typeof x === 'object', where x is of type unknown, narrows x to type object. We don't
// need to check for the reverse typeof x === 'object' && x since that already narrows correctly.
if (typeOfExpr.parent.parent.kind === SyntaxKind.BinaryExpression) {
const expr = typeOfExpr.parent.parent as BinaryExpression;
if (expr.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken && expr.right === typeOfExpr.parent && containsTruthyCheck(reference, expr.left)) {
return nonPrimitiveType;
}
}
return getUnionType([nonPrimitiveType, nullType]);
// The non-null unknown type is used to track whether a previous narrowing operation has removed the null type
// from the unknown type. For example, the expression `x && typeof x === 'object'` first narrows x to the non-null
// unknown type, and then narrows that to the non-primitive type.
return type === nonNullUnknownType ? nonPrimitiveType : getUnionType([nonPrimitiveType, nullType]);
}
const facts = assumeTrue ?
typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject :

View file

@ -0,0 +1,77 @@
tests/cases/conformance/controlFlow/controlFlowTypeofObject.ts(66,13): error TS2345: Argument of type 'object | null' is not assignable to parameter of type 'object'.
Type 'null' is not assignable to type 'object'.
==== tests/cases/conformance/controlFlow/controlFlowTypeofObject.ts (1 errors) ====
declare function obj(x: object): void;
function f1(x: unknown) {
if (!x) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f2(x: unknown) {
if (x === null) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f3(x: unknown) {
if (x == null) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f4(x: unknown) {
if (x == undefined) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f5(x: unknown) {
if (!!true) {
if (!x) {
return;
}
}
else {
if (x === null) {
return;
}
}
if (typeof x === 'object') {
obj(x);
}
}
function f6(x: unknown) {
if (x === null) {
x;
}
else {
x;
if (typeof x === 'object') {
obj(x);
}
}
if (typeof x === 'object') {
obj(x); // Error
~
!!! error TS2345: Argument of type 'object | null' is not assignable to parameter of type 'object'.
!!! error TS2345: Type 'null' is not assignable to type 'object'.
}
}

View file

@ -0,0 +1,144 @@
//// [controlFlowTypeofObject.ts]
declare function obj(x: object): void;
function f1(x: unknown) {
if (!x) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f2(x: unknown) {
if (x === null) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f3(x: unknown) {
if (x == null) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f4(x: unknown) {
if (x == undefined) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f5(x: unknown) {
if (!!true) {
if (!x) {
return;
}
}
else {
if (x === null) {
return;
}
}
if (typeof x === 'object') {
obj(x);
}
}
function f6(x: unknown) {
if (x === null) {
x;
}
else {
x;
if (typeof x === 'object') {
obj(x);
}
}
if (typeof x === 'object') {
obj(x); // Error
}
}
//// [controlFlowTypeofObject.js]
"use strict";
function f1(x) {
if (!x) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f2(x) {
if (x === null) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f3(x) {
if (x == null) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f4(x) {
if (x == undefined) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f5(x) {
if (!!true) {
if (!x) {
return;
}
}
else {
if (x === null) {
return;
}
}
if (typeof x === 'object') {
obj(x);
}
}
function f6(x) {
if (x === null) {
x;
}
else {
x;
if (typeof x === 'object') {
obj(x);
}
}
if (typeof x === 'object') {
obj(x); // Error
}
}
//// [controlFlowTypeofObject.d.ts]
declare function obj(x: object): void;
declare function f1(x: unknown): void;
declare function f2(x: unknown): void;
declare function f3(x: unknown): void;
declare function f4(x: unknown): void;
declare function f5(x: unknown): void;
declare function f6(x: unknown): void;

View file

@ -0,0 +1,136 @@
=== tests/cases/conformance/controlFlow/controlFlowTypeofObject.ts ===
declare function obj(x: object): void;
>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0))
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 0, 21))
function f1(x: unknown) {
>f1 : Symbol(f1, Decl(controlFlowTypeofObject.ts, 0, 38))
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 2, 12))
if (!x) {
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 2, 12))
return;
}
if (typeof x === 'object') {
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 2, 12))
obj(x);
>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0))
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 2, 12))
}
}
function f2(x: unknown) {
>f2 : Symbol(f2, Decl(controlFlowTypeofObject.ts, 9, 1))
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 11, 12))
if (x === null) {
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 11, 12))
return;
}
if (typeof x === 'object') {
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 11, 12))
obj(x);
>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0))
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 11, 12))
}
}
function f3(x: unknown) {
>f3 : Symbol(f3, Decl(controlFlowTypeofObject.ts, 18, 1))
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 20, 12))
if (x == null) {
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 20, 12))
return;
}
if (typeof x === 'object') {
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 20, 12))
obj(x);
>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0))
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 20, 12))
}
}
function f4(x: unknown) {
>f4 : Symbol(f4, Decl(controlFlowTypeofObject.ts, 27, 1))
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 29, 12))
if (x == undefined) {
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 29, 12))
>undefined : Symbol(undefined)
return;
}
if (typeof x === 'object') {
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 29, 12))
obj(x);
>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0))
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 29, 12))
}
}
function f5(x: unknown) {
>f5 : Symbol(f5, Decl(controlFlowTypeofObject.ts, 36, 1))
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 38, 12))
if (!!true) {
if (!x) {
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 38, 12))
return;
}
}
else {
if (x === null) {
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 38, 12))
return;
}
}
if (typeof x === 'object') {
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 38, 12))
obj(x);
>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0))
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 38, 12))
}
}
function f6(x: unknown) {
>f6 : Symbol(f6, Decl(controlFlowTypeofObject.ts, 52, 1))
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12))
if (x === null) {
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12))
x;
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12))
}
else {
x;
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12))
if (typeof x === 'object') {
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12))
obj(x);
>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0))
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12))
}
}
if (typeof x === 'object') {
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12))
obj(x); // Error
>obj : Symbol(obj, Decl(controlFlowTypeofObject.ts, 0, 0))
>x : Symbol(x, Decl(controlFlowTypeofObject.ts, 54, 12))
}
}

View file

@ -0,0 +1,179 @@
=== tests/cases/conformance/controlFlow/controlFlowTypeofObject.ts ===
declare function obj(x: object): void;
>obj : (x: object) => void
>x : object
function f1(x: unknown) {
>f1 : (x: unknown) => void
>x : unknown
if (!x) {
>!x : boolean
>x : unknown
return;
}
if (typeof x === 'object') {
>typeof x === 'object' : boolean
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>x : unknown
>'object' : "object"
obj(x);
>obj(x) : void
>obj : (x: object) => void
>x : object
}
}
function f2(x: unknown) {
>f2 : (x: unknown) => void
>x : unknown
if (x === null) {
>x === null : boolean
>x : unknown
>null : null
return;
}
if (typeof x === 'object') {
>typeof x === 'object' : boolean
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>x : unknown
>'object' : "object"
obj(x);
>obj(x) : void
>obj : (x: object) => void
>x : object
}
}
function f3(x: unknown) {
>f3 : (x: unknown) => void
>x : unknown
if (x == null) {
>x == null : boolean
>x : unknown
>null : null
return;
}
if (typeof x === 'object') {
>typeof x === 'object' : boolean
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>x : unknown
>'object' : "object"
obj(x);
>obj(x) : void
>obj : (x: object) => void
>x : object
}
}
function f4(x: unknown) {
>f4 : (x: unknown) => void
>x : unknown
if (x == undefined) {
>x == undefined : boolean
>x : unknown
>undefined : undefined
return;
}
if (typeof x === 'object') {
>typeof x === 'object' : boolean
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>x : unknown
>'object' : "object"
obj(x);
>obj(x) : void
>obj : (x: object) => void
>x : object
}
}
function f5(x: unknown) {
>f5 : (x: unknown) => void
>x : unknown
if (!!true) {
>!!true : true
>!true : false
>true : true
if (!x) {
>!x : boolean
>x : unknown
return;
}
}
else {
if (x === null) {
>x === null : boolean
>x : unknown
>null : null
return;
}
}
if (typeof x === 'object') {
>typeof x === 'object' : boolean
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>x : unknown
>'object' : "object"
obj(x);
>obj(x) : void
>obj : (x: object) => void
>x : object
}
}
function f6(x: unknown) {
>f6 : (x: unknown) => void
>x : unknown
if (x === null) {
>x === null : boolean
>x : unknown
>null : null
x;
>x : null
}
else {
x;
>x : unknown
if (typeof x === 'object') {
>typeof x === 'object' : boolean
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>x : unknown
>'object' : "object"
obj(x);
>obj(x) : void
>obj : (x: object) => void
>x : object
}
}
if (typeof x === 'object') {
>typeof x === 'object' : boolean
>typeof x : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>x : unknown
>'object' : "object"
obj(x); // Error
>obj(x) : void
>obj : (x: object) => void
>x : object | null
}
}

View file

@ -0,0 +1,71 @@
// @strict: true
// @declaration: true
declare function obj(x: object): void;
function f1(x: unknown) {
if (!x) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f2(x: unknown) {
if (x === null) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f3(x: unknown) {
if (x == null) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f4(x: unknown) {
if (x == undefined) {
return;
}
if (typeof x === 'object') {
obj(x);
}
}
function f5(x: unknown) {
if (!!true) {
if (!x) {
return;
}
}
else {
if (x === null) {
return;
}
}
if (typeof x === 'object') {
obj(x);
}
}
function f6(x: unknown) {
if (x === null) {
x;
}
else {
x;
if (typeof x === 'object') {
obj(x);
}
}
if (typeof x === 'object') {
obj(x); // Error
}
}