From c4cff9833df7a90868473d1461a38dbc7be49896 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Wed, 2 Dec 2015 18:35:49 -0800 Subject: [PATCH] first pass at this type predicates --- src/compiler/checker.ts | 281 +++++++++++++++++---------- src/compiler/diagnosticMessages.json | 4 + src/compiler/parser.ts | 29 ++- src/compiler/types.ts | 28 ++- src/compiler/utilities.ts | 4 + 5 files changed, 231 insertions(+), 115 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index e737cd58bd5..a5a904ab3a9 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -2058,7 +2058,12 @@ namespace ts { let returnType: Type; if (signature.typePredicate) { - writer.writeParameter(signature.typePredicate.parameterName); + if (signature.typePredicate.kind === TypePredicateKind.Identifier) { + writer.writeParameter((signature.typePredicate as IdentifierTypePredicate).parameterName); + } + else { + writeKeyword(writer, SyntaxKind.ThisKeyword); + } writeSpace(writer); writeKeyword(writer, SyntaxKind.IsKeyword); writeSpace(writer); @@ -3356,7 +3361,7 @@ namespace ts { } function createSignature(declaration: SignatureDeclaration, typeParameters: TypeParameter[], parameters: Symbol[], - resolvedReturnType: Type, typePredicate: TypePredicate, minArgumentCount: number, hasRestParameter: boolean, hasStringLiterals: boolean): Signature { + resolvedReturnType: Type, typePredicate: IdentifierTypePredicate | ThisTypePredicate, minArgumentCount: number, hasRestParameter: boolean, hasStringLiterals: boolean): Signature { const sig = new Signature(checker); sig.declaration = declaration; sig.typeParameters = typeParameters; @@ -3884,19 +3889,29 @@ namespace ts { } let returnType: Type; - let typePredicate: TypePredicate; + let typePredicate: IdentifierTypePredicate | ThisTypePredicate; if (classType) { returnType = classType; } else if (declaration.type) { returnType = getTypeFromTypeNode(declaration.type); if (declaration.type.kind === SyntaxKind.TypePredicate) { - const typePredicateNode = declaration.type; - typePredicate = { - parameterName: typePredicateNode.parameterName ? typePredicateNode.parameterName.text : undefined, - parameterIndex: typePredicateNode.parameterName ? getTypePredicateParameterIndex(declaration.parameters, typePredicateNode.parameterName) : undefined, - type: getTypeFromTypeNode(typePredicateNode.type) - }; + const typePredicateNode = declaration.type as TypePredicateNode; + if (typePredicateNode.parameterName.kind === SyntaxKind.Identifier) { + const parameterName = typePredicateNode.parameterName as Identifier; + typePredicate = { + kind: TypePredicateKind.Identifier, + parameterName: parameterName ? parameterName.text : undefined, + parameterIndex: parameterName ? getTypePredicateParameterIndex(declaration.parameters, parameterName) : undefined, + type: getTypeFromTypeNode(typePredicateNode.type) + }; + } + else { + typePredicate = { + kind: TypePredicateKind.This, + type: getTypeFromTypeNode(typePredicateNode.type) + } as ThisTypePredicate; + } } } else { @@ -4716,19 +4731,33 @@ namespace ts { return result; } + function cloneTypePredicate(predicate: TypePredicate, mapper: TypeMapper): ThisTypePredicate | IdentifierTypePredicate { + if (predicate.kind === TypePredicateKind.Identifier) { + const identifierPredicate = predicate as IdentifierTypePredicate; + return { + kind: TypePredicateKind.Identifier, + parameterName: identifierPredicate.parameterName, + parameterIndex: identifierPredicate.parameterIndex, + type: instantiateType(predicate.type, mapper) + } as IdentifierTypePredicate; + } + else { + return { + kind: TypePredicateKind.This, + type: instantiateType(predicate.type, mapper) + } as ThisTypePredicate; + } + } + function instantiateSignature(signature: Signature, mapper: TypeMapper, eraseTypeParameters?: boolean): Signature { let freshTypeParameters: TypeParameter[]; - let freshTypePredicate: TypePredicate; + let freshTypePredicate: ThisTypePredicate | IdentifierTypePredicate; if (signature.typeParameters && !eraseTypeParameters) { freshTypeParameters = instantiateList(signature.typeParameters, mapper, instantiateTypeParameter); mapper = combineTypeMappers(createTypeMapper(signature.typeParameters, freshTypeParameters), mapper); } if (signature.typePredicate) { - freshTypePredicate = { - parameterName: signature.typePredicate.parameterName, - parameterIndex: signature.typePredicate.parameterIndex, - type: instantiateType(signature.typePredicate.type, mapper) - }; + freshTypePredicate = cloneTypePredicate(signature.typePredicate, mapper); } const result = createSignature(signature.declaration, freshTypeParameters, instantiateList(signature.parameters, mapper, instantiateSymbol), @@ -5548,33 +5577,55 @@ namespace ts { } if (source.typePredicate && target.typePredicate) { - const hasDifferentParameterIndex = source.typePredicate.parameterIndex !== target.typePredicate.parameterIndex; - let hasDifferentTypes: boolean; - if (hasDifferentParameterIndex || - (hasDifferentTypes = !isTypeIdenticalTo(source.typePredicate.type, target.typePredicate.type))) { - + if (source.typePredicate.kind !== target.typePredicate.kind) { if (reportErrors) { - const sourceParamText = source.typePredicate.parameterName; - const targetParamText = target.typePredicate.parameterName; - const sourceTypeText = typeToString(source.typePredicate.type); - const targetTypeText = typeToString(target.typePredicate.type); - - if (hasDifferentParameterIndex) { - reportError(Diagnostics.Parameter_0_is_not_in_the_same_position_as_parameter_1, - sourceParamText, - targetParamText); - } - else if (hasDifferentTypes) { + reportError(Diagnostics.A_this_based_type_guard_is_not_assignable_to_a_parameter_based_type_guard); + } + return Ternary.False; + } + if (source.typePredicate.kind === TypePredicateKind.This) { + if (!isTypeIdenticalTo(source.typePredicate.type, target.typePredicate.type)) { + if (reportErrors) { + const sourceTypeText = typeToString(source.typePredicate.type); + const targetTypeText = typeToString(target.typePredicate.type); reportError(Diagnostics.Type_0_is_not_assignable_to_type_1, sourceTypeText, targetTypeText); } - - reportError(Diagnostics.Type_predicate_0_is_not_assignable_to_1, - `${sourceParamText} is ${sourceTypeText}`, - `${targetParamText} is ${targetTypeText}`); + return Ternary.False; + } + } + else { + const sourcePredicate = source.typePredicate as IdentifierTypePredicate; + const targetPredicate = target.typePredicate as IdentifierTypePredicate; + const hasDifferentParameterIndex = sourcePredicate.parameterIndex !== targetPredicate.parameterIndex; + let hasDifferentTypes: boolean; + if (hasDifferentParameterIndex || + (hasDifferentTypes = !isTypeIdenticalTo(sourcePredicate.type, targetPredicate.type))) { + + if (reportErrors) { + const sourceParamText = sourcePredicate.parameterName; + const targetParamText = targetPredicate.parameterName; + const sourceTypeText = typeToString(sourcePredicate.type); + const targetTypeText = typeToString(targetPredicate.type); + + if (hasDifferentParameterIndex) { + reportError(Diagnostics.Parameter_0_is_not_in_the_same_position_as_parameter_1, + sourceParamText, + targetParamText); + } + else if (hasDifferentTypes) { + reportError(Diagnostics.Type_0_is_not_assignable_to_type_1, + sourceTypeText, + targetTypeText); + } + + reportError(Diagnostics.Type_predicate_0_is_not_assignable_to_1, + `${sourceParamText} is ${sourceTypeText}`, + `${targetParamText} is ${targetTypeText}`); + } + return Ternary.False; } - return Ternary.False; } } else if (!source.typePredicate && target.typePredicate) { @@ -6235,11 +6286,15 @@ namespace ts { function inferFromSignature(source: Signature, target: Signature) { forEachMatchingParameterType(source, target, inferFromTypes); if (source.typePredicate && target.typePredicate) { - if (target.typePredicate.parameterIndex === source.typePredicate.parameterIndex) { - // Return types from type predicates are treated as booleans. In order to infer types - // from type predicates we would need to infer using the type within the type predicate - // (i.e. 'Foo' from 'x is Foo'). - inferFromTypes(source.typePredicate.type, target.typePredicate.type); + if (target.typePredicate.kind === source.typePredicate.kind) { + if ((target.typePredicate.kind === TypePredicateKind.Identifier + && (target.typePredicate as IdentifierTypePredicate).parameterIndex === (source.typePredicate as IdentifierTypePredicate).parameterIndex) + || target.typePredicate.kind === TypePredicateKind.This) { + // Return types from type predicates are treated as booleans. In order to infer types + // from type predicates we would need to infer using the type within the type predicate + // (i.e. 'Foo' from 'x is Foo'). + inferFromTypes(source.typePredicate.type, target.typePredicate.type); + } } } else { @@ -6654,20 +6709,20 @@ namespace ts { } if (targetType) { - if (!assumeTrue) { - if (type.flags & TypeFlags.Union) { - return getUnionType(filter((type).types, t => !isTypeSubtypeOf(t, targetType))); - } - return type; - } - - return getNarrowedType(type, targetType); + return getNarrowedType(type, targetType, assumeTrue); } return type; } - function getNarrowedType(originalType: Type, narrowedTypeCandidate: Type) { + function getNarrowedType(originalType: Type, narrowedTypeCandidate: Type, assumeTrue: boolean) { + if (!assumeTrue) { + if (originalType.flags & TypeFlags.Union) { + return getUnionType(filter((originalType).types, t => !isTypeSubtypeOf(t, narrowedTypeCandidate))); + } + return originalType; + } + // If the current type is a union type, remove all constituents that aren't assignable to target. If that produces // 0 candidates, fall back to the assignability check if (originalType.flags & TypeFlags.Union) { @@ -6691,19 +6746,35 @@ namespace ts { } const signature = getResolvedSignature(expr); - if (signature.typePredicate && - expr.arguments[signature.typePredicate.parameterIndex] && - getSymbolAtLocation(expr.arguments[signature.typePredicate.parameterIndex]) === symbol) { - - if (!assumeTrue) { - if (type.flags & TypeFlags.Union) { - return getUnionType(filter((type).types, t => !isTypeSubtypeOf(t, signature.typePredicate.type))); - } - return type; + if (!signature.typePredicate) { + return type; + } + const predicate = signature.typePredicate; + if (isIdentifierTypePredicate(predicate)) { + if (expr.arguments[predicate.parameterIndex] && + getSymbolAtLocation(expr.arguments[predicate.parameterIndex]) === symbol) { + return getNarrowedType(type, predicate.type, assumeTrue); + } + } + else { + const expression = skipParenthesizedNodes(expr.expression); + + if (expression.kind === SyntaxKind.ElementAccessExpression || expression.kind === SyntaxKind.PropertyAccessExpression) { + const accessExpression = expression as ElementAccessExpression | PropertyAccessExpression; + const possibleIdentifier = skipParenthesizedNodes(accessExpression.expression); + if (possibleIdentifier.kind === SyntaxKind.Identifier && getSymbolAtLocation(possibleIdentifier) === symbol) { + return getNarrowedType(type, predicate.type, assumeTrue); + } } - return getNarrowedType(type, signature.typePredicate.type); } return type; + + function skipParenthesizedNodes(expression: Expression): Expression { + while (expression.kind === SyntaxKind.ParenthesizedExpression) { + expression = (expression as ParenthesizedExpression).expression; + } + return expression; + } } // Narrow the given type based on the given expression having the assumed boolean value. The returned type @@ -10962,51 +11033,57 @@ namespace ts { const typePredicate = getSignatureFromDeclaration(node).typePredicate; const typePredicateNode = node.type; if (isInLegalTypePredicatePosition(typePredicateNode)) { - if (typePredicate.parameterIndex >= 0) { - if (node.parameters[typePredicate.parameterIndex].dotDotDotToken) { - error(typePredicateNode.parameterName, - Diagnostics.A_type_predicate_cannot_reference_a_rest_parameter); + if (isIdentifierTypePredicate(typePredicate)) { + if (typePredicate.parameterIndex >= 0) { + if (node.parameters[typePredicate.parameterIndex].dotDotDotToken) { + error(typePredicateNode.parameterName, + Diagnostics.A_type_predicate_cannot_reference_a_rest_parameter); + } + else { + checkTypeAssignableTo(typePredicate.type, + getTypeOfNode(node.parameters[typePredicate.parameterIndex]), + typePredicateNode.type); + } } - else { - checkTypeAssignableTo(typePredicate.type, - getTypeOfNode(node.parameters[typePredicate.parameterIndex]), - typePredicateNode.type); + else if (typePredicateNode.parameterName) { + let hasReportedError = false; + for (var param of node.parameters) { + if (hasReportedError) { + break; + } + if (param.name.kind === SyntaxKind.ObjectBindingPattern || + param.name.kind === SyntaxKind.ArrayBindingPattern) { + + (function checkBindingPattern(pattern: BindingPattern) { + for (const element of pattern.elements) { + if (element.name.kind === SyntaxKind.Identifier && + (element.name).text === typePredicate.parameterName) { + + error(typePredicateNode.parameterName, + Diagnostics.A_type_predicate_cannot_reference_element_0_in_a_binding_pattern, + typePredicate.parameterName); + hasReportedError = true; + break; + } + else if (element.name.kind === SyntaxKind.ArrayBindingPattern || + element.name.kind === SyntaxKind.ObjectBindingPattern) { + + checkBindingPattern(element.name); + } + } + })(param.name); + } + } + if (!hasReportedError) { + error(typePredicateNode.parameterName, + Diagnostics.Cannot_find_parameter_0, + typePredicate.parameterName); + } } } - else if (typePredicateNode.parameterName) { - let hasReportedError = false; - for (var param of node.parameters) { - if (hasReportedError) { - break; - } - if (param.name.kind === SyntaxKind.ObjectBindingPattern || - param.name.kind === SyntaxKind.ArrayBindingPattern) { - - (function checkBindingPattern(pattern: BindingPattern) { - for (const element of pattern.elements) { - if (element.name.kind === SyntaxKind.Identifier && - (element.name).text === typePredicate.parameterName) { - - error(typePredicateNode.parameterName, - Diagnostics.A_type_predicate_cannot_reference_element_0_in_a_binding_pattern, - typePredicate.parameterName); - hasReportedError = true; - break; - } - else if (element.name.kind === SyntaxKind.ArrayBindingPattern || - element.name.kind === SyntaxKind.ObjectBindingPattern) { - - checkBindingPattern(element.name); - } - } - })(param.name); - } - } - if (!hasReportedError) { - error(typePredicateNode.parameterName, - Diagnostics.Cannot_find_parameter_0, - typePredicate.parameterName); - } + else { + // Reuse this type diagnostics on the this type node to determine if a this type predicate is valid + getTypeFromThisTypeNode(typePredicateNode.parameterName as ThisTypeNode); } } else { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 0c19f6d8247..f48a37968ca 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -1632,6 +1632,10 @@ "category": "Error", "code": 2517 }, + "A this based type guard is not assignable to a parameter based type guard": { + "category": "Error", + "code": 2518 + }, "Duplicate identifier '{0}'. Compiler uses declaration '{1}' to support async functions.": { "category": "Error", "code": 2520 diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 3a3350f2166..8b735ad0aa3 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -1959,11 +1959,7 @@ namespace ts { function parseTypeReferenceOrTypePredicate(): TypeReferenceNode | TypePredicateNode { const typeName = parseEntityName(/*allowReservedWords*/ false, Diagnostics.Type_expected); if (typeName.kind === SyntaxKind.Identifier && token === SyntaxKind.IsKeyword && !scanner.hasPrecedingLineBreak()) { - nextToken(); - const node = createNode(SyntaxKind.TypePredicate, typeName.pos); - node.parameterName = typeName; - node.type = parseType(); - return finishNode(node); + return parseTypePredicate(typeName as Identifier); } const node = createNode(SyntaxKind.TypeReference, typeName.pos); node.typeName = typeName; @@ -1973,8 +1969,16 @@ namespace ts { return finishNode(node); } - function parseThisTypeNode(): TypeNode { - const node = createNode(SyntaxKind.ThisType); + function parseTypePredicate(lhs: Identifier | ThisTypeNode): TypePredicateNode { + nextToken(); + const node = createNode(SyntaxKind.TypePredicate, lhs.pos) as TypePredicateNode; + node.parameterName = lhs; + node.type = parseType(); + return finishNode(node); + } + + function parseThisTypeNode(): ThisTypeNode { + const node = createNode(SyntaxKind.ThisType) as ThisTypeNode; nextToken(); return finishNode(node); } @@ -2420,8 +2424,15 @@ namespace ts { return parseStringLiteralTypeNode(); case SyntaxKind.VoidKeyword: return parseTokenNode(); - case SyntaxKind.ThisKeyword: - return parseThisTypeNode(); + case SyntaxKind.ThisKeyword: { + const thisKeyword = parseThisTypeNode(); + if (token === SyntaxKind.IsKeyword && !scanner.hasPrecedingLineBreak()) { + return parseTypePredicate(thisKeyword); + } + else { + return thisKeyword; + } + } case SyntaxKind.TypeOfKeyword: return parseTypeQuery(); case SyntaxKind.OpenBraceToken: diff --git a/src/compiler/types.ts b/src/compiler/types.ts index dbd0322aa18..a55373224b9 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -733,11 +733,15 @@ namespace ts { // @kind(SyntaxKind.StringKeyword) // @kind(SyntaxKind.SymbolKeyword) // @kind(SyntaxKind.VoidKeyword) - // @kind(SyntaxKind.ThisType) export interface TypeNode extends Node { _typeNodeBrand: any; } + // @kind(SyntaxKind.ThisType) + export interface ThisTypeNode extends TypeNode { + _thisTypeNodeBrand: any; + } + export interface FunctionOrConstructorTypeNode extends TypeNode, SignatureDeclaration { _functionOrConstructorTypeNodeBrand: any; } @@ -756,7 +760,7 @@ namespace ts { // @kind(SyntaxKind.TypePredicate) export interface TypePredicateNode extends TypeNode { - parameterName: Identifier; + parameterName: Identifier | ThisTypeNode; type: TypeNode; } @@ -1820,10 +1824,26 @@ namespace ts { CannotBeNamed } + /* @internal */ + export const enum TypePredicateKind { + This, + Identifier + } + export interface TypePredicate { + kind: TypePredicateKind; + type: Type; + } + + // @kind (TypePredicateKind.This) + export interface ThisTypePredicate extends TypePredicate { + _thisTypePredicateBrand: any; + } + + // @kind (TypePredicateKind.Identifier) + export interface IdentifierTypePredicate extends TypePredicate { parameterName: string; parameterIndex: number; - type: Type; } /* @internal */ @@ -2237,7 +2257,7 @@ namespace ts { declaration: SignatureDeclaration; // Originating declaration typeParameters: TypeParameter[]; // Type parameters (undefined if non-generic) parameters: Symbol[]; // Parameters - typePredicate?: TypePredicate; // Type predicate + typePredicate?: ThisTypePredicate | IdentifierTypePredicate; // Type predicate /* @internal */ resolvedReturnType: Type; // Resolved return type /* @internal */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 280b8203ff4..5a35bcbdee1 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -692,6 +692,10 @@ namespace ts { return node && node.kind === SyntaxKind.MethodDeclaration && node.parent.kind === SyntaxKind.ObjectLiteralExpression; } + export function isIdentifierTypePredicate(predicate: TypePredicate): predicate is IdentifierTypePredicate { + return predicate.kind === TypePredicateKind.Identifier; + } + export function getContainingFunction(node: Node): FunctionLikeDeclaration { while (true) { node = node.parent;