diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 1dcbde1f9d0..cf2099badde 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3844,44 +3844,285 @@ module ts { // EXPRESSION TYPE CHECKING - function checkIdentifier(node: Identifier): Type { - function isInTypeQuery(node: Node): boolean { - // TypeScript 1.0 spec (April 2014): 3.6.3 - // A type query consists of the keyword typeof followed by an expression. - // The expression is restricted to a single identifier or a sequence of identifiers separated by periods - while (node) { - switch (node.kind) { - case SyntaxKind.TypeQuery: - return true; - case SyntaxKind.Identifier: - case SyntaxKind.QualifiedName: - node = node.parent; - continue; - default: - return false; + function getResolvedSymbol(node: Identifier): Symbol { + var links = getNodeLinks(node); + if (!links.resolvedSymbol) { + links.resolvedSymbol = resolveName(node, node.text, SymbolFlags.Value | SymbolFlags.ExportValue, Diagnostics.Cannot_find_name_0, identifierToString(node)) || unknownSymbol; + } + return links.resolvedSymbol; + } + + function isInTypeQuery(node: Node): boolean { + // TypeScript 1.0 spec (April 2014): 3.6.3 + // A type query consists of the keyword typeof followed by an expression. + // The expression is restricted to a single identifier or a sequence of identifiers separated by periods + while (node) { + switch (node.kind) { + case SyntaxKind.TypeQuery: + return true; + case SyntaxKind.Identifier: + case SyntaxKind.QualifiedName: + node = node.parent; + continue; + default: + return false; + } + } + Debug.fail("should not get here"); + } + + // Remove one or more primitive types from a union type + function subtractPrimitiveTypes(type: Type, subtractMask: TypeFlags): Type { + if (type.flags & TypeFlags.Union) { + var types = (type).types; + if (forEach(types, t => t.flags & subtractMask)) { + var newTypes: Type[] = []; + forEach(types, t => { + if (!(t.flags & subtractMask)) { + newTypes.push(t); + } + }); + return getUnionType(newTypes); + } + } + return type; + } + + // Check if a given variable is assigned within a given syntax node + function IsVariableAssignedWithin(symbol: Symbol, node: Node): boolean { + var links = getNodeLinks(node); + if (links.assignmentChecks) { + var cachedResult = links.assignmentChecks[symbol.id]; + if (cachedResult !== undefined) { + return cachedResult; + } + } + else { + links.assignmentChecks = {}; + } + return links.assignmentChecks[symbol.id] = isAssignedIn(node); + + function isAssignedInBinaryExpression(node: BinaryExpression) { + if (node.operator >= SyntaxKind.FirstAssignment && node.operator <= SyntaxKind.LastAssignment) { + var n = node.left; + while (n.kind === SyntaxKind.ParenExpression) { + n = (n).expression; + } + if (n.kind === SyntaxKind.Identifier && getResolvedSymbol(n) === symbol) { + return true; } } - Debug.fail("should not get here"); + return forEachChild(node, isAssignedIn); } - var symbol = resolveName(node, node.text, SymbolFlags.Value | SymbolFlags.ExportValue, Diagnostics.Cannot_find_name_0, identifierToString(node)); - if (!symbol) { - symbol = unknownSymbol; + function isAssignedInVariableDeclaration(node: VariableDeclaration) { + if (getSymbolOfNode(node) === symbol && node.initializer) { + return true; + } + return forEachChild(node, isAssignedIn); } + function isAssignedIn(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.BinaryExpression: + return isAssignedInBinaryExpression(node); + case SyntaxKind.VariableDeclaration: + return isAssignedInVariableDeclaration(node); + case SyntaxKind.ArrayLiteral: + case SyntaxKind.ObjectLiteral: + case SyntaxKind.PropertyAccess: + case SyntaxKind.IndexedAccess: + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + case SyntaxKind.TypeAssertion: + case SyntaxKind.ParenExpression: + case SyntaxKind.PrefixOperator: + case SyntaxKind.PostfixOperator: + case SyntaxKind.ConditionalExpression: + case SyntaxKind.Block: + case SyntaxKind.VariableStatement: + case SyntaxKind.ExpressionStatement: + case SyntaxKind.IfStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ReturnStatement: + case SyntaxKind.WithStatement: + case SyntaxKind.SwitchStatement: + case SyntaxKind.CaseClause: + case SyntaxKind.DefaultClause: + case SyntaxKind.LabeledStatement: + case SyntaxKind.ThrowStatement: + case SyntaxKind.TryStatement: + case SyntaxKind.TryBlock: + case SyntaxKind.CatchBlock: + case SyntaxKind.FinallyBlock: + return forEachChild(node, isAssignedIn); + } + return false; + } + } + + // Get the narrowed type of a given symbol at a given location + function getNarrowedTypeOfSymbol(symbol: Symbol, node: Node) { + var type = getTypeOfSymbol(symbol); + // Only narrow when symbol is variable of a non-primitive type + if (symbol.flags & SymbolFlags.Variable && isTypeAnyOrObjectOrTypeParameter(type)) { + while (true) { + var child = node; + node = node.parent; + // Stop at containing function or module block + if (!node || node.kind === SyntaxKind.FunctionBlock || node.kind === SyntaxKind.ModuleBlock) { + break; + } + var narrowedType = type; + switch (node.kind) { + case SyntaxKind.IfStatement: + // In a branch of an if statement, narrow based on controlling expression + if (child !== (node).expression) { + narrowedType = narrowType(type, (node).expression, child === (node).thenStatement); + } + break; + case SyntaxKind.ConditionalExpression: + // In a branch of a conditional expression, narrow based on controlling condition + if (child !== (node).condition) { + narrowedType = narrowType(type, (node).condition, child === (node).whenTrue); + } + break; + case SyntaxKind.BinaryExpression: + // In the right operand of an && or ||, narrow based on left operand + if (child === (node).right) { + if ((node).operator === SyntaxKind.AmpersandAmpersandToken) { + narrowedType = narrowType(type, (node).left, true); + } + else if ((node).operator === SyntaxKind.BarBarToken) { + narrowedType = narrowType(type, (node).left, false); + } + } + break; + } + // Only use narrowed type if construct contains no assignments to variable + if (narrowedType !== type && !IsVariableAssignedWithin(symbol, node)) { + type = narrowedType; + } + } + } + return type; + + function narrowTypeByEquality(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { + var left = expr.left; + var right = expr.right; + // Check that we have 'typeof ' on the left and string literal on the right + if (left.kind !== SyntaxKind.PrefixOperator || left.operator !== SyntaxKind.TypeOfKeyword || + left.operand.kind !== SyntaxKind.Identifier || right.kind !== SyntaxKind.StringLiteral || + getResolvedSymbol(left.operand) !== symbol) { + return type; + } + var t = right.text; + var checkType: Type = t === "string" ? stringType : t === "number" ? numberType : t === "boolean" ? booleanType : emptyObjectType; + if (expr.operator === SyntaxKind.ExclamationEqualsEqualsToken) { + assumeTrue = !assumeTrue; + } + if (assumeTrue) { + // The assumed result is true. If check was for a primitive type, that type is the narrowed type. Otherwise we can + // remove the primitive types from the narrowed type. + return checkType === emptyObjectType ? subtractPrimitiveTypes(type, TypeFlags.String | TypeFlags.Number | TypeFlags.Boolean) : checkType; + } + else { + // The assumed result is false. If check was for a primitive type we can remove that type from the narrowed type. + // Otherwise we don't have enough information to do anything. + return checkType === emptyObjectType ? type : subtractPrimitiveTypes(type, checkType.flags); + } + } + + function narrowTypeByAnd(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { + if (assumeTrue) { + // The assumed result is true, therefore we narrow assuming each operand to be true. + return narrowType(narrowType(type, expr.left, true), expr.right, true); + } + else { + // The assumed result is true. This means either the first operand was false, or the first operand was true + // and the second operand was false. We narrow with those assumptions and union the two resulting types. + return getUnionType([narrowType(type, expr.left, false), narrowType(narrowType(type, expr.left, true), expr.right, false)]); + } + } + + function narrowTypeByOr(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { + if (assumeTrue) { + // The assumed result is true. This means either the first operand was true, or the first operand was false + // and the second operand was true. We narrow with those assumptions and union the two resulting types. + return getUnionType([narrowType(type, expr.left, true), narrowType(narrowType(type, expr.left, false), expr.right, true)]); + } + else { + // The assumed result is false, therefore we narrow assuming each operand to be false. + return narrowType(narrowType(type, expr.left, false), expr.right, false); + } + } + + function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { + // Check that we have variable symbol on the left + if (expr.left.kind !== SyntaxKind.Identifier || getResolvedSymbol(expr.left) !== symbol) { + return type; + } + // Check that right operand is a function type with a prototype property + var rightType = checkExpression(expr.right); + if (!isTypeSubtypeOf(rightType, globalFunctionType)) { + return type; + } + var prototypeProperty = getPropertyOfType(getApparentType(rightType), "prototype"); + if (!prototypeProperty) { + return type; + } + var prototypeType = getTypeOfSymbol(prototypeProperty); + // Narrow to type of prototype property if it is a subtype of current type + return isTypeSubtypeOf(prototypeType, type) ? prototypeType : type; + } + + // Narrow the given type based on the given expression having the assumed boolean value + function narrowType(type: Type, expr: Expression, assumeTrue: boolean): Type { + switch (expr.kind) { + case SyntaxKind.ParenExpression: + return narrowType(type, (expr).expression, assumeTrue); + case SyntaxKind.BinaryExpression: + var operator = (expr).operator; + if (operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) { + return narrowTypeByEquality(type, expr, assumeTrue); + } + else if (operator === SyntaxKind.AmpersandAmpersandToken) { + return narrowTypeByAnd(type, expr, assumeTrue); + } + else if (operator === SyntaxKind.BarBarToken) { + return narrowTypeByOr(type, expr, assumeTrue); + } + else if (operator === SyntaxKind.InstanceOfKeyword) { + return narrowTypeByInstanceof(type, expr, assumeTrue); + } + break; + case SyntaxKind.PrefixOperator: + if ((expr).operator === SyntaxKind.ExclamationToken) { + return narrowType(type, (expr).operand, !assumeTrue); + } + break; + } + return type; + } + } + + function checkIdentifier(node: Identifier): Type { + var symbol = getResolvedSymbol(node); + if (symbol.flags & SymbolFlags.Import) { // Mark the import as referenced so that we emit it in the final .js file. // exception: identifiers that appear in type queries getSymbolLinks(symbol).referenced = !isInTypeQuery(node); } - getNodeLinks(node).resolvedSymbol = symbol; - checkCollisionWithCapturedSuperVariable(node, node); checkCollisionWithCapturedThisVariable(node, node); checkCollisionWithIndexVariableInGeneratedCode(node, node); - return getTypeOfSymbol(getExportSymbolOfValueSymbolIfExported(symbol)); + return getNarrowedTypeOfSymbol(getExportSymbolOfValueSymbolIfExported(symbol), node); } function captureLexicalThis(node: Node, container: Node): void { @@ -5134,8 +5375,8 @@ module ts { return numberType; } - function isTypeAnyTypeObjectTypeOrTypeParameter(type: Type): boolean { - return type === anyType || ((type.flags & (TypeFlags.ObjectType | TypeFlags.TypeParameter)) !== 0); + function isTypeAnyOrObjectOrTypeParameter(type: Type): boolean { + return (type.flags & (TypeFlags.Any | TypeFlags.ObjectType | TypeFlags.TypeParameter)) !== 0; } function checkInstanceOfExpression(node: BinaryExpression, leftType: Type, rightType: Type): Type { @@ -5144,7 +5385,7 @@ module ts { // and the right operand to be of type Any or a subtype of the 'Function' interface type. // The result is always of the Boolean primitive type. // NOTE: do not raise error if leftType is unknown as related error was already reported - if (leftType !== unknownType && !isTypeAnyTypeObjectTypeOrTypeParameter(leftType)) { + if (leftType !== unknownType && !isTypeAnyOrObjectOrTypeParameter(leftType)) { error(node.left, Diagnostics.The_left_hand_side_of_an_instanceof_expression_must_be_of_type_any_an_object_type_or_a_type_parameter); } // NOTE: do not raise error if right is unknown as related error was already reported @@ -5162,7 +5403,7 @@ module ts { if (leftType !== anyType && leftType !== stringType && leftType !== numberType) { error(node.left, Diagnostics.The_left_hand_side_of_an_in_expression_must_be_of_types_any_string_or_number); } - if (!isTypeAnyTypeObjectTypeOrTypeParameter(rightType)) { + if (!isTypeAnyOrObjectOrTypeParameter(rightType)) { error(node.right, Diagnostics.The_right_hand_side_of_an_in_expression_must_be_of_type_any_an_object_type_or_a_type_parameter); } return booleanType; @@ -6338,7 +6579,7 @@ module ts { var exprType = checkExpression(node.expression); // unknownType is returned i.e. if node.expression is identifier whose name cannot be resolved // in this case error about missing name is already reported - do not report extra one - if (!isTypeAnyTypeObjectTypeOrTypeParameter(exprType) && exprType !== unknownType) { + if (!isTypeAnyOrObjectOrTypeParameter(exprType) && exprType !== unknownType) { error(node.expression, Diagnostics.The_right_hand_side_of_a_for_in_statement_must_be_of_type_any_an_object_type_or_a_type_parameter); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 0c322b49d25..f6691353c22 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -827,14 +827,15 @@ module ts { } export interface NodeLinks { - resolvedType?: Type; // Cached type of type node - resolvedSignature?: Signature; // Cached signature of signature node or call expression - resolvedSymbol?: Symbol; // Cached name resolution result - flags?: NodeCheckFlags; // Set of flags specific to Node - enumMemberValue?: number; // Constant value of enum member + resolvedType?: Type; // Cached type of type node + resolvedSignature?: Signature; // Cached signature of signature node or call expression + resolvedSymbol?: Symbol; // Cached name resolution result + flags?: NodeCheckFlags; // Set of flags specific to Node + enumMemberValue?: number; // Constant value of enum member isIllegalTypeReferenceInConstraint?: boolean; // Is type reference in constraint refers to the type parameter from the same list - isVisible?: boolean; // Is this node visible - localModuleName?: string; // Local name for module instance + isVisible?: boolean; // Is this node visible + localModuleName?: string; // Local name for module instance + assignmentChecks?: Map; // Cache of assignment checks } export enum TypeFlags { @@ -856,10 +857,10 @@ module ts { Anonymous = 0x00008000, // Anonymous FromSignature = 0x00010000, // Created for signature assignment check - Intrinsic = Any | String | Number | Boolean | Void | Undefined | Null, + Intrinsic = Any | String | Number | Boolean | Void | Undefined | Null, StringLike = String | StringLiteral, NumberLike = Number | Enum, - ObjectType = Class | Interface | Reference | Tuple | Union | Anonymous + ObjectType = Class | Interface | Reference | Tuple | Union | Anonymous, } // Properties common to all types