From 3d7631dbe86b5b500c6218588df0833932ca809a Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sat, 27 Feb 2016 11:39:16 -0800 Subject: [PATCH] Support dotted names ("x.y.z") in type guards --- src/compiler/checker.ts | 323 +++++++++++++++++++++++----------------- src/compiler/types.ts | 4 +- 2 files changed, 191 insertions(+), 136 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a2b8b5a2a7a..cd4a21204a2 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -69,10 +69,7 @@ namespace ts { isUnknownSymbol: symbol => symbol === unknownSymbol, getDiagnostics, getGlobalDiagnostics, - - // The language service will always care about the narrowed type of a symbol, because that is - // the type the language says the symbol should have. - getTypeOfSymbolAtLocation: getNarrowedTypeOfSymbol, + getTypeOfSymbolAtLocation, getSymbolsOfParameterPropertyDeclaration, getDeclaredTypeOfSymbol, getPropertiesOfType, @@ -6869,7 +6866,7 @@ namespace ts { function getResolvedSymbol(node: Identifier): Symbol { const links = getNodeLinks(node); if (!links.resolvedSymbol) { - links.resolvedSymbol = (!nodeIsMissing(node) && resolveName(node, node.text, SymbolFlags.Value | SymbolFlags.ExportValue, Diagnostics.Cannot_find_name_0, node)) || unknownSymbol; + links.resolvedSymbol = !nodeIsMissing(node) && resolveName(node, node.text, SymbolFlags.Value | SymbolFlags.ExportValue, Diagnostics.Cannot_find_name_0, node) || unknownSymbol; } return links.resolvedSymbol; } @@ -6893,48 +6890,59 @@ namespace ts { Debug.fail("should not get here"); } + // Return the assignment key for a "dotted name" (i.e. a sequence of identifiers + // separated by dots). The key consists of the id of the symbol referenced by the + // leftmost identifier followed by zero or more property names separated by dots. + // The result is undefined if the reference isn't a dotted name. + function getAssignmentKey(node: Node): string { + if (node.kind === SyntaxKind.Identifier) { + const symbol = getResolvedSymbol(node); + return symbol !== unknownSymbol ? "" + getSymbolId(symbol) : undefined; + } + if (node.kind === SyntaxKind.PropertyAccessExpression) { + const key = getAssignmentKey((node).expression); + return key && key + "." + (node).name.text; + } + return undefined; + } + function hasInitializer(node: VariableLikeDeclaration): boolean { return !!(node.initializer || isBindingPattern(node.parent) && hasInitializer(node.parent.parent)); } - // Check if a given variable is assigned within a given syntax node - function isVariableAssignedWithin(symbol: Symbol, node: Node): boolean { - const links = getNodeLinks(node); - if (links.assignmentChecks) { - const cachedResult = links.assignmentChecks[symbol.id]; - if (cachedResult !== undefined) { - return cachedResult; - } - } - else { - links.assignmentChecks = {}; - } - return links.assignmentChecks[symbol.id] = isAssignedIn(node); + // For a given node compute a map of which dotted names are assigned within + // the node. + function getAssignmentMap(node: Node): Map { + const assignmentMap: Map = {}; + visit(node); + return assignmentMap; - function isAssignedInBinaryExpression(node: BinaryExpression) { + function visitBinaryExpression(node: BinaryExpression) { if (node.operatorToken.kind >= SyntaxKind.FirstAssignment && node.operatorToken.kind <= SyntaxKind.LastAssignment) { - const n = skipParenthesizedNodes(node.left); - if (n.kind === SyntaxKind.Identifier && getResolvedSymbol(n) === symbol) { - return true; + const key = getAssignmentKey(skipParenthesizedNodes(node.left)); + if (key) { + assignmentMap[key] = true; } } - return forEachChild(node, isAssignedIn); + forEachChild(node, visit); } - function isAssignedInVariableDeclaration(node: VariableLikeDeclaration) { - if (!isBindingPattern(node.name) && getSymbolOfNode(node) === symbol && hasInitializer(node)) { - return true; + function visitVariableDeclaration(node: VariableLikeDeclaration) { + if (!isBindingPattern(node.name) && hasInitializer(node)) { + assignmentMap[getSymbolId(getSymbolOfNode(node))] = true; } - return forEachChild(node, isAssignedIn); + forEachChild(node, visit); } - function isAssignedIn(node: Node): boolean { + function visit(node: Node) { switch (node.kind) { case SyntaxKind.BinaryExpression: - return isAssignedInBinaryExpression(node); + visitBinaryExpression(node); + break; case SyntaxKind.VariableDeclaration: case SyntaxKind.BindingElement: - return isAssignedInVariableDeclaration(node); + visitVariableDeclaration(node); + break; case SyntaxKind.ObjectBindingPattern: case SyntaxKind.ArrayBindingPattern: case SyntaxKind.ArrayLiteralExpression: @@ -6980,9 +6988,30 @@ namespace ts { case SyntaxKind.JsxSpreadAttribute: case SyntaxKind.JsxOpeningElement: case SyntaxKind.JsxExpression: - return forEachChild(node, isAssignedIn); + forEachChild(node, visit); + break; } - return false; + } + } + + function isReferenceAssignedWithin(reference: Node, node: Node): boolean { + const key = getAssignmentKey(reference); + if (key) { + const links = getNodeLinks(node); + return (links.assignmentMap || (links.assignmentMap = getAssignmentMap(node)))[key]; + } + return false; + } + + function isAnyPartOfReferenceAssignedWithin(reference: Node, node: Node) { + while (true) { + if (isReferenceAssignedWithin(reference, node)) { + return true; + } + if (reference.kind !== SyntaxKind.PropertyAccessExpression) { + return false; + } + reference = (reference).expression; } } @@ -6991,83 +7020,112 @@ namespace ts { node.kind === SyntaxKind.Identifier && getResolvedSymbol(node) === undefinedSymbol; } - // Get the narrowed type of a given symbol at a given location - function getNarrowedTypeOfSymbol(symbol: Symbol, node: Node) { - let type = getTypeOfSymbol(symbol); - // Only narrow when symbol is variable of type any or an object, union, or type parameter type - if (node && symbol.flags & SymbolFlags.Variable) { - if (isTypeAny(type) || type.flags & (TypeFlags.ObjectType | TypeFlags.Union | TypeFlags.TypeParameter)) { - const declaration = getDeclarationOfKind(symbol, SyntaxKind.VariableDeclaration); - const top = declaration && getDeclarationContainer(declaration); - const originalType = type; - const nodeStack: {node: Node, child: Node}[] = []; - loop: while (node.parent) { - const child = node; - node = node.parent; - switch (node.kind) { - case SyntaxKind.IfStatement: - case SyntaxKind.ConditionalExpression: - case SyntaxKind.BinaryExpression: - nodeStack.push({node, child}); - break; - case SyntaxKind.SourceFile: - case SyntaxKind.ModuleDeclaration: - // Stop at the first containing file or module declaration - break loop; - } - if (node === top) { - break; - } - } + function getLeftmostIdentifier(node: Node): Identifier { + switch (node.kind) { + case SyntaxKind.Identifier: + return node; + case SyntaxKind.PropertyAccessExpression: + return getLeftmostIdentifier((node).expression); + } + return undefined; + } - let nodes: {node: Node, child: Node}; - while (nodes = nodeStack.pop()) { - const {node, child} = nodes; - switch (node.kind) { - case SyntaxKind.IfStatement: - // In a branch of an if statement, narrow based on controlling expression - if (child !== (node).expression) { - type = narrowType(type, (node).expression, /*assumeTrue*/ child === (node).thenStatement); - } - break; - case SyntaxKind.ConditionalExpression: - // In a branch of a conditional expression, narrow based on controlling condition - if (child !== (node).condition) { - type = narrowType(type, (node).condition, /*assumeTrue*/ 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).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken) { - type = narrowType(type, (node).left, /*assumeTrue*/ true); - } - else if ((node).operatorToken.kind === SyntaxKind.BarBarToken) { - type = narrowType(type, (node).left, /*assumeTrue*/ false); - } - } - break; - default: - Debug.fail("Unreachable!"); - } - - // Use original type if construct contains assignments to variable - if (type !== originalType && isVariableAssignedWithin(symbol, node)) { - type = originalType; - } - } - - // Preserve old top-level behavior - if the branch is really an empty set, revert to prior type - if (type === emptyUnionType) { - type = originalType; - } + function isMatchingReference(source: Node, target: Node): boolean { + if (source.kind === target.kind) { + if (source.kind === SyntaxKind.Identifier) { + return getResolvedSymbol(source) === getResolvedSymbol(target); } + if (source.kind === SyntaxKind.PropertyAccessExpression) { + return (source).name.text === (target).name.text && + isMatchingReference((source).expression, (target).expression); + } + } + return false; + } + + // Get the narrowed type of a given symbol at a given location + function getNarrowedTypeOfReference(type: Type, reference: IdentifierOrPropertyAccess) { + if (!(type.flags & (TypeFlags.Any | TypeFlags.ObjectType | TypeFlags.Union | TypeFlags.TypeParameter))) { + return type; + } + const leftmostIdentifier = getLeftmostIdentifier(reference); + if (!leftmostIdentifier) { + return type; + } + const leftmostSymbol = getResolvedSymbol(leftmostIdentifier); + if (!(leftmostSymbol.flags & SymbolFlags.Variable)) { + return type; + } + const declaration = getDeclarationOfKind(leftmostSymbol, SyntaxKind.VariableDeclaration); + const top = declaration && getDeclarationContainer(declaration); + const originalType = type; + const nodeStack: { node: Node, child: Node }[] = []; + let node: Node = reference; + loop: while (node.parent) { + const child = node; + node = node.parent; + switch (node.kind) { + case SyntaxKind.IfStatement: + case SyntaxKind.ConditionalExpression: + case SyntaxKind.BinaryExpression: + nodeStack.push({node, child}); + break; + case SyntaxKind.SourceFile: + case SyntaxKind.ModuleDeclaration: + // Stop at the first containing file or module declaration + break loop; + } + if (node === top) { + break; + } + } + + let nodes: { node: Node, child: Node }; + while (nodes = nodeStack.pop()) { + const {node, child} = nodes; + switch (node.kind) { + case SyntaxKind.IfStatement: + // In a branch of an if statement, narrow based on controlling expression + if (child !== (node).expression) { + type = narrowType(type, (node).expression, /*assumeTrue*/ child === (node).thenStatement); + } + break; + case SyntaxKind.ConditionalExpression: + // In a branch of a conditional expression, narrow based on controlling condition + if (child !== (node).condition) { + type = narrowType(type, (node).condition, /*assumeTrue*/ 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).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken) { + type = narrowType(type, (node).left, /*assumeTrue*/ true); + } + else if ((node).operatorToken.kind === SyntaxKind.BarBarToken) { + type = narrowType(type, (node).left, /*assumeTrue*/ false); + } + } + break; + default: + Debug.fail("Unreachable!"); + } + + // Use original type if construct contains assignments to variable + if (type !== originalType && isAnyPartOfReferenceAssignedWithin(reference, node)) { + type = originalType; + } + } + + // Preserve old top-level behavior - if the branch is really an empty set, revert to prior type + if (type === emptyUnionType) { + type = originalType; } return type; function narrowTypeByTruthiness(type: Type, expr: Identifier, assumeTrue: boolean): Type { - return strictNullChecks && assumeTrue && getResolvedSymbol(expr) === symbol ? getNonNullableType(type) : type; + return strictNullChecks && assumeTrue && isMatchingReference(expr, reference) ? getNonNullableType(type) : type; } function narrowTypeByBinaryExpression(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { @@ -7095,14 +7153,11 @@ namespace ts { } function narrowTypeByNullCheck(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { - // We have '==' or '!=' operator with 'null' on the right + // We have '==' or '!=' operator with 'null' or 'undefined' on the right if (expr.operatorToken.kind === SyntaxKind.ExclamationEqualsToken) { assumeTrue = !assumeTrue; } - if (!strictNullChecks || assumeTrue) { - return type; - } - if (expr.left.kind !== SyntaxKind.Identifier || getResolvedSymbol(expr.left) !== symbol) { + if (!strictNullChecks || assumeTrue || !isMatchingReference(expr.left, reference)) { return type; } return getNonNullableType(type); @@ -7113,7 +7168,7 @@ namespace ts { // and string literal on the right const left = expr.left; const right = expr.right; - if (left.expression.kind !== SyntaxKind.Identifier || getResolvedSymbol(left.expression) !== symbol) { + if (!isMatchingReference(left.expression, reference)) { return type; } if (expr.operatorToken.kind === SyntaxKind.ExclamationEqualsToken || @@ -7181,7 +7236,7 @@ namespace ts { function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { // Check that type is not any, assumed result is true, and we have variable symbol on the left - if (isTypeAny(type) || expr.left.kind !== SyntaxKind.Identifier || getResolvedSymbol(expr.left) !== symbol) { + if (isTypeAny(type) || !isMatchingReference(expr.left, reference)) { return type; } @@ -7252,50 +7307,35 @@ namespace ts { return type; } const signature = getResolvedSignature(callExpression); - const predicate = signature.typePredicate; if (!predicate) { return type; } - if (isIdentifierTypePredicate(predicate)) { - if (callExpression.arguments[predicate.parameterIndex] && - getSymbolAtTypePredicatePosition(callExpression.arguments[predicate.parameterIndex]) === symbol) { + const predicateArgument = callExpression.arguments[predicate.parameterIndex]; + if (predicateArgument && isMatchingReference(predicateArgument, reference)) { return getNarrowedType(type, predicate.type, assumeTrue); } } else { const invokedExpression = skipParenthesizedNodes(callExpression.expression); - return narrowTypeByThisTypePredicate(type, predicate, invokedExpression, assumeTrue); - } - return type; - } - - function narrowTypeByThisTypePredicate(type: Type, predicate: ThisTypePredicate, invokedExpression: Expression, assumeTrue: boolean): Type { - if (invokedExpression.kind === SyntaxKind.ElementAccessExpression || invokedExpression.kind === SyntaxKind.PropertyAccessExpression) { - const accessExpression = invokedExpression as ElementAccessExpression | PropertyAccessExpression; - const possibleIdentifier = skipParenthesizedNodes(accessExpression.expression); - if (possibleIdentifier.kind === SyntaxKind.Identifier && getSymbolAtTypePredicatePosition(possibleIdentifier) === symbol) { - return getNarrowedType(type, predicate.type, assumeTrue); + if (invokedExpression.kind === SyntaxKind.ElementAccessExpression || invokedExpression.kind === SyntaxKind.PropertyAccessExpression) { + const accessExpression = invokedExpression as ElementAccessExpression | PropertyAccessExpression; + const possibleReference= skipParenthesizedNodes(accessExpression.expression); + if (isMatchingReference(possibleReference, reference)) { + return getNarrowedType(type, predicate.type, assumeTrue); + } } } return type; } - function getSymbolAtTypePredicatePosition(expr: Expression): Symbol { - expr = skipParenthesizedNodes(expr); - switch (expr.kind) { - case SyntaxKind.Identifier: - case SyntaxKind.PropertyAccessExpression: - return getSymbolOfEntityNameOrPropertyAccessExpression(expr as (Identifier | PropertyAccessExpression)); - } - } - // Narrow the given type based on the given expression having the assumed boolean value. The returned type // will be a subtype or the same type as the argument. function narrowType(type: Type, expr: Expression, assumeTrue: boolean): Type { switch (expr.kind) { case SyntaxKind.Identifier: + case SyntaxKind.PropertyAccessExpression: return narrowTypeByTruthiness(type, expr, assumeTrue); case SyntaxKind.CallExpression: return narrowTypeByTypePredicate(type, expr, assumeTrue); @@ -7313,6 +7353,16 @@ namespace ts { } } + function getTypeOfSymbolAtLocation(symbol: Symbol, location: Node) { + // The language service will always care about the narrowed type of a symbol, because that is + // the type the language says the symbol should have. + let type = getTypeOfSymbol(symbol); + if (location.kind === SyntaxKind.Identifier && isExpression(location) && getResolvedSymbol(location) === symbol) { + type = getNarrowedTypeOfReference(type, location); + } + return type; + } + function skipParenthesizedNodes(expression: Expression): Expression { while (expression.kind === SyntaxKind.ParenthesizedExpression) { expression = (expression as ParenthesizedExpression).expression; @@ -7371,7 +7421,7 @@ namespace ts { checkCollisionWithCapturedThisVariable(node, node); checkNestedBlockScopedBinding(node, symbol); - return getNarrowedTypeOfSymbol(localOrExportSymbol, node); + return getNarrowedTypeOfReference(getTypeOfSymbol(localOrExportSymbol), node); } function isInsideFunction(node: Node, threshold: Node): boolean { @@ -9122,7 +9172,10 @@ namespace ts { if (prop.parent && prop.parent.flags & SymbolFlags.Class) { checkClassPropertyAccess(node, left, apparentType, prop); } - return getTypeOfSymbol(prop); + + const propType = getTypeOfSymbol(prop); + return node.kind === SyntaxKind.PropertyAccessExpression && prop.flags & SymbolFlags.Property ? + getNarrowedTypeOfReference(propType, node) : propType; } function isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName, propertyName: string): boolean { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 380bbdb3b90..b4b81c2732f 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -974,6 +974,8 @@ namespace ts { name: Identifier; } + export type IdentifierOrPropertyAccess = Identifier | PropertyAccessExpression; + // @kind(SyntaxKind.ElementAccessExpression) export interface ElementAccessExpression extends MemberExpression { expression: LeftHandSideExpression; @@ -2071,7 +2073,7 @@ namespace ts { isVisible?: boolean; // Is this node visible generatedName?: string; // Generated name for module, enum, or import declaration generatedNames?: Map; // Generated names table for source file - assignmentChecks?: Map; // Cache of assignment checks + assignmentMap?: Map; // Cached map of references assigned within this node hasReportedStatementInAmbientContext?: boolean; // Cache boolean if we report statements in ambient context importOnRightSide?: Symbol; // for import declarations - import that appear on the right side jsxFlags?: JsxFlags; // flags for knowing what kind of element/attributes we're dealing with