diff --git a/Jakefile b/Jakefile index ba82e9c3f4363..815bc84cd4c86 100644 --- a/Jakefile +++ b/Jakefile @@ -252,7 +252,7 @@ compileFile(tscFile, compilerSources, [builtLocalDirectory, copyright].concat(co var servicesFile = path.join(builtLocalDirectory, "typescriptServices.js"); var servicesDefinitionsFile = path.join(builtLocalDirectory, "typescriptServices.d.ts"); -compileFile(servicesFile, servicesSources, [builtLocalDirectory, copyright].concat(servicesSources), [copyright], /*useBuiltCompiler:*/ true, /*noOutFile:*/ false, /*generateDeclarations:*/ true); +compileFile(servicesFile, servicesSources, [builtLocalDirectory, copyright].concat(servicesSources), [copyright], /*useBuiltCompiler:*/ false, /*noOutFile:*/ false, /*generateDeclarations:*/ true); // Local target to build the compiler and services desc("Builds the full compiler and services"); diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c43d1074dfa20..9c243a91e3803 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4,6 +4,7 @@ /// /// /// +/// module ts { var nextSymbolId = 1; @@ -6150,7 +6151,7 @@ module ts { function checkFunctionExpressionBody(node: FunctionExpression) { if (node.type) { - checkIfNonVoidFunctionHasReturnExpressionsOrSingleThrowStatment(node, getTypeFromTypeNode(node.type)); + checkControlFlowOfFunction(node, getTypeFromTypeNode(node.type) !== voidType, error); } if (node.body.kind === SyntaxKind.FunctionBlock) { checkSourceElement(node.body); @@ -7294,7 +7295,7 @@ module ts { checkSourceElement(node.body); if (node.type && !isAccessor(node.kind)) { - checkIfNonVoidFunctionHasReturnExpressionsOrSingleThrowStatment(node, getTypeFromTypeNode(node.type)); + checkControlFlowOfFunction(node, getTypeFromTypeNode(node.type) !== voidType, error); } // If there is no body and no explicit return type, then report an error. diff --git a/src/compiler/controlflow.ts b/src/compiler/controlflow.ts new file mode 100644 index 0000000000000..78c30dce27ed5 --- /dev/null +++ b/src/compiler/controlflow.ts @@ -0,0 +1,382 @@ +/// + +module ts { + export function checkControlFlowOfFunction(decl: FunctionLikeDeclaration, noImplicitReturns: boolean, error: (n: Node, message: DiagnosticMessage, arg0?: any) => void) { + if (!decl.body || decl.body.kind !== SyntaxKind.FunctionBlock) { + return; + } + + var finalState = checkControlFlow(decl.body, error); + if (noImplicitReturns && finalState === ControlFlowState.Reachable) { + var errorNode: Node = decl.name || decl; + error(errorNode, Diagnostics.Not_all_code_paths_return_a_value); + } + } + + export function checkControlFlowOfBlock(block: Block, error: (n: Node, message: DiagnosticMessage, arg0?: any) => void) { + checkControlFlow(block, error); + } + + function checkControlFlow(decl: Node, error: (n: Node, message: DiagnosticMessage, arg0?: any) => void): ControlFlowState { + var currentState = ControlFlowState.Reachable; + + function setState(newState: ControlFlowState) { + currentState = newState; + } + + function or(s1: ControlFlowState, s2: ControlFlowState): ControlFlowState { + if (s1 === ControlFlowState.Reachable || s2 === ControlFlowState.Reachable) { + return ControlFlowState.Reachable; + } + if (s1 === ControlFlowState.ReportedUnreachable && s2 === ControlFlowState.ReportedUnreachable) { + return ControlFlowState.ReportedUnreachable; + } + return ControlFlowState.Unreachable; + } + + function reportIfNotReachable(n: Node): boolean { + switch (currentState) { + case ControlFlowState.Unreachable: + error(n, Diagnostics.Unreachable_code_detected); + currentState = ControlFlowState.ReportedUnreachable; + return true; + case ControlFlowState.ReportedUnreachable: + return true; + default: + return false; + } + } + + // label name -> index in 'labelStack' + var labels: Map = {}; + // CF state at all seen labels + var labelStack: ControlFlowState[] = []; + // indices of implicit labels in 'labelStack' + var implicitLabels: number[] = []; + + function pushNamedLabel(name: Identifier): boolean { + if (hasProperty(labels, name.text)) { + return false; + } + var newLen = labelStack.push(ControlFlowState.Uninitialized); + labels[name.text] = newLen - 1; + return true; + } + + function pushImplicitLabel(): number { + var newLen = labelStack.push(ControlFlowState.Uninitialized); + implicitLabels.push(newLen - 1); + return newLen - 1; + } + + function setFinalStateAtLabel(mergedStates: ControlFlowState, outerState: ControlFlowState, name: Identifier): void { + if (mergedStates === ControlFlowState.Uninitialized) { + if (name) { + error(name, Diagnostics.Unused_label); + } + setState(outerState); + } + else { + setState(or(mergedStates, outerState)); + } + } + + function popNamedLabel(name: Identifier, outerState: ControlFlowState): void { + Debug.assert(hasProperty(labels, name.text)); + var index = labels[name.text]; + Debug.assert(labelStack.length === index + 1); + labels[name.text] = undefined; + var mergedStates = labelStack.pop(); + setFinalStateAtLabel(mergedStates, outerState, name); + } + + function popImplicitLabel(index: number, outerState: ControlFlowState): void { + Debug.assert(labelStack.length === index + 1); + var i = implicitLabels.pop(); + Debug.assert(index === i); + var mergedStates = labelStack.pop(); + setFinalStateAtLabel(mergedStates, outerState, /*name*/ undefined); + } + + function gotoLabel(label: Identifier, outerState: ControlFlowState): void { + var stateIndex: number; + if (label) { + if (!hasProperty(labels, label.text)) { + // reference to non-existing label + return; + } + stateIndex = labels[label.text]; + } + else { + if (implicitLabels.length === 0) { + // non-labeled break\continue being used outside loops + return; + } + + stateIndex = implicitLabels[implicitLabels.length - 1]; + } + var stateAtLabel = labelStack[stateIndex]; + labelStack[stateIndex] = stateAtLabel === ControlFlowState.Uninitialized ? outerState : or(outerState, stateAtLabel); + } + + function checkWhileStatement(n: WhileStatement): void { + if (reportIfNotReachable(n)) { + // current state is unreachable. + // since nothing downstream can change it - no need to continue + return; + } + + var preWhileState = + n.expression.kind === SyntaxKind.FalseKeyword ? ControlFlowState.Unreachable : currentState; + var postWhileState = + n.expression.kind === SyntaxKind.TrueKeyword ? ControlFlowState.Unreachable : currentState; + + setState(preWhileState); + + var index = pushImplicitLabel(); + check(n.statement); + popImplicitLabel(index, postWhileState); + } + + function checkDoStatement(n: DoStatement): void { + if (reportIfNotReachable(n)) { + // current state is unreachable. + // since nothing downstream can change it - no need to continue + return; + } + + var preDoState = currentState; + + var index = pushImplicitLabel(); + check(n.statement); + + var postDoState = n.expression.kind === SyntaxKind.TrueKeyword ? ControlFlowState.Unreachable : preDoState; + popImplicitLabel(index, postDoState); + } + + function checkForStatement(n: ForStatement): void { + if (reportIfNotReachable(n)) { + // current state is unreachable. + // since nothing downstream can change it - no need to continue + return; + } + + var preForState = currentState; + var index = pushImplicitLabel(); + check(n.statement); + + // for statement is considered infinite when it condition is either omitted or is true keyword + // - for(..;;..) + // - for(..;true;..) + var isInfiniteLoop = (!n.condition || n.condition.kind === SyntaxKind.TrueKeyword); + + var postForState = isInfiniteLoop ? ControlFlowState.Unreachable : preForState; + popImplicitLabel(index, postForState); + } + + function checkForInStatement(n: ForInStatement): void { + if (reportIfNotReachable(n)) { + // current state is unreachable. + // since nothing downstream can change it - no need to continue + return; + } + + var preForInState = currentState; + var index = pushImplicitLabel(); + check(n.statement); + popImplicitLabel(index, preForInState); + } + + function checkBlock(n: Block): void { + forEach(n.statements, check); + } + + function checkIfStatement(n: IfStatement): void { + var ifTrueState: ControlFlowState = n.expression.kind === SyntaxKind.FalseKeyword ? ControlFlowState.Unreachable : currentState; + var ifFalseState: ControlFlowState = n.expression.kind === SyntaxKind.TrueKeyword ? ControlFlowState.Unreachable : currentState; + + setState(ifTrueState); + check(n.thenStatement); + if (n.elseStatement) { + ifTrueState = currentState; + setState(ifFalseState); + check(n.elseStatement); + setState(or(currentState, ifTrueState)); + } + else { + setState(or(currentState, ifFalseState)) + } + } + + function checkReturnOrThrow(n: Node): void { + reportIfNotReachable(n); + setState(ControlFlowState.Unreachable); + } + + function checkBreakOrContinueStatement(n: BreakOrContinueStatement): void { + if (reportIfNotReachable(n)) { + // current state is unreachable. + // since nothing downstream can change it - no need to continue + return; + } + if (n.kind === SyntaxKind.BreakStatement) { + gotoLabel(n.label, currentState); + } + else { + gotoLabel(n.label, ControlFlowState.Unreachable); // touch label so it will be marked a used + } + setState(ControlFlowState.Unreachable); + } + + function checkTryStatement(n: TryStatement): void { + if (reportIfNotReachable(n)) { + // current state is unreachable. + // since nothing downstream can change it - no need to continue + return; + } + + // catch\finally blocks has the same reachability as try block + var startState = currentState; + check(n.tryBlock); + var postTryState = currentState; + + setState(startState); + check(n.catchBlock); + var postCatchState = currentState; + + if (n.finallyBlock) { + setState(startState); + check(n.finallyBlock); + // post-finally state become current state + } + else { + // post catch 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 + setState(or(postTryState, postCatchState)) + } + } + + function checkSwitchStatement(n: SwitchStatement): void { + if (reportIfNotReachable(n)) { + // current state is unreachable. + // since nothing downstream can change it - no need to continue + return; + } + + var startState = currentState; + var hasDefault = false; + + var index = pushImplicitLabel(); + + forEach(n.clauses, (c: CaseOrDefaultClause) => { + hasDefault = hasDefault || c.kind === SyntaxKind.DefaultClause; + setState(startState); + forEach(c.statements, check); + if (c.statements.length && currentState === ControlFlowState.Reachable) { + error(c.expression, Diagnostics.Fallthrough_case_in_switch); + } + }); + + // post switch state is unreachable if switch is exaustive (has a default case ) and does not have fallthrough from the last case + var postSwitchState = hasDefault && currentState !== ControlFlowState.Reachable ? ControlFlowState.Unreachable : startState; + + popImplicitLabel(index, postSwitchState); + } + + function checkLabelledStatement(n: LabeledStatement): void { + if (reportIfNotReachable(n)) { + // current state is unreachable. + // since nothing downstream can change it - no need to continue + return; + } + + var ok = pushNamedLabel(n.label); + check(n.statement); + if (ok) { + popNamedLabel(n.label, currentState); + } + } + + function checkWithStatement(n: WithStatement): void { + if (reportIfNotReachable(n)) { + // current state is unreachable. + // since nothing downstream can change it - no need to continue + return; + } + + check(n.statement); + } + + // current assumption: only statements affect CF + function check(n: Node): void { + if (!n || currentState === ControlFlowState.ReportedUnreachable) { + return; + } + switch (n.kind) { + case SyntaxKind.WhileStatement: + checkWhileStatement(n); + break; + case SyntaxKind.SourceFile: + forEach((n).statements, check); + break; + case SyntaxKind.Block: + case SyntaxKind.TryBlock: + case SyntaxKind.CatchBlock: + case SyntaxKind.FinallyBlock: + case SyntaxKind.ModuleBlock: + case SyntaxKind.FunctionBlock: + checkBlock(n); + break; + case SyntaxKind.IfStatement: + checkIfStatement(n); + break; + case SyntaxKind.ReturnStatement: + case SyntaxKind.ThrowStatement: + checkReturnOrThrow(n); + break; + case SyntaxKind.BreakStatement: + case SyntaxKind.ContinueStatement: + checkBreakOrContinueStatement(n); + break; + case SyntaxKind.VariableStatement: + case SyntaxKind.EmptyStatement: + case SyntaxKind.ExpressionStatement: + case SyntaxKind.DebuggerStatement: + reportIfNotReachable(n); + break; + case SyntaxKind.DoStatement: + checkDoStatement(n); + break; + case SyntaxKind.ForInStatement: + checkForInStatement(n); + break; + case SyntaxKind.ForStatement: + checkForStatement(n); + break; + case SyntaxKind.LabeledStatement: + checkLabelledStatement(n); + break; + case SyntaxKind.SwitchStatement: + checkSwitchStatement(n); + break; + case SyntaxKind.TryStatement: + checkTryStatement(n); + break; + case SyntaxKind.WithStatement: + checkWithStatement(n); + break; + } + } + + check(decl); + return currentState; + } + + const enum ControlFlowState { + Uninitialized = 0, + Reachable = 1, + Unreachable = 2, + ReportedUnreachable = 3, + } +} \ No newline at end of file diff --git a/src/compiler/diagnosticInformationMap.generated.ts b/src/compiler/diagnosticInformationMap.generated.ts index c17cf94e8c007..88143272ab658 100644 --- a/src/compiler/diagnosticInformationMap.generated.ts +++ b/src/compiler/diagnosticInformationMap.generated.ts @@ -288,6 +288,10 @@ module ts { Type_alias_0_circularly_references_itself: { code: 2456, category: DiagnosticCategory.Error, key: "Type alias '{0}' circularly references itself." }, Type_alias_name_cannot_be_0: { code: 2457, category: DiagnosticCategory.Error, key: "Type alias name cannot be '{0}'" }, An_AMD_module_cannot_have_multiple_name_assignments: { code: 2458, category: DiagnosticCategory.Error, key: "An AMD module cannot have multiple name assignments." }, + Not_all_code_paths_return_a_value: { code: 2459, category: DiagnosticCategory.Warning, key: "Not all code paths return a value" }, + Unreachable_code_detected: { code: 2460, category: DiagnosticCategory.Warning, key: "Unreachable code detected" }, + Unused_label: { code: 2461, category: DiagnosticCategory.Warning, key: "Unused label" }, + Fallthrough_case_in_switch: { code: 2462, category: DiagnosticCategory.Warning, key: "Fallthrough case in switch" }, Import_declaration_0_is_using_private_name_1: { code: 4000, category: DiagnosticCategory.Error, key: "Import declaration '{0}' is using private name '{1}'." }, Type_parameter_0_of_exported_class_has_or_is_using_private_name_1: { code: 4002, category: DiagnosticCategory.Error, key: "Type parameter '{0}' of exported class has or is using private name '{1}'." }, Type_parameter_0_of_exported_interface_has_or_is_using_private_name_1: { code: 4004, category: DiagnosticCategory.Error, key: "Type parameter '{0}' of exported interface has or is using private name '{1}'." }, diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 3d5be6d566a89..8ceedac516662 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -1147,8 +1147,23 @@ "An AMD module cannot have multiple name assignments.": { "category": "Error", "code": 2458 + }, + "Not all code paths return a value": { + "category": "Warning", + "code": 2459 + }, + "Unreachable code detected": { + "category": "Warning", + "code": 2460 + }, + "Unused label": { + "category": "Warning", + "code": 2461 + }, + "Fallthrough case in switch": { + "category": "Warning", + "code": 2462 }, - "Import declaration '{0}' is using private name '{1}'.": { "category": "Error", "code": 4000