diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 6957be484f3..b49e22e8192 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -11,19 +11,11 @@ namespace ts { ConstEnumOnly = 2 } - const enum Reachability { - Uninitialized = 1 << 0, - Reachable = 1 << 1, - Unreachable = 1 << 2, - ReportedUnreachable = 1 << 3 - } - - function or(state1: Reachability, state2: Reachability): Reachability { - return (state1 | state2) & Reachability.Reachable - ? Reachability.Reachable - : (state1 & state2) & Reachability.ReportedUnreachable - ? Reachability.ReportedUnreachable - : Reachability.Unreachable; + interface ActiveLabel { + name: string; + breakTarget: FlowLabel; + continueTarget: FlowLabel; + referenced: boolean; } export function getModuleInstanceState(node: Node): ModuleInstanceState { @@ -112,10 +104,11 @@ namespace ts { // state used by reachability checks let hasExplicitReturn: boolean; - let currentReachabilityState: Reachability; - let labelStack: Reachability[]; - let labelIndexMap: Map; - let implicitLabels: number[]; + let currentFlow: FlowNode; + let breakTarget: FlowLabel; + let continueTarget: FlowLabel; + let preSwitchCaseFlow: FlowNode; + let activeLabels: ActiveLabel[]; // state used for emit helpers let hasClassExtends: boolean; @@ -132,6 +125,9 @@ namespace ts { let Symbol: { new (flags: SymbolFlags, name: string): Symbol }; let classifiableNames: Map; + const unreachableFlow: FlowNode = { kind: FlowKind.Unreachable }; + const reportedUncreachableFlow: FlowNode = { kind: FlowKind.Unreachable }; + function bindSourceFile(f: SourceFile, opts: CompilerOptions) { file = f; options = opts; @@ -154,9 +150,10 @@ namespace ts { lastContainer = undefined; seenThisKeyword = false; hasExplicitReturn = false; - labelStack = undefined; - labelIndexMap = undefined; - implicitLabels = undefined; + currentFlow = undefined; + breakTarget = undefined; + continueTarget = undefined; + activeLabels = undefined; hasClassExtends = false; hasAsyncFunctions = false; hasDecorators = false; @@ -436,11 +433,11 @@ namespace ts { blockScopeContainer.locals = undefined; } - let savedReachabilityState: Reachability; - let savedLabelStack: Reachability[]; - let savedLabels: Map; - let savedImplicitLabels: number[]; let savedHasExplicitReturn: boolean; + let savedCurrentFlow: FlowNode; + let savedBreakTarget: FlowLabel; + let savedContinueTarget: FlowLabel; + let savedActiveLabels: ActiveLabel[]; const kind = node.kind; let flags = node.flags; @@ -457,15 +454,17 @@ namespace ts { const saveState = kind === SyntaxKind.SourceFile || kind === SyntaxKind.ModuleBlock || isFunctionLikeKind(kind); if (saveState) { - savedReachabilityState = currentReachabilityState; - savedLabelStack = labelStack; - savedLabels = labelIndexMap; - savedImplicitLabels = implicitLabels; savedHasExplicitReturn = hasExplicitReturn; + savedCurrentFlow = currentFlow; + savedBreakTarget = breakTarget; + savedContinueTarget = continueTarget; + savedActiveLabels = activeLabels; - currentReachabilityState = Reachability.Reachable; hasExplicitReturn = false; - labelStack = labelIndexMap = implicitLabels = undefined; + currentFlow = { kind: FlowKind.Start }; + breakTarget = undefined; + continueTarget = undefined; + activeLabels = undefined; } if (isInJavaScriptFile(node) && node.jsDocComment) { @@ -474,7 +473,7 @@ namespace ts { bindReachableStatement(node); - if (currentReachabilityState === Reachability.Reachable && isFunctionLikeKind(kind) && nodeIsPresent((node).body)) { + if (currentFlow.kind !== FlowKind.Unreachable && isFunctionLikeKind(kind) && nodeIsPresent((node).body)) { flags |= NodeFlags.HasImplicitReturn; if (hasExplicitReturn) { flags |= NodeFlags.HasExplicitReturn; @@ -503,11 +502,11 @@ namespace ts { node.flags = flags; if (saveState) { + activeLabels = savedActiveLabels; + continueTarget = savedContinueTarget; + breakTarget = savedBreakTarget; + currentFlow = savedCurrentFlow; hasExplicitReturn = savedHasExplicitReturn; - currentReachabilityState = savedReachabilityState; - labelStack = savedLabelStack; - labelIndexMap = savedLabels; - implicitLabels = savedImplicitLabels; } container = saveContainer; @@ -562,174 +561,381 @@ namespace ts { case SyntaxKind.LabeledStatement: bindLabeledStatement(node); break; + case SyntaxKind.BinaryExpression: + bindBinaryExpressionFlow(node); + break; + case SyntaxKind.ConditionalExpression: + bindConditionalExpressionFlow(node); + break; + case SyntaxKind.VariableDeclaration: + bindVariableDeclarationFlow(node); + break; default: forEachChild(node, bind); break; } } - function bindWhileStatement(n: WhileStatement): void { - const preWhileState = - n.expression.kind === SyntaxKind.FalseKeyword ? Reachability.Unreachable : currentReachabilityState; - const postWhileState = - n.expression.kind === SyntaxKind.TrueKeyword ? Reachability.Unreachable : currentReachabilityState; - - // bind expressions (don't affect reachability) - bind(n.expression); - - currentReachabilityState = preWhileState; - const postWhileLabel = pushImplicitLabel(); - bind(n.statement); - popImplicitLabel(postWhileLabel, postWhileState); + function isNarrowableReference(expr: Expression): boolean { + return expr.kind === SyntaxKind.Identifier || + expr.kind === SyntaxKind.ThisKeyword || + expr.kind === SyntaxKind.PropertyAccessExpression && isNarrowableReference((expr).expression); } - function bindDoStatement(n: DoStatement): void { - const preDoState = currentReachabilityState; - - const postDoLabel = pushImplicitLabel(); - bind(n.statement); - const postDoState = n.expression.kind === SyntaxKind.TrueKeyword ? Reachability.Unreachable : preDoState; - popImplicitLabel(postDoLabel, postDoState); - - // bind expressions (don't affect reachability) - bind(n.expression); - } - - function bindForStatement(n: ForStatement): void { - const preForState = currentReachabilityState; - const postForLabel = pushImplicitLabel(); - - // bind expressions (don't affect reachability) - bind(n.initializer); - bind(n.condition); - bind(n.incrementor); - - bind(n.statement); - - // for statement is considered infinite when it condition is either omitted or is true keyword - // - for(..;;..) - // - for(..;true;..) - const isInfiniteLoop = (!n.condition || n.condition.kind === SyntaxKind.TrueKeyword); - const postForState = isInfiniteLoop ? Reachability.Unreachable : preForState; - popImplicitLabel(postForLabel, postForState); - } - - function bindForInOrForOfStatement(n: ForInStatement | ForOfStatement): void { - const preStatementState = currentReachabilityState; - const postStatementLabel = pushImplicitLabel(); - - // bind expressions (don't affect reachability) - bind(n.initializer); - bind(n.expression); - - bind(n.statement); - popImplicitLabel(postStatementLabel, preStatementState); - } - - function bindIfStatement(n: IfStatement): void { - // denotes reachability state when entering 'thenStatement' part of the if statement: - // i.e. if condition is false then thenStatement is unreachable - const ifTrueState = n.expression.kind === SyntaxKind.FalseKeyword ? Reachability.Unreachable : currentReachabilityState; - // denotes reachability state when entering 'elseStatement': - // i.e. if condition is true then elseStatement is unreachable - const ifFalseState = n.expression.kind === SyntaxKind.TrueKeyword ? Reachability.Unreachable : currentReachabilityState; - - currentReachabilityState = ifTrueState; - - // bind expression (don't affect reachability) - bind(n.expression); - - bind(n.thenStatement); - if (n.elseStatement) { - const preElseState = currentReachabilityState; - currentReachabilityState = ifFalseState; - bind(n.elseStatement); - currentReachabilityState = or(currentReachabilityState, preElseState); + function isNarrowingExpression(expr: Expression): boolean { + switch (expr.kind) { + case SyntaxKind.Identifier: + case SyntaxKind.ThisKeyword: + case SyntaxKind.PropertyAccessExpression: + return isNarrowableReference(expr); + case SyntaxKind.CallExpression: + return true; + case SyntaxKind.ParenthesizedExpression: + return isNarrowingExpression((expr).expression); + case SyntaxKind.BinaryExpression: + return isNarrowingBinaryExpression(expr); + case SyntaxKind.PrefixUnaryExpression: + return (expr).operator === SyntaxKind.ExclamationToken && isNarrowingExpression((expr).operand); } - else { - currentReachabilityState = or(currentReachabilityState, ifFalseState); + return false; + } + + function isNarrowingBinaryExpression(expr: BinaryExpression) { + switch (expr.operatorToken.kind) { + case SyntaxKind.EqualsEqualsToken: + case SyntaxKind.ExclamationEqualsToken: + case SyntaxKind.EqualsEqualsEqualsToken: + case SyntaxKind.ExclamationEqualsEqualsToken: + if (isNarrowingExpression(expr.left) && (expr.right.kind === SyntaxKind.NullKeyword || expr.right.kind === SyntaxKind.Identifier)) { + return true; + } + if (expr.left.kind === SyntaxKind.TypeOfExpression && isNarrowingExpression((expr.left).expression) && expr.right.kind === SyntaxKind.StringLiteral) { + return true; + } + return false; + case SyntaxKind.AmpersandAmpersandToken: + case SyntaxKind.BarBarToken: + return isNarrowingExpression(expr.left) || isNarrowingExpression(expr.right); + case SyntaxKind.InstanceOfKeyword: + return isNarrowingExpression(expr.left); + } + return false; + } + + function createFlowLabel(): FlowLabel { + return { + kind: FlowKind.Label, + antecedents: undefined + }; + } + + function addAntecedent(label: FlowLabel, antecedent: FlowNode): void { + if (antecedent.kind !== FlowKind.Unreachable && !contains(label.antecedents, antecedent)) { + (label.antecedents || (label.antecedents = [])).push(antecedent); } } - function bindReturnOrThrow(n: ReturnStatement | ThrowStatement): void { - // bind expression (don't affect reachability) - bind(n.expression); - if (n.kind === SyntaxKind.ReturnStatement) { + function createFlowCondition(antecedent: FlowNode, expression: Expression, assumeTrue: boolean): FlowNode { + if (!expression) { + return assumeTrue ? antecedent : unreachableFlow; + } + if (expression.kind === SyntaxKind.TrueKeyword && !assumeTrue || expression.kind === SyntaxKind.FalseKeyword && assumeTrue) { + return unreachableFlow; + } + if (!isNarrowingExpression(expression)) { + return antecedent; + } + return { + kind: FlowKind.Condition, + antecedent, + expression, + assumeTrue + }; + } + + function createFlowAssignment(antecedent: FlowNode, node: BinaryExpression | VariableDeclaration | ForInStatement | ForOfStatement): FlowNode { + return { + kind: FlowKind.Assignment, + antecedent, + node + }; + } + + function finishFlow(flow: FlowNode): FlowNode { + while (flow.kind === FlowKind.Label) { + const antecedents = (flow).antecedents; + if (!antecedents) { + return unreachableFlow; + } + if (antecedents.length > 1) { + break; + } + flow = antecedents[0]; + } + return flow; + } + + function bindWhileStatement(node: WhileStatement): void { + const preWhileLabel = createFlowLabel(); + const postWhileLabel = createFlowLabel(); + addAntecedent(preWhileLabel, currentFlow); + currentFlow = preWhileLabel; + bind(node.expression); + addAntecedent(postWhileLabel, createFlowCondition(currentFlow, node.expression, /*assumeTrue*/ false)); + currentFlow = createFlowCondition(currentFlow, node.expression, /*assumeTrue*/ true); + const saveBreakTarget = breakTarget; + const saveContinueTarget = continueTarget; + breakTarget = postWhileLabel; + continueTarget = preWhileLabel; + bind(node.statement); + breakTarget = saveBreakTarget; + continueTarget = saveContinueTarget; + addAntecedent(preWhileLabel, currentFlow); + currentFlow = finishFlow(postWhileLabel); + } + + function bindDoStatement(node: DoStatement): void { + const preDoLabel = createFlowLabel(); + const postDoLabel = createFlowLabel(); + addAntecedent(preDoLabel, currentFlow); + currentFlow = preDoLabel; + const saveBreakTarget = breakTarget; + const saveContinueTarget = continueTarget; + breakTarget = postDoLabel; + continueTarget = preDoLabel; + bind(node.statement); + breakTarget = saveBreakTarget; + continueTarget = saveContinueTarget; + bind(node.expression); + addAntecedent(preDoLabel, createFlowCondition(currentFlow, node.expression, /*assumeTrue*/ true)); + addAntecedent(postDoLabel, createFlowCondition(currentFlow, node.expression, /*assumeTrue*/ false)); + currentFlow = finishFlow(postDoLabel); + } + + function bindForStatement(node: ForStatement): void { + const preLoopLabel = createFlowLabel(); + const postLoopLabel = createFlowLabel(); + bind(node.initializer); + addAntecedent(preLoopLabel, currentFlow); + currentFlow = preLoopLabel; + bind(node.condition); + addAntecedent(postLoopLabel, createFlowCondition(currentFlow, node.condition, /*assumeTrue*/ false)); + currentFlow = createFlowCondition(currentFlow, node.condition, /*assumeTrue*/ true); + const saveBreakTarget = breakTarget; + const saveContinueTarget = continueTarget; + breakTarget = postLoopLabel; + continueTarget = preLoopLabel; + bind(node.statement); + bind(node.incrementor); + breakTarget = saveBreakTarget; + continueTarget = saveContinueTarget; + addAntecedent(preLoopLabel, currentFlow); + currentFlow = finishFlow(postLoopLabel); + } + + function bindForInOrForOfStatement(node: ForInStatement | ForOfStatement): void { + const preLoopLabel = createFlowLabel(); + const postLoopLabel = createFlowLabel(); + bind(node.initializer); + bind(node.expression); + addAntecedent(preLoopLabel, currentFlow); + addAntecedent(postLoopLabel, currentFlow); + currentFlow = preLoopLabel; + const saveBreakTarget = breakTarget; + const saveContinueTarget = continueTarget; + breakTarget = postLoopLabel; + continueTarget = preLoopLabel; + currentFlow = createFlowAssignment(currentFlow, node); + bind(node.statement); + breakTarget = saveBreakTarget; + continueTarget = saveContinueTarget; + addAntecedent(preLoopLabel, currentFlow); + addAntecedent(postLoopLabel, currentFlow); + currentFlow = finishFlow(postLoopLabel); + } + + function bindIfStatement(node: IfStatement): void { + const postIfLabel = createFlowLabel(); + bind(node.expression); + const postConditionFlow = currentFlow; + currentFlow = createFlowCondition(currentFlow, node.expression, /*assumeTrue*/ true); + bind(node.thenStatement); + addAntecedent(postIfLabel, currentFlow); + currentFlow = createFlowCondition(postConditionFlow, node.expression, /*assumeTrue*/ false); + bind(node.elseStatement); + addAntecedent(postIfLabel, currentFlow); + currentFlow = finishFlow(postIfLabel); + } + + function bindReturnOrThrow(node: ReturnStatement | ThrowStatement): void { + bind(node.expression); + if (node.kind === SyntaxKind.ReturnStatement) { hasExplicitReturn = true; } - currentReachabilityState = Reachability.Unreachable; + currentFlow = unreachableFlow; } - function bindBreakOrContinueStatement(n: BreakOrContinueStatement): void { - // call bind on label (don't affect reachability) - bind(n.label); - // for continue case touch label so it will be marked a used - const isValidJump = jumpToLabel(n.label, n.kind === SyntaxKind.BreakStatement ? currentReachabilityState : Reachability.Unreachable); - if (isValidJump) { - currentReachabilityState = Reachability.Unreachable; + function findActiveLabel(name: string) { + if (activeLabels) { + for (const label of activeLabels) { + if (label.name === name) { + return label; + } + } + } + return undefined; + } + + function bindbreakOrContinueFlow(node: BreakOrContinueStatement, breakTarget: FlowLabel, continueTarget: FlowLabel) { + const flowLabel = node.kind === SyntaxKind.BreakStatement ? breakTarget : continueTarget; + if (flowLabel) { + addAntecedent(flowLabel, currentFlow); + currentFlow = unreachableFlow; } } - function bindTryStatement(n: TryStatement): void { - // catch\finally blocks has the same reachability as try block - const preTryState = currentReachabilityState; - bind(n.tryBlock); - const postTryState = currentReachabilityState; - - currentReachabilityState = preTryState; - bind(n.catchClause); - const postCatchState = currentReachabilityState; - - currentReachabilityState = preTryState; - bind(n.finallyBlock); - - // post catch/finally state is reachable if - // - post try state is reachable - control flow can fall out of try block - // - post catch state is reachable - control flow can fall out of catch block - currentReachabilityState = n.catchClause ? or(postTryState, postCatchState) : postTryState; + function bindBreakOrContinueStatement(node: BreakOrContinueStatement): void { + bind(node.label); + if (node.label) { + const activeLabel = findActiveLabel(node.label.text); + if (activeLabel) { + activeLabel.referenced = true; + bindbreakOrContinueFlow(node, activeLabel.breakTarget, activeLabel.continueTarget); + } + } + else { + bindbreakOrContinueFlow(node, breakTarget, continueTarget); + } } - function bindSwitchStatement(n: SwitchStatement): void { - const preSwitchState = currentReachabilityState; - const postSwitchLabel = pushImplicitLabel(); - - // bind expression (don't affect reachability) - bind(n.expression); - - bind(n.caseBlock); - - const hasDefault = forEach(n.caseBlock.clauses, c => c.kind === SyntaxKind.DefaultClause); - - // post switch state is unreachable if switch is exhaustive (has a default case ) and does not have fallthrough from the last case - const postSwitchState = hasDefault && currentReachabilityState !== Reachability.Reachable ? Reachability.Unreachable : preSwitchState; - - popImplicitLabel(postSwitchLabel, postSwitchState); + function bindTryStatement(node: TryStatement): void { + const postFinallyLabel = createFlowLabel(); + const preTryFlow = currentFlow; + // TODO: Every statement in try block is potentially an exit point! + bind(node.tryBlock); + addAntecedent(postFinallyLabel, currentFlow); + if (node.catchClause) { + currentFlow = preTryFlow; + bind(node.catchClause); + addAntecedent(postFinallyLabel, currentFlow); + } + if (node.finallyBlock) { + currentFlow = preTryFlow; + bind(node.finallyBlock); + } + currentFlow = finishFlow(postFinallyLabel); } - function bindCaseBlock(n: CaseBlock): void { - const startState = currentReachabilityState; + function bindSwitchStatement(node: SwitchStatement): void { + const postSwitchLabel = createFlowLabel(); + bind(node.expression); + const saveBreakTarget = breakTarget; + const savePreSwitchCaseFlow = preSwitchCaseFlow; + breakTarget = postSwitchLabel; + preSwitchCaseFlow = currentFlow; + bind(node.caseBlock); + addAntecedent(postSwitchLabel, currentFlow); + const hasDefault = forEach(node.caseBlock.clauses, c => c.kind === SyntaxKind.DefaultClause); + if (!hasDefault) { + addAntecedent(postSwitchLabel, preSwitchCaseFlow); + } + breakTarget = saveBreakTarget; + preSwitchCaseFlow = savePreSwitchCaseFlow; + currentFlow = finishFlow(postSwitchLabel); + } - for (let i = 0; i < n.clauses.length; i++) { - const clause = n.clauses[i]; - currentReachabilityState = startState; - bind(clause); - if (clause.statements.length && - i !== n.clauses.length - 1 && // allow fallthrough from the last case - currentReachabilityState === Reachability.Reachable && - options.noFallthroughCasesInSwitch) { - errorOnFirstToken(clause, Diagnostics.Fallthrough_case_in_switch); + function bindCaseBlock(node: CaseBlock): void { + const clauses = node.clauses; + for (let i = 0; i < clauses.length; i++) { + const clause = clauses[i]; + if (clause.statements.length) { + if (currentFlow.kind === FlowKind.Unreachable) { + currentFlow = preSwitchCaseFlow; + } + else { + const preCaseLabel = createFlowLabel(); + addAntecedent(preCaseLabel, preSwitchCaseFlow); + addAntecedent(preCaseLabel, currentFlow); + currentFlow = finishFlow(preCaseLabel); + } + bind(clause); + if (currentFlow.kind !== FlowKind.Unreachable && i !== clauses.length - 1 && options.noFallthroughCasesInSwitch) { + errorOnFirstToken(clause, Diagnostics.Fallthrough_case_in_switch); + } + } + else { + bind(clause); } } } - function bindLabeledStatement(n: LabeledStatement): void { - // call bind on label (don't affect reachability) - bind(n.label); + function pushActiveLabel(name: string, breakTarget: FlowLabel, continueTarget: FlowLabel): ActiveLabel { + const activeLabel = { + name, + breakTarget, + continueTarget, + referenced: false + }; + (activeLabels || (activeLabels = [])).push(activeLabel); + return activeLabel; + } - const ok = pushNamedLabel(n.label); - bind(n.statement); - if (ok) { - popNamedLabel(n.label, currentReachabilityState); + function popActiveLabel() { + activeLabels.pop(); + } + + function bindLabeledStatement(node: LabeledStatement): void { + const preStatementLabel = createFlowLabel(); + const postStatementLabel = createFlowLabel(); + bind(node.label); + addAntecedent(preStatementLabel, currentFlow); + const activeLabel = pushActiveLabel(node.label.text, postStatementLabel, preStatementLabel); + bind(node.statement); + popActiveLabel(); + if (!activeLabel.referenced && !options.allowUnusedLabels) { + file.bindDiagnostics.push(createDiagnosticForNode(node.label, Diagnostics.Unused_label)); + } + addAntecedent(postStatementLabel, currentFlow); + currentFlow = finishFlow(postStatementLabel); + } + + function bindBinaryExpressionFlow(node: BinaryExpression) { + const operator = node.operatorToken.kind; + if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken) { + const postExpressionLabel = createFlowLabel(); + bind(node.left); + bind(node.operatorToken); + addAntecedent(postExpressionLabel, currentFlow); + currentFlow = createFlowCondition(currentFlow, node.left, /*assumeTrue*/ operator === SyntaxKind.AmpersandAmpersandToken); + bind(node.right); + addAntecedent(postExpressionLabel, currentFlow); + currentFlow = finishFlow(postExpressionLabel); + } + else { + forEachChild(node, bind); + if (operator === SyntaxKind.EqualsToken) { + currentFlow = createFlowAssignment(currentFlow, node); + } + } + } + + function bindConditionalExpressionFlow(node: ConditionalExpression) { + const postExpressionLabel = createFlowLabel(); + bind(node.condition); + const postConditionFlow = currentFlow; + currentFlow = createFlowCondition(currentFlow, node.condition, /*assumeTrue*/ true); + bind(node.whenTrue); + addAntecedent(postExpressionLabel, currentFlow); + currentFlow = createFlowCondition(postConditionFlow, node.condition, /*assumeTrue*/ false); + bind(node.whenFalse); + addAntecedent(postExpressionLabel, currentFlow); + currentFlow = finishFlow(postExpressionLabel); + } + + function bindVariableDeclarationFlow(node: VariableDeclaration) { + forEachChild(node, bind); + if (node.initializer) { + currentFlow = createFlowAssignment(currentFlow, node); } } @@ -1239,7 +1445,16 @@ namespace ts { switch (node.kind) { /* Strict mode checks */ case SyntaxKind.Identifier: + case SyntaxKind.ThisKeyword: + if (currentFlow && (isExpression(node) || parent.kind === SyntaxKind.ShorthandPropertyAssignment)) { + node.flowNode = currentFlow; + } return checkStrictModeIdentifier(node); + case SyntaxKind.PropertyAccessExpression: + if (currentFlow && isNarrowableReference(node)) { + node.flowNode = currentFlow; + } + break; case SyntaxKind.BinaryExpression: if (isInJavaScriptFile(node)) { const specialKind = getSpecialPropertyAssignmentKind(node); @@ -1663,132 +1878,53 @@ namespace ts { // reachability checks - function pushNamedLabel(name: Identifier): boolean { - initializeReachabilityStateIfNecessary(); - - if (hasProperty(labelIndexMap, name.text)) { - return false; - } - labelIndexMap[name.text] = labelStack.push(Reachability.Uninitialized) - 1; - return true; - } - - function pushImplicitLabel(): number { - initializeReachabilityStateIfNecessary(); - - const index = labelStack.push(Reachability.Uninitialized) - 1; - implicitLabels.push(index); - return index; - } - - function popNamedLabel(label: Identifier, outerState: Reachability): void { - const index = labelIndexMap[label.text]; - Debug.assert(index !== undefined); - Debug.assert(labelStack.length == index + 1); - - labelIndexMap[label.text] = undefined; - - setCurrentStateAtLabel(labelStack.pop(), outerState, label); - } - - function popImplicitLabel(implicitLabelIndex: number, outerState: Reachability): void { - if (labelStack.length !== implicitLabelIndex + 1) { - Debug.assert(false, `Label stack: ${labelStack.length}, index:${implicitLabelIndex}`); - } - - const i = implicitLabels.pop(); - - if (implicitLabelIndex !== i) { - Debug.assert(false, `i: ${i}, index: ${implicitLabelIndex}`); - } - - setCurrentStateAtLabel(labelStack.pop(), outerState, /*name*/ undefined); - } - - function setCurrentStateAtLabel(innerMergedState: Reachability, outerState: Reachability, label: Identifier): void { - if (innerMergedState === Reachability.Uninitialized) { - if (label && !options.allowUnusedLabels) { - file.bindDiagnostics.push(createDiagnosticForNode(label, Diagnostics.Unused_label)); - } - currentReachabilityState = outerState; - } - else { - currentReachabilityState = or(innerMergedState, outerState); - } - } - - function jumpToLabel(label: Identifier, outerState: Reachability): boolean { - initializeReachabilityStateIfNecessary(); - - const index = label ? labelIndexMap[label.text] : lastOrUndefined(implicitLabels); - if (index === undefined) { - // reference to unknown label or - // break/continue used outside of loops - return false; - } - const stateAtLabel = labelStack[index]; - labelStack[index] = stateAtLabel === Reachability.Uninitialized ? outerState : or(stateAtLabel, outerState); - return true; + function shouldReportErrorOnModuleDeclaration(node: ModuleDeclaration): boolean { + const instanceState = getModuleInstanceState(node); + return instanceState === ModuleInstanceState.Instantiated || (instanceState === ModuleInstanceState.ConstEnumOnly && options.preserveConstEnums); } function checkUnreachable(node: Node): boolean { - switch (currentReachabilityState) { - case Reachability.Unreachable: - const reportError = - // report error on all statements except empty ones - (isStatement(node) && node.kind !== SyntaxKind.EmptyStatement) || - // report error on class declarations - node.kind === SyntaxKind.ClassDeclaration || - // report error on instantiated modules or const-enums only modules if preserveConstEnums is set - (node.kind === SyntaxKind.ModuleDeclaration && shouldReportErrorOnModuleDeclaration(node)) || - // report error on regular enums and const enums if preserveConstEnums is set - (node.kind === SyntaxKind.EnumDeclaration && (!isConstEnumDeclaration(node) || options.preserveConstEnums)); + if (currentFlow.kind !== FlowKind.Unreachable) { + return false; + } + if (currentFlow === unreachableFlow) { + const reportError = + // report error on all statements except empty ones + (isStatement(node) && node.kind !== SyntaxKind.EmptyStatement) || + // report error on class declarations + node.kind === SyntaxKind.ClassDeclaration || + // report error on instantiated modules or const-enums only modules if preserveConstEnums is set + (node.kind === SyntaxKind.ModuleDeclaration && shouldReportErrorOnModuleDeclaration(node)) || + // report error on regular enums and const enums if preserveConstEnums is set + (node.kind === SyntaxKind.EnumDeclaration && (!isConstEnumDeclaration(node) || options.preserveConstEnums)); - if (reportError) { - currentReachabilityState = Reachability.ReportedUnreachable; + if (reportError) { + currentFlow = reportedUncreachableFlow; - // unreachable code is reported if - // - user has explicitly asked about it AND - // - statement is in not ambient context (statements in ambient context is already an error - // so we should not report extras) AND - // - node is not variable statement OR - // - node is block scoped variable statement OR - // - node is not block scoped variable statement and at least one variable declaration has initializer - // Rationale: we don't want to report errors on non-initialized var's since they are hoisted - // On the other side we do want to report errors on non-initialized 'lets' because of TDZ - const reportUnreachableCode = - !options.allowUnreachableCode && - !isInAmbientContext(node) && - ( - node.kind !== SyntaxKind.VariableStatement || - getCombinedNodeFlags((node).declarationList) & NodeFlags.BlockScoped || - forEach((node).declarationList.declarations, d => d.initializer) - ); + // unreachable code is reported if + // - user has explicitly asked about it AND + // - statement is in not ambient context (statements in ambient context is already an error + // so we should not report extras) AND + // - node is not variable statement OR + // - node is block scoped variable statement OR + // - node is not block scoped variable statement and at least one variable declaration has initializer + // Rationale: we don't want to report errors on non-initialized var's since they are hoisted + // On the other side we do want to report errors on non-initialized 'lets' because of TDZ + const reportUnreachableCode = + !options.allowUnreachableCode && + !isInAmbientContext(node) && + ( + node.kind !== SyntaxKind.VariableStatement || + getCombinedNodeFlags((node).declarationList) & NodeFlags.BlockScoped || + forEach((node).declarationList.declarations, d => d.initializer) + ); - if (reportUnreachableCode) { - errorOnFirstToken(node, Diagnostics.Unreachable_code_detected); - } + if (reportUnreachableCode) { + errorOnFirstToken(node, Diagnostics.Unreachable_code_detected); } - case Reachability.ReportedUnreachable: - return true; - default: - return false; + } } - - function shouldReportErrorOnModuleDeclaration(node: ModuleDeclaration): boolean { - const instanceState = getModuleInstanceState(node); - return instanceState === ModuleInstanceState.Instantiated || (instanceState === ModuleInstanceState.ConstEnumOnly && options.preserveConstEnums); - } - } - - function initializeReachabilityStateIfNecessary(): void { - if (labelIndexMap) { - return; - } - currentReachabilityState = Reachability.Reachable; - labelIndexMap = {}; - labelStack = []; - implicitLabels = []; + return true; } } } diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 92c5065b7dd..da5cc90caa4 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -5,6 +5,7 @@ namespace ts { let nextSymbolId = 1; let nextNodeId = 1; let nextMergeId = 1; + let nextFlowId = 1; export function getNodeId(node: Node): number { if (!node.id) { @@ -118,6 +119,7 @@ namespace ts { const nullType = createIntrinsicType(TypeFlags.Null | nullableWideningFlags, "null"); const emptyArrayElementType = createIntrinsicType(TypeFlags.Undefined | TypeFlags.ContainsUndefinedOrNull, "undefined"); const unknownType = createIntrinsicType(TypeFlags.Any, "unknown"); + const resolvingFlowType = createIntrinsicType(TypeFlags.Void, "__resolving__"); const emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); const emptyUnionType = emptyObjectType; @@ -186,6 +188,7 @@ namespace ts { const mergedSymbols: Symbol[] = []; const symbolLinks: SymbolLinks[] = []; const nodeLinks: NodeLinks[] = []; + const flowTypeCaches: Map[] = []; const potentialThisCollisions: Node[] = []; const awaitedTypeStack: number[] = []; @@ -7136,11 +7139,11 @@ namespace ts { Debug.fail("should not get here"); } - // Return the assignment key for a "dotted name" (i.e. a sequence of identifiers + // Return the flow cache 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 { + function getFlowCacheKey(node: Node): string { if (node.kind === SyntaxKind.Identifier) { const symbol = getResolvedSymbol(node); return symbol !== unknownSymbol ? "" + getSymbolId(symbol) : undefined; @@ -7149,125 +7152,12 @@ namespace ts { return "0"; } if (node.kind === SyntaxKind.PropertyAccessExpression) { - const key = getAssignmentKey((node).expression); + const key = getFlowCacheKey((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)); - } - - // 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 visitReference(node: Identifier | PropertyAccessExpression) { - if (isAssignmentTarget(node) || isCompoundAssignmentTarget(node)) { - const key = getAssignmentKey(node); - if (key) { - assignmentMap[key] = true; - } - } - forEachChild(node, visit); - } - - function visitVariableDeclaration(node: VariableLikeDeclaration) { - if (!isBindingPattern(node.name) && hasInitializer(node)) { - assignmentMap[getSymbolId(getSymbolOfNode(node))] = true; - } - forEachChild(node, visit); - } - - function visit(node: Node) { - switch (node.kind) { - case SyntaxKind.Identifier: - case SyntaxKind.PropertyAccessExpression: - visitReference(node); - break; - case SyntaxKind.VariableDeclaration: - case SyntaxKind.BindingElement: - visitVariableDeclaration(node); - break; - case SyntaxKind.BinaryExpression: - case SyntaxKind.ObjectBindingPattern: - case SyntaxKind.ArrayBindingPattern: - case SyntaxKind.ArrayLiteralExpression: - case SyntaxKind.ObjectLiteralExpression: - case SyntaxKind.ElementAccessExpression: - case SyntaxKind.CallExpression: - case SyntaxKind.NewExpression: - case SyntaxKind.TypeAssertionExpression: - case SyntaxKind.AsExpression: - case SyntaxKind.NonNullExpression: - case SyntaxKind.ParenthesizedExpression: - case SyntaxKind.PrefixUnaryExpression: - case SyntaxKind.DeleteExpression: - case SyntaxKind.AwaitExpression: - case SyntaxKind.TypeOfExpression: - case SyntaxKind.VoidExpression: - case SyntaxKind.PostfixUnaryExpression: - case SyntaxKind.YieldExpression: - case SyntaxKind.ConditionalExpression: - case SyntaxKind.SpreadElementExpression: - 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.ForOfStatement: - case SyntaxKind.ReturnStatement: - case SyntaxKind.WithStatement: - case SyntaxKind.SwitchStatement: - case SyntaxKind.CaseBlock: - case SyntaxKind.CaseClause: - case SyntaxKind.DefaultClause: - case SyntaxKind.LabeledStatement: - case SyntaxKind.ThrowStatement: - case SyntaxKind.TryStatement: - case SyntaxKind.CatchClause: - case SyntaxKind.JsxElement: - case SyntaxKind.JsxSelfClosingElement: - case SyntaxKind.JsxAttribute: - case SyntaxKind.JsxSpreadAttribute: - case SyntaxKind.JsxOpeningElement: - case SyntaxKind.JsxExpression: - forEachChild(node, visit); - break; - } - } - } - - function isReferenceAssignedWithin(reference: Node, node: Node): boolean { - if (reference.kind !== SyntaxKind.ThisKeyword) { - 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; - } - } - function isNullOrUndefinedLiteral(node: Expression) { return node.kind === SyntaxKind.NullKeyword || node.kind === SyntaxKind.Identifier && getResolvedSymbol(node) === undefinedSymbol; @@ -7299,16 +7189,68 @@ namespace ts { return false; } - // Get the narrowed type of a given symbol at a given location + function containsMatchingReference(source: Node, target: Node) { + while (true) { + if (isMatchingReference(source, target)) { + return true; + } + if (source.kind !== SyntaxKind.PropertyAccessExpression) { + return false; + } + source = (source).expression; + } + } + + function hasMatchingArgument(callExpression: CallExpression, target: Node) { + if (callExpression.arguments) { + for (const argument of callExpression.arguments) { + if (isMatchingReference(argument, target)) { + return true; + } + } + } + if (callExpression.expression.kind === SyntaxKind.PropertyAccessExpression && + isMatchingReference((callExpression.expression).expression, target)) { + return true; + } + return false; + } + + function getFlowTypeCache(flow: FlowNode): Map { + if (!flow.id) { + flow.id = nextFlowId; + nextFlowId++; + } + return flowTypeCaches[flow.id] || (flowTypeCaches[flow.id] = {}); + } + + function isNarrowableReference(expr: Node): boolean { + return expr.kind === SyntaxKind.Identifier || + expr.kind === SyntaxKind.ThisKeyword || + expr.kind === SyntaxKind.PropertyAccessExpression && isNarrowableReference((expr).expression); + } + + function getAssignmentReducedType(type: Type, assignedType: Type) { + if (type.flags & TypeFlags.Union) { + const reducedTypes = filter((type).types, t => isTypeAssignableTo(assignedType, t)); + if (reducedTypes.length) { + return reducedTypes.length === 1 ? reducedTypes[0] : getUnionType(reducedTypes); + } + } + return type; + } + function getNarrowedTypeOfReference(type: Type, reference: Node) { if (!(type.flags & (TypeFlags.Any | TypeFlags.ObjectType | TypeFlags.Union | TypeFlags.TypeParameter))) { return type; } + if (!isNarrowableReference(reference)) { + return type; + } const leftmostNode = getLeftmostIdentifierOrThis(reference); if (!leftmostNode) { return type; } - let top: Node; if (leftmostNode.kind === SyntaxKind.Identifier) { const leftmostSymbol = getExportSymbolOfValueSymbolIfExported(getResolvedSymbol(leftmostNode)); if (!leftmostSymbol) { @@ -7318,74 +7260,138 @@ namespace ts { if (!declaration || declaration.kind !== SyntaxKind.VariableDeclaration && declaration.kind !== SyntaxKind.Parameter && declaration.kind !== SyntaxKind.BindingElement) { return type; } - top = 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: - break loop; - default: - if (node === top || isFunctionLikeKind(node.kind)) { - break loop; - } - break; - } } + return getFlowTypeOfReference(reference, type, type); + } - 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); + function getFlowTypeOfReference(reference: Node, declaredType: Type, initialType: Type) { + let key: string; + return reference.flowNode ? getTypeAtFlowNode(reference.flowNode) : initialType; + + function getTypeAtFlowNode(flow: FlowNode): Type { + while (true) { + switch (flow.kind) { + case FlowKind.Assignment: + const type = getTypeAtFlowAssignment(flow); + if (!type) { + flow = (flow).antecedent; + continue; } - else if ((node).operatorToken.kind === SyntaxKind.BarBarToken) { - type = narrowType(type, (node).left, /*assumeTrue*/ false); + return type; + case FlowKind.Condition: + return getTypeAtFlowCondition(flow); + case FlowKind.Label: + if ((flow).antecedents.length === 1) { + flow = (flow).antecedents[0]; + continue; } + return getTypeAtFlowLabel(flow); + } + // At the top of the flow we have the initial type + return initialType; + } + } + + function getTypeAtVariableDeclaration(node: VariableDeclaration) { + if (reference.kind === SyntaxKind.Identifier && !isBindingPattern(node.name) && getResolvedSymbol(reference) === getSymbolOfNode(node)) { + return getAssignmentReducedType(declaredType, checkExpressionCached((node).initializer)); + } + return undefined; + } + + function getTypeAtForInOrForOfStatement(node: ForInStatement | ForOfStatement) { + if (node.initializer.kind === SyntaxKind.VariableDeclarationList) { + if (reference.kind === SyntaxKind.Identifier) { + const variable = (node.initializer).declarations[0]; + if (variable && !isBindingPattern(variable.name) && getResolvedSymbol(reference) === getSymbolOfNode(variable)) { + return declaredType; + } + } + } + else { + if (isMatchingReference(reference, node.initializer)) { + const type = node.kind === SyntaxKind.ForOfStatement ? checkRightHandSideOfForOf(node.expression) : stringType; + return getAssignmentReducedType(declaredType, type); + } + if (reference.kind === SyntaxKind.PropertyAccessExpression && + containsMatchingReference((reference).expression, node.initializer)) { + return declaredType; + } + } + return undefined; + } + + function getTypeAtFlowAssignment(flow: FlowAssignment) { + const node = flow.node; + switch (node.kind) { + case SyntaxKind.BinaryExpression: + // If reference matches left hand side and type on right is properly assignable, + // return type on right. Otherwise default to the declared type. + if (isMatchingReference(reference, (node).left)) { + return getAssignmentReducedType(declaredType, checkExpressionCached((node).right)); + } + // We didn't have a direct match. However, if the reference is a dotted name, this + // may be an assignment to a left hand part of the reference. For example, for a + // reference 'x.y.z', we may be at an assignment to 'x.y' or 'x'. In that case, + // return the declared type. + if (reference.kind === SyntaxKind.PropertyAccessExpression && + containsMatchingReference((reference).expression, (node).left)) { + return declaredType; } break; - default: - Debug.fail("Unreachable!"); - } - - // Use original type if construct contains assignments to variable - if (type !== originalType && isAnyPartOfReferenceAssignedWithin(reference, node)) { - type = originalType; + case SyntaxKind.VariableDeclaration: + return getTypeAtVariableDeclaration(node); + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + return getTypeAtForInOrForOfStatement(node); } + // Assignment doesn't affect reference + return undefined; } - // Preserve old top-level behavior - if the branch is really an empty set, revert to prior type - if (type === emptyUnionType) { - type = originalType; + function getTypeAtFlowCondition(flow: FlowCondition) { + const type = getTypeAtFlowNode(flow.antecedent); + if (type === resolvingFlowType) { + return type; + } + return narrowType(type, (flow).expression, (flow).assumeTrue); } - return type; + function getTypeAtFlowNodeCached(flow: FlowNode) { + const cache = getFlowTypeCache(flow); + if (!key) { + key = getFlowCacheKey(reference); + } + let type = cache[key]; + if (type) { + return type; + } + cache[key] = resolvingFlowType; + type = getTypeAtFlowNode(flow); + cache[key] = type !== resolvingFlowType ? type : undefined; + return type; + } + + function getTypeAtFlowLabel(flow: FlowLabel) { + const antecedentTypes: Type[] = []; + for (const antecedent of flow.antecedents) { + const t = getTypeAtFlowNodeCached(antecedent); + if (t !== resolvingFlowType) { + // If the type at a particular antecedent path is the declared type, there is no + // reason to process more antecedents since the only possible outcome is subtypes + // that are be removed in the final union type anyway. + if (t === declaredType) { + return t; + } + if (!contains(antecedentTypes, t)) { + antecedentTypes.push(t); + } + } + } + return antecedentTypes.length === 0 ? declaredType : + antecedentTypes.length === 1 ? antecedentTypes[0] : + getUnionType(antecedentTypes); + } function narrowTypeByTruthiness(type: Type, expr: Expression, assumeTrue: boolean): Type { return strictNullChecks && assumeTrue && isMatchingReference(expr, reference) ? getNonNullableType(type) : type; @@ -7574,7 +7580,7 @@ namespace ts { } function narrowTypeByTypePredicate(type: Type, callExpression: CallExpression, assumeTrue: boolean): Type { - if (type.flags & TypeFlags.Any) { + if (type.flags & TypeFlags.Any || !hasMatchingArgument(callExpression, reference)) { return type; } const signature = getResolvedSignature(callExpression); @@ -7656,98 +7662,6 @@ namespace ts { return expression; } - function findFirstAssignment(symbol: Symbol, container: Node): Node { - return visit(isFunctionLike(container) ? (container).body : container); - - function visit(node: Node): Node { - switch (node.kind) { - case SyntaxKind.Identifier: - const assignment = getAssignmentRoot(node); - return assignment && getResolvedSymbol(node) === symbol ? assignment : undefined; - case SyntaxKind.BinaryExpression: - case SyntaxKind.VariableDeclaration: - case SyntaxKind.BindingElement: - case SyntaxKind.ObjectBindingPattern: - case SyntaxKind.ArrayBindingPattern: - case SyntaxKind.ArrayLiteralExpression: - case SyntaxKind.ObjectLiteralExpression: - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.ElementAccessExpression: - case SyntaxKind.CallExpression: - case SyntaxKind.NewExpression: - case SyntaxKind.TypeAssertionExpression: - case SyntaxKind.AsExpression: - case SyntaxKind.NonNullExpression: - case SyntaxKind.ParenthesizedExpression: - case SyntaxKind.PrefixUnaryExpression: - case SyntaxKind.DeleteExpression: - case SyntaxKind.AwaitExpression: - case SyntaxKind.TypeOfExpression: - case SyntaxKind.VoidExpression: - case SyntaxKind.PostfixUnaryExpression: - case SyntaxKind.YieldExpression: - case SyntaxKind.ConditionalExpression: - case SyntaxKind.SpreadElementExpression: - case SyntaxKind.VariableStatement: - case SyntaxKind.ExpressionStatement: - case SyntaxKind.IfStatement: - case SyntaxKind.DoStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.ForStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.ForOfStatement: - case SyntaxKind.ReturnStatement: - case SyntaxKind.WithStatement: - case SyntaxKind.SwitchStatement: - case SyntaxKind.CaseBlock: - case SyntaxKind.CaseClause: - case SyntaxKind.DefaultClause: - case SyntaxKind.LabeledStatement: - case SyntaxKind.ThrowStatement: - case SyntaxKind.TryStatement: - case SyntaxKind.CatchClause: - case SyntaxKind.JsxElement: - case SyntaxKind.JsxSelfClosingElement: - case SyntaxKind.JsxAttribute: - case SyntaxKind.JsxSpreadAttribute: - case SyntaxKind.JsxOpeningElement: - case SyntaxKind.JsxExpression: - case SyntaxKind.Block: - case SyntaxKind.SourceFile: - return forEachChild(node, visit); - } - return undefined; - } - } - - function checkVariableAssignedBefore(symbol: Symbol, reference: Node) { - if (!(symbol.flags & SymbolFlags.Variable)) { - return; - } - const declaration = symbol.valueDeclaration; - if (!declaration || declaration.kind !== SyntaxKind.VariableDeclaration || (declaration).initializer) { - return; - } - const parentParentKind = declaration.parent.parent.kind; - if (parentParentKind === SyntaxKind.ForOfStatement || parentParentKind === SyntaxKind.ForInStatement) { - return; - } - const declarationContainer = getContainingFunction(declaration) || getSourceFileOfNode(declaration); - const referenceContainer = getContainingFunction(reference) || getSourceFileOfNode(reference); - if (declarationContainer !== referenceContainer) { - return; - } - const links = getSymbolLinks(symbol); - if (!links.firstAssignmentChecked) { - links.firstAssignmentChecked = true; - links.firstAssignment = findFirstAssignment(symbol, declarationContainer); - } - if (links.firstAssignment && links.firstAssignment.end <= reference.pos) { - return; - } - error(reference, Diagnostics.Variable_0_is_used_before_being_assigned, symbolToString(symbol)); - } - function checkIdentifier(node: Identifier): Type { const symbol = getResolvedSymbol(node); @@ -7800,10 +7714,18 @@ namespace ts { checkNestedBlockScopedBinding(node, symbol); const type = getTypeOfSymbol(localOrExportSymbol); - if (strictNullChecks && !isAssignmentTarget(node) && !(type.flags & TypeFlags.Any) && !(getNullableKind(type) & TypeFlags.Undefined)) { - checkVariableAssignedBefore(symbol, node); + if (!(localOrExportSymbol.flags & SymbolFlags.Variable) || isAssignmentTarget(node)) { + return type; } - return getNarrowedTypeOfReference(type, node); + const declaration = localOrExportSymbol.valueDeclaration; + const defaultsToDeclaredType = !strictNullChecks || !declaration || + declaration.kind === SyntaxKind.Parameter || isInAmbientContext(declaration) || + getContainingFunction(declaration) !== getContainingFunction(node); + const flowType = getFlowTypeOfReference(node, type, defaultsToDeclaredType ? type : undefinedType); + if (strictNullChecks && !(type.flags & TypeFlags.Any) && !(getNullableKind(type) & TypeFlags.Undefined) && getNullableKind(flowType) & TypeFlags.Undefined) { + error(node, Diagnostics.Variable_0_is_used_before_being_assigned, symbolToString(symbol)); + } + return flowType; } function isInsideFunction(node: Node, threshold: Node): boolean { @@ -8715,8 +8637,10 @@ namespace ts { return mapper && mapper.context; } - // Return the root assignment node of an assignment target - function getAssignmentRoot(node: Node): Node { + // A node is an assignment target if it is on the left hand side of an '=' token, if it is parented by a property + // assignment in an object literal that is an assignment target, or if it is parented by an array literal that is + // an assignment target. Examples include 'a = xxx', '{ p: a } = xxx', '[{ p: a}] = xxx'. + function isAssignmentTarget(node: Node): boolean { while (node.parent.kind === SyntaxKind.ParenthesizedExpression) { node = node.parent; } @@ -8734,23 +8658,7 @@ namespace ts { const parent = node.parent; return parent.kind === SyntaxKind.BinaryExpression && (parent).operatorToken.kind === SyntaxKind.EqualsToken && - (parent).left === node ? parent : undefined; - } - - // A node is an assignment target if it is on the left hand side of an '=' token, if it is parented by a property - // assignment in an object literal that is an assignment target, or if it is parented by an array literal that is - // an assignment target. Examples include 'a = xxx', '{ p: a } = xxx', '[{ p: a}] = xxx'. - function isAssignmentTarget(node: Node): boolean { - return !!getAssignmentRoot(node); - } - - function isCompoundAssignmentTarget(node: Node) { - const parent = node.parent; - if (parent.kind === SyntaxKind.BinaryExpression && (parent).left === node) { - const operator = (parent).operatorToken.kind; - return operator >= SyntaxKind.FirstAssignment && operator <= SyntaxKind.LastAssignment; - } - return false; + (parent).left === node; } function checkSpreadElementExpression(node: SpreadElementExpression, contextualMapper?: TypeMapper): Type { @@ -9604,7 +9512,7 @@ namespace ts { } const propType = getTypeOfSymbol(prop); - return node.kind === SyntaxKind.PropertyAccessExpression && prop.flags & SymbolFlags.Property ? + return node.kind === SyntaxKind.PropertyAccessExpression && prop.flags & SymbolFlags.Property && !isAssignmentTarget(node) ? getNarrowedTypeOfReference(propType, node) : propType; } @@ -16177,7 +16085,7 @@ namespace ts { } if (entityName.parent.kind === SyntaxKind.ExportAssignment) { - return resolveEntityName(entityName, + return resolveEntityName(entityName, /*all meanings*/ SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 546d1631945..c850fe47012 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -449,6 +449,7 @@ namespace ts { /* @internal */ locals?: SymbolTable; // Locals associated with node (initialized by binding) /* @internal */ nextContainer?: Node; // Next container in declaration order (initialized by binding) /* @internal */ localSymbol?: Symbol; // Local symbol declared by node (initialized by binding only for exported nodes) + /* @internal */ flowNode?: FlowNode; // Associated FlowNode (initialized by binding) } export interface NodeArray extends Array, TextRange { @@ -1518,6 +1519,39 @@ namespace ts { isBracketed: boolean; } + export const enum FlowKind { + Unreachable, + Start, + Label, + Assignment, + Condition + } + + export interface FlowNode { + kind: FlowKind; // Node kind + id?: number; // Node id used by flow type cache in checker + } + + // FlowLabel represents a junction with multiple possible preceding control flows. + export interface FlowLabel extends FlowNode { + antecedents: FlowNode[]; + } + + // FlowAssignment represents a node that possibly assigns a value to one or more + // references. + export interface FlowAssignment extends FlowNode { + node: BinaryExpression | VariableDeclaration | ForInStatement | ForOfStatement; + antecedent: FlowNode; + } + + // FlowCondition represents a condition that is known to be true or false at the + // node's location in the control flow. + export interface FlowCondition extends FlowNode { + expression: Expression; + assumeTrue: boolean; + antecedent: FlowNode; + } + export interface AmdDependency { path: string; name: string;