Merge pull request #11001 from Microsoft/silentNever

Fix control flow loop analysis with incomplete types
This commit is contained in:
Anders Hejlsberg
2016-09-21 13:22:36 -07:00
committed by GitHub
5 changed files with 254 additions and 18 deletions

View File

@@ -132,6 +132,7 @@ namespace ts {
const esSymbolType = createIntrinsicType(TypeFlags.ESSymbol, "symbol");
const voidType = createIntrinsicType(TypeFlags.Void, "void");
const neverType = createIntrinsicType(TypeFlags.Never, "never");
const silentNeverType = createIntrinsicType(TypeFlags.Never, "never");
const emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
const emptyGenericType = <GenericType><ObjectType>createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined);
@@ -147,6 +148,7 @@ namespace ts {
const anySignature = createSignature(undefined, undefined, undefined, emptyArray, anyType, /*typePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false);
const unknownSignature = createSignature(undefined, undefined, undefined, emptyArray, unknownType, /*typePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false);
const resolvingSignature = createSignature(undefined, undefined, undefined, emptyArray, anyType, /*typePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false);
const silentNeverSignature = createSignature(undefined, undefined, undefined, emptyArray, silentNeverType, /*typePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false);
const enumNumberIndexInfo = createIndexInfo(stringType, /*isReadonly*/ true);
@@ -8073,8 +8075,11 @@ namespace ts {
// we remove type string.
function getAssignmentReducedType(declaredType: UnionType, assignedType: Type) {
if (declaredType !== assignedType) {
if (assignedType.flags & TypeFlags.Never) {
return assignedType;
}
const reducedType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t));
if (reducedType !== neverType) {
if (!(reducedType.flags & TypeFlags.Never)) {
return reducedType;
}
}
@@ -8354,7 +8359,7 @@ namespace ts {
const visitedFlowStart = visitedFlowCount;
const result = getTypeFromFlowType(getTypeAtFlowNode(reference.flowNode));
visitedFlowCount = visitedFlowStart;
if (reference.parent.kind === SyntaxKind.NonNullExpression && getTypeWithFacts(result, TypeFacts.NEUndefinedOrNull) === neverType) {
if (reference.parent.kind === SyntaxKind.NonNullExpression && getTypeWithFacts(result, TypeFacts.NEUndefinedOrNull).flags & TypeFlags.Never) {
return declaredType;
}
return result;
@@ -8443,17 +8448,18 @@ namespace ts {
function getTypeAtFlowCondition(flow: FlowCondition): FlowType {
const flowType = getTypeAtFlowNode(flow.antecedent);
let type = getTypeFromFlowType(flowType);
if (type !== neverType) {
if (!(type.flags & TypeFlags.Never)) {
// If we have an antecedent type (meaning we're reachable in some way), we first
// attempt to narrow the antecedent type. If that produces the never type, and if
// the antecedent type is incomplete (i.e. a transient type in a loop), then we
// take the type guard as an indication that control *could* reach here once we
// have the complete type. We proceed by reverting to the declared type and then
// narrow that.
// have the complete type. We proceed by switching to the silent never type which
// doesn't report errors when operators are applied to it. Note that this is the
// *only* place a silent never type is ever generated.
const assumeTrue = (flow.flags & FlowFlags.TrueCondition) !== 0;
type = narrowType(type, flow.expression, assumeTrue);
if (type === neverType && isIncomplete(flowType)) {
type = narrowType(declaredType, flow.expression, assumeTrue);
if (type.flags & TypeFlags.Never && isIncomplete(flowType)) {
type = silentNeverType;
}
}
return createFlowType(type, isIncomplete(flowType));
@@ -8662,7 +8668,7 @@ namespace ts {
}
if (assumeTrue) {
const narrowedType = filterType(type, t => areTypesComparable(t, valueType));
return narrowedType !== neverType ? narrowedType : type;
return narrowedType.flags & TypeFlags.Never ? type : narrowedType;
}
return isUnitType(valueType) ? filterType(type, t => t !== valueType) : type;
}
@@ -8705,12 +8711,12 @@ namespace ts {
const clauseTypes = switchTypes.slice(clauseStart, clauseEnd);
const hasDefaultClause = clauseStart === clauseEnd || contains(clauseTypes, neverType);
const discriminantType = getUnionType(clauseTypes);
const caseType = discriminantType === neverType ? neverType : filterType(type, t => isTypeComparableTo(discriminantType, t));
const caseType = discriminantType.flags & TypeFlags.Never ? neverType : filterType(type, t => isTypeComparableTo(discriminantType, t));
if (!hasDefaultClause) {
return caseType;
}
const defaultType = filterType(type, t => !(isUnitType(t) && contains(switchTypes, t)));
return caseType === neverType ? defaultType : getUnionType([caseType, defaultType]);
return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]);
}
function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
@@ -8774,7 +8780,7 @@ namespace ts {
// the candidate type. If one or more constituents remain, return a union of those.
if (type.flags & TypeFlags.Union) {
const assignableType = filterType(type, t => isTypeInstanceOf(t, candidate));
if (assignableType !== neverType) {
if (!(assignableType.flags & TypeFlags.Never)) {
return assignableType;
}
}
@@ -10892,7 +10898,7 @@ namespace ts {
function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, right: Identifier) {
const type = checkNonNullExpression(left);
if (isTypeAny(type)) {
if (isTypeAny(type) || type === silentNeverType) {
return type;
}
@@ -11039,8 +11045,8 @@ namespace ts {
const objectType = getApparentType(checkNonNullExpression(node.expression));
const indexType = node.argumentExpression ? checkExpression(node.argumentExpression) : unknownType;
if (objectType === unknownType) {
return unknownType;
if (objectType === unknownType || objectType === silentNeverType) {
return objectType;
}
const isConstEnum = isConstEnumObjectType(objectType);
@@ -12090,6 +12096,9 @@ namespace ts {
}
const funcType = checkNonNullExpression(node.expression);
if (funcType === silentNeverType) {
return silentNeverSignature;
}
const apparentType = getApparentType(funcType);
if (apparentType === unknownType) {
@@ -12162,6 +12171,9 @@ namespace ts {
}
let expressionType = checkNonNullExpression(node.expression);
if (expressionType === silentNeverType) {
return silentNeverSignature;
}
// If expressionType's apparent type(section 3.8.1) is an object type with one or
// more construct signatures, the expression is processed in the same manner as a
@@ -12721,7 +12733,7 @@ namespace ts {
// the native Promise<T> type by the caller.
type = checkAwaitedType(type, func, Diagnostics.Return_expression_in_async_function_does_not_have_a_valid_callable_then_member);
}
if (type === neverType) {
if (type.flags & TypeFlags.Never) {
hasReturnOfTypeNever = true;
}
else if (!contains(aggregatedTypes, type)) {
@@ -12771,7 +12783,7 @@ namespace ts {
const hasExplicitReturn = func.flags & NodeFlags.HasExplicitReturn;
if (returnType === neverType) {
if (returnType && returnType.flags & TypeFlags.Never) {
error(func.type, Diagnostics.A_function_returning_never_cannot_have_a_reachable_end_point);
}
else if (returnType && !hasExplicitReturn) {
@@ -13027,6 +13039,9 @@ namespace ts {
function checkPrefixUnaryExpression(node: PrefixUnaryExpression): Type {
const operandType = checkExpression(node.operand);
if (operandType === silentNeverType) {
return silentNeverType;
}
if (node.operator === SyntaxKind.MinusToken && node.operand.kind === SyntaxKind.NumericLiteral) {
return getLiteralTypeForText(TypeFlags.NumberLiteral, "" + -(<LiteralExpression>node.operand).text);
}
@@ -13060,6 +13075,9 @@ namespace ts {
function checkPostfixUnaryExpression(node: PostfixUnaryExpression): Type {
const operandType = checkExpression(node.operand);
if (operandType === silentNeverType) {
return silentNeverType;
}
const ok = checkArithmeticOperandType(node.operand, getNonNullableType(operandType),
Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_or_an_enum_type);
if (ok) {
@@ -13124,6 +13142,9 @@ namespace ts {
}
function checkInstanceOfExpression(left: Expression, right: Expression, leftType: Type, rightType: Type): Type {
if (leftType === silentNeverType || rightType === silentNeverType) {
return silentNeverType;
}
// TypeScript 1.0 spec (April 2014): 4.15.4
// The instanceof operator requires the left operand to be of type Any, an object type, or a type parameter type,
// and the right operand to be of type Any or a subtype of the 'Function' interface type.
@@ -13140,6 +13161,9 @@ namespace ts {
}
function checkInExpression(left: Expression, right: Expression, leftType: Type, rightType: Type): Type {
if (leftType === silentNeverType || rightType === silentNeverType) {
return silentNeverType;
}
// TypeScript 1.0 spec (April 2014): 4.15.5
// The in operator requires the left operand to be of type Any, the String primitive type, or the Number primitive type,
// and the right operand to be of type Any, an object type, or a type parameter type.
@@ -13404,6 +13428,9 @@ namespace ts {
case SyntaxKind.CaretEqualsToken:
case SyntaxKind.AmpersandToken:
case SyntaxKind.AmpersandEqualsToken:
if (leftType === silentNeverType || rightType === silentNeverType) {
return silentNeverType;
}
// TypeScript 1.0 spec (April 2014): 4.19.1
// These operators require their operands to be of type Any, the Number primitive type,
// or an enum type. Operands of an enum type are treated
@@ -13436,6 +13463,9 @@ namespace ts {
return numberType;
case SyntaxKind.PlusToken:
case SyntaxKind.PlusEqualsToken:
if (leftType === silentNeverType || rightType === silentNeverType) {
return silentNeverType;
}
// TypeScript 1.0 spec (April 2014): 4.19.2
// The binary + operator requires both operands to be of the Number primitive type or an enum type,
// or at least one of the operands to be of type Any or the String primitive type.
@@ -16268,7 +16298,7 @@ namespace ts {
// Now that we've removed all the StringLike types, if no constituents remain, then the entire
// arrayOrStringType was a string.
if (arrayType === neverType) {
if (arrayType.flags & TypeFlags.Never) {
return stringType;
}
}
@@ -16329,7 +16359,7 @@ namespace ts {
if (func) {
const signature = getSignatureFromDeclaration(func);
const returnType = getReturnTypeOfSignature(signature);
if (strictNullChecks || node.expression || returnType === neverType) {
if (strictNullChecks || node.expression || returnType.flags & TypeFlags.Never) {
const exprType = node.expression ? checkExpressionCached(node.expression) : undefinedType;
if (func.asteriskToken) {

View File

@@ -0,0 +1,53 @@
//// [controlFlowWithIncompleteTypes.ts]
// Repro from #11000
declare var cond: boolean;
function foo1() {
let x: string | number | boolean = 0;
while (cond) {
if (typeof x === "string") {
x = x.slice();
}
else {
x = "abc";
}
}
}
function foo2() {
let x: string | number | boolean = 0;
while (cond) {
if (typeof x === "number") {
x = "abc";
}
else {
x = x.slice();
}
}
}
//// [controlFlowWithIncompleteTypes.js]
// Repro from #11000
function foo1() {
var x = 0;
while (cond) {
if (typeof x === "string") {
x = x.slice();
}
else {
x = "abc";
}
}
}
function foo2() {
var x = 0;
while (cond) {
if (typeof x === "number") {
x = "abc";
}
else {
x = x.slice();
}
}
}

View File

@@ -0,0 +1,55 @@
=== tests/cases/compiler/controlFlowWithIncompleteTypes.ts ===
// Repro from #11000
declare var cond: boolean;
>cond : Symbol(cond, Decl(controlFlowWithIncompleteTypes.ts, 2, 11))
function foo1() {
>foo1 : Symbol(foo1, Decl(controlFlowWithIncompleteTypes.ts, 2, 26))
let x: string | number | boolean = 0;
>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 5, 7))
while (cond) {
>cond : Symbol(cond, Decl(controlFlowWithIncompleteTypes.ts, 2, 11))
if (typeof x === "string") {
>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 5, 7))
x = x.slice();
>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 5, 7))
>x.slice : Symbol(String.slice, Decl(lib.d.ts, --, --))
>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 5, 7))
>slice : Symbol(String.slice, Decl(lib.d.ts, --, --))
}
else {
x = "abc";
>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 5, 7))
}
}
}
function foo2() {
>foo2 : Symbol(foo2, Decl(controlFlowWithIncompleteTypes.ts, 14, 1))
let x: string | number | boolean = 0;
>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 17, 7))
while (cond) {
>cond : Symbol(cond, Decl(controlFlowWithIncompleteTypes.ts, 2, 11))
if (typeof x === "number") {
>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 17, 7))
x = "abc";
>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 17, 7))
}
else {
x = x.slice();
>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 17, 7))
>x.slice : Symbol(String.slice, Decl(lib.d.ts, --, --))
>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 17, 7))
>slice : Symbol(String.slice, Decl(lib.d.ts, --, --))
}
}
}

View File

@@ -0,0 +1,71 @@
=== tests/cases/compiler/controlFlowWithIncompleteTypes.ts ===
// Repro from #11000
declare var cond: boolean;
>cond : boolean
function foo1() {
>foo1 : () => void
let x: string | number | boolean = 0;
>x : string | number | boolean
>0 : 0
while (cond) {
>cond : boolean
if (typeof x === "string") {
>typeof x === "string" : boolean
>typeof x : string
>x : string | number
>"string" : "string"
x = x.slice();
>x = x.slice() : string
>x : string | number | boolean
>x.slice() : string
>x.slice : (start?: number, end?: number) => string
>x : string
>slice : (start?: number, end?: number) => string
}
else {
x = "abc";
>x = "abc" : "abc"
>x : string | number | boolean
>"abc" : "abc"
}
}
}
function foo2() {
>foo2 : () => void
let x: string | number | boolean = 0;
>x : string | number | boolean
>0 : 0
while (cond) {
>cond : boolean
if (typeof x === "number") {
>typeof x === "number" : boolean
>typeof x : string
>x : string | number
>"number" : "number"
x = "abc";
>x = "abc" : "abc"
>x : string | number | boolean
>"abc" : "abc"
}
else {
x = x.slice();
>x = x.slice() : string
>x : string | number | boolean
>x.slice() : string
>x.slice : (start?: number, end?: number) => string
>x : string
>slice : (start?: number, end?: number) => string
}
}
}

View File

@@ -0,0 +1,27 @@
// Repro from #11000
declare var cond: boolean;
function foo1() {
let x: string | number | boolean = 0;
while (cond) {
if (typeof x === "string") {
x = x.slice();
}
else {
x = "abc";
}
}
}
function foo2() {
let x: string | number | boolean = 0;
while (cond) {
if (typeof x === "number") {
x = "abc";
}
else {
x = x.slice();
}
}
}