Skip to content

Commit 3ec5eb1

Browse files
committed
initial rev of controlflow analysis
1 parent d99023e commit 3ec5eb1

File tree

5 files changed

+342
-4
lines changed

5 files changed

+342
-4
lines changed

Jakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ var tscFile = path.join(builtLocalDirectory, compilerFilename);
252252
compileFile(tscFile, compilerSources, [builtLocalDirectory, copyright].concat(compilerSources), [copyright], /*useBuiltCompiler:*/ false);
253253

254254
var servicesFile = path.join(builtLocalDirectory, "typescriptServices.js");
255-
compileFile(servicesFile, servicesSources, [builtLocalDirectory, copyright].concat(servicesSources), [copyright], /*useBuiltCompiler:*/ true);
255+
compileFile(servicesFile, servicesSources, [builtLocalDirectory, copyright].concat(servicesSources), [copyright], /*useBuiltCompiler:*/ false);
256256

257257
// Local target to build the compiler and services
258258
desc("Builds the full compiler and services");

src/compiler/checker.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
/// <reference path="parser.ts"/>
55
/// <reference path="binder.ts"/>
66
/// <reference path="emitter.ts"/>
7+
/// <reference path="controlflow.ts"/>
78

89
module ts {
910

@@ -5896,7 +5897,8 @@ module ts {
58965897

58975898
function checkFunctionExpressionBody(node: FunctionExpression) {
58985899
if (node.type) {
5899-
checkIfNonVoidFunctionHasReturnExpressionsOrSingleThrowStatment(node, getTypeFromTypeNode(node.type));
5900+
checkControlFlowOfFunction(node, getTypeFromTypeNode(node.type) !== voidType, error);
5901+
// checkIfNonVoidFunctionHasReturnExpressionsOrSingleThrowStatment(node, getTypeFromTypeNode(node.type));
59005902
}
59015903
if (node.body.kind === SyntaxKind.FunctionBlock) {
59025904
checkSourceElement(node.body);
@@ -7015,7 +7017,8 @@ module ts {
70157017

70167018
checkSourceElement(node.body);
70177019
if (node.type && !isAccessor(node.kind)) {
7018-
checkIfNonVoidFunctionHasReturnExpressionsOrSingleThrowStatment(node, getTypeFromTypeNode(node.type));
7020+
checkControlFlowOfFunction(node, getTypeFromTypeNode(node.type) !== voidType, error);
7021+
// checkIfNonVoidFunctionHasReturnExpressionsOrSingleThrowStatment(node, getTypeFromTypeNode(node.type));
70197022
}
70207023

70217024
// If there is no body and no explicit return type, then report an error.

src/compiler/controlflow.ts

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
/// <reference path="types.ts"/>
2+
3+
module ts {
4+
export function checkControlFlowOfFunction(decl: FunctionLikeDeclaration, noImplicitReturns: boolean, error: (n: Node, message: DiagnosticMessage, arg0?: any) => void) {
5+
if (!decl.body || decl.body.kind !== SyntaxKind.FunctionBlock) {
6+
return;
7+
}
8+
9+
var finalState = checkControlFlow(decl.body, error);
10+
if (noImplicitReturns && finalState === ControlFlowState.Reachable) {
11+
var errorNode: Node = decl.name || decl;
12+
error(errorNode, Diagnostics.Not_all_code_paths_return_a_value);
13+
}
14+
}
15+
16+
export function checkControlFlowOfBlock(block: Block, error: (n: Node, message: DiagnosticMessage, arg0?: any) => void) {
17+
checkControlFlow(block, error);
18+
}
19+
20+
function checkControlFlow(decl: Node, error: (n: Node, message: DiagnosticMessage, arg0?: any) => void): ControlFlowState {
21+
var currentState = ControlFlowState.Reachable;
22+
23+
function setState(newState: ControlFlowState) {
24+
currentState = newState;
25+
}
26+
27+
function or(s1: ControlFlowState, s2: ControlFlowState): ControlFlowState {
28+
if (s1 === ControlFlowState.Reachable || s2 === ControlFlowState.Reachable) {
29+
return ControlFlowState.Reachable;
30+
}
31+
if (s1 === ControlFlowState.ReportedUnreachable && s2 === ControlFlowState.ReportedUnreachable) {
32+
return ControlFlowState.ReportedUnreachable;
33+
}
34+
return ControlFlowState.Unreachable;
35+
}
36+
37+
function verifyReachable(n: Node): void {
38+
if (currentState === ControlFlowState.Unreachable) {
39+
error(n, Diagnostics.Unreachable_code_detected);
40+
currentState = ControlFlowState.ReportedUnreachable;
41+
}
42+
}
43+
44+
// label name -> index in 'labelStack'
45+
var labels: Map<number> = {};
46+
// CF state at all seen labels
47+
var labelStack: ControlFlowState[] = [];
48+
// indices of implicit labels in 'labelStack'
49+
var implicitLabels: number[] = [];
50+
51+
function pushNamedLabel(name: Identifier): boolean {
52+
if (hasProperty(labels, name.text)) {
53+
return false;
54+
}
55+
var newLen = labelStack.push(ControlFlowState.Uninitialized);
56+
labels[name.text] = newLen - 1;
57+
return true;
58+
}
59+
60+
function pushImplicitLabel(): number {
61+
var newLen = labelStack.push(ControlFlowState.Uninitialized);
62+
implicitLabels.push(newLen - 1);
63+
return newLen - 1;
64+
}
65+
66+
function setFinalStateAtLabel(mergedStates: ControlFlowState, outerState: ControlFlowState, name: Identifier): void {
67+
if (mergedStates === ControlFlowState.Uninitialized) {
68+
if (name) {
69+
error(name, Diagnostics.Unused_label);
70+
}
71+
setState(outerState);
72+
}
73+
else {
74+
setState(or(mergedStates, outerState));
75+
}
76+
}
77+
78+
function popNamedLabel(name: Identifier, outerState: ControlFlowState): void {
79+
Debug.assert(hasProperty(labels, name.text));
80+
var index = labels[name.text];
81+
Debug.assert(labelStack.length === index + 1);
82+
labels[name.text] = undefined;
83+
var mergedStates = labelStack.pop();
84+
setFinalStateAtLabel(mergedStates, outerState, name);
85+
}
86+
87+
function popImplicitLabel(index: number, outerState: ControlFlowState): void {
88+
Debug.assert(labelStack.length === index + 1);
89+
var i = implicitLabels.pop();
90+
Debug.assert(index === i);
91+
var mergedStates = labelStack.pop();
92+
setFinalStateAtLabel(mergedStates, outerState, /*name*/ undefined);
93+
}
94+
95+
function gotoLabel(label: Identifier, outerState: ControlFlowState): void {
96+
var stateIndex: number;
97+
if (label) {
98+
if (!hasProperty(labels, label.text)) {
99+
// reference to non-existing label
100+
return;
101+
}
102+
stateIndex = labels[label.text];
103+
}
104+
else {
105+
if (implicitLabels.length === 0) {
106+
// non-labeled break\continue being used outside loops
107+
return;
108+
}
109+
110+
stateIndex = implicitLabels[implicitLabels.length - 1];
111+
}
112+
var stateAtLabel = labelStack[stateIndex];
113+
labelStack[stateIndex] = stateAtLabel === ControlFlowState.Uninitialized ? outerState : or(outerState, stateAtLabel);
114+
}
115+
116+
function checkWhileStatement(n: WhileStatement): void {
117+
verifyReachable(n);
118+
119+
var preWhileState: ControlFlowState = n.expression.kind === SyntaxKind.FalseKeyword ? ControlFlowState.Unreachable : currentState;
120+
var postWhileState: ControlFlowState = n.expression.kind === SyntaxKind.TrueKeyword ? ControlFlowState.Unreachable : currentState;
121+
122+
setState(preWhileState);
123+
124+
var index = pushImplicitLabel();
125+
check(n.statement);
126+
popImplicitLabel(index, postWhileState);
127+
}
128+
129+
function checkDoStatement(n: DoStatement): void {
130+
verifyReachable(n);
131+
var preDoState = currentState;
132+
133+
var index = pushImplicitLabel();
134+
check(n.statement);
135+
136+
var postDoState = n.expression.kind === SyntaxKind.TrueKeyword ? ControlFlowState.Unreachable : preDoState;
137+
popImplicitLabel(index, postDoState);
138+
}
139+
140+
function checkForStatement(n: ForStatement): void {
141+
verifyReachable(n);
142+
143+
var preForState = currentState;
144+
var index = pushImplicitLabel();
145+
check(n.statement);
146+
var postForState = n.declarations || n.initializer || n.condition || n.iterator ? preForState : ControlFlowState.Unreachable;
147+
popImplicitLabel(index, postForState);
148+
}
149+
150+
function checkForInStatement(n: ForInStatement): void {
151+
verifyReachable(n);
152+
var preForInState = currentState;
153+
var index = pushImplicitLabel();
154+
check(n.statement);
155+
popImplicitLabel(index, preForInState);
156+
}
157+
158+
function checkBlock(n: Block): void {
159+
forEach(n.statements, check);
160+
}
161+
162+
function checkIfStatement(n: IfStatement): void {
163+
var ifTrueState: ControlFlowState = n.expression.kind === SyntaxKind.FalseKeyword ? ControlFlowState.Unreachable : currentState;
164+
var ifFalseState: ControlFlowState = n.expression.kind === SyntaxKind.TrueKeyword ? ControlFlowState.Unreachable : currentState;
165+
166+
setState(ifTrueState);
167+
check(n.thenStatement);
168+
ifTrueState = currentState;
169+
170+
setState(ifFalseState);
171+
check(n.elseStatement);
172+
173+
currentState = or(currentState, ifTrueState);
174+
}
175+
176+
function checkReturnOrThrow(n: Node): void {
177+
verifyReachable(n);
178+
setState(ControlFlowState.Unreachable);
179+
}
180+
181+
function checkBreakOrContinueStatement(n: BreakOrContinueStatement): void {
182+
verifyReachable(n);
183+
if (n.kind === SyntaxKind.BreakStatement) {
184+
gotoLabel(n.label, currentState);
185+
}
186+
else {
187+
gotoLabel(n.label, ControlFlowState.Unreachable); // touch label so it will be marked a used
188+
}
189+
setState(ControlFlowState.Unreachable);
190+
}
191+
192+
function checkTryStatement(n: TryStatement): void {
193+
verifyReachable(n);
194+
195+
// catch\finally blocks has the same reachability as try block
196+
var startState = currentState;
197+
check(n.tryBlock);
198+
var postTryState = currentState;
199+
200+
setState(startState);
201+
check(n.catchBlock);
202+
var postCatchState = currentState;
203+
204+
setState(startState);
205+
check(n.finallyBlock);
206+
setState(or(postTryState, postCatchState));
207+
}
208+
209+
function checkSwitchStatement(n: SwitchStatement): void {
210+
verifyReachable(n);
211+
var startState = currentState;
212+
var hasDefault = false;
213+
214+
var index = pushImplicitLabel();
215+
216+
forEach(n.clauses, (c: CaseOrDefaultClause) => {
217+
hasDefault = hasDefault || c.kind === SyntaxKind.DefaultClause;
218+
setState(startState);
219+
forEach(c.statements, check);
220+
if (c.statements.length && currentState === ControlFlowState.Reachable) {
221+
error(c.expression, Diagnostics.Fallthrough_case_in_switch);
222+
}
223+
});
224+
225+
// post switch state is unreachable if switch is exaustive (has a default case ) and does not have fallthrough from the last case
226+
var postSwitchState = hasDefault && currentState !== ControlFlowState.Reachable ? ControlFlowState.Unreachable : startState;
227+
228+
popImplicitLabel(index, postSwitchState);
229+
}
230+
231+
function checkLabelledStatement(n: LabeledStatement): void {
232+
verifyReachable(n);
233+
var ok = pushNamedLabel(n.label);
234+
check(n.statement);
235+
if (ok) {
236+
popNamedLabel(n.label, currentState);
237+
}
238+
}
239+
240+
function checkWithStatement(n: WithStatement): void {
241+
verifyReachable(n);
242+
check(n.statement);
243+
}
244+
245+
// current assumption: only statements affect CF
246+
function check(n: Node): void {
247+
if (!n || currentState === ControlFlowState.ReportedUnreachable) {
248+
return;
249+
}
250+
switch (n.kind) {
251+
case SyntaxKind.WhileStatement:
252+
checkWhileStatement(<WhileStatement>n);
253+
break;
254+
case SyntaxKind.SourceFile:
255+
checkBlock(<SourceFile>n);
256+
break;
257+
case SyntaxKind.Block:
258+
case SyntaxKind.TryBlock:
259+
case SyntaxKind.CatchBlock:
260+
case SyntaxKind.FinallyBlock:
261+
case SyntaxKind.ModuleBlock:
262+
case SyntaxKind.FunctionBlock:
263+
checkBlock(<Block>n);
264+
break;
265+
case SyntaxKind.IfStatement:
266+
checkIfStatement(<IfStatement>n);
267+
break;
268+
case SyntaxKind.ReturnStatement:
269+
case SyntaxKind.ThrowStatement:
270+
checkReturnOrThrow(n);
271+
break;
272+
case SyntaxKind.BreakStatement:
273+
case SyntaxKind.ContinueStatement:
274+
checkBreakOrContinueStatement(<BreakOrContinueStatement>n);
275+
break;
276+
case SyntaxKind.VariableStatement:
277+
case SyntaxKind.EmptyStatement:
278+
case SyntaxKind.ExpressionStatement:
279+
case SyntaxKind.DebuggerStatement:
280+
verifyReachable(n);
281+
break;
282+
case SyntaxKind.DoStatement:
283+
checkDoStatement(<DoStatement>n);
284+
break;
285+
case SyntaxKind.ForInStatement:
286+
checkForInStatement(<ForInStatement>n);
287+
break;
288+
case SyntaxKind.ForStatement:
289+
checkForStatement(<ForStatement>n);
290+
break;
291+
case SyntaxKind.LabeledStatement:
292+
checkLabelledStatement(<LabeledStatement>n);
293+
break;
294+
case SyntaxKind.SwitchStatement:
295+
checkSwitchStatement(<SwitchStatement>n);
296+
break;
297+
case SyntaxKind.TryStatement:
298+
checkTryStatement(<TryStatement>n);
299+
break;
300+
case SyntaxKind.WithStatement:
301+
checkWithStatement(<WithStatement>n);
302+
break;
303+
}
304+
}
305+
306+
check(decl);
307+
return currentState;
308+
}
309+
310+
const enum ControlFlowState {
311+
Uninitialized = 0,
312+
Reachable = 1,
313+
Unreachable = 2,
314+
ReportedUnreachable = 3,
315+
}
316+
}

src/compiler/diagnosticInformationMap.generated.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,10 @@ module ts {
270270
Type_argument_candidate_1_is_not_a_valid_type_argument_because_it_is_not_a_supertype_of_candidate_0: { code: 2455, category: DiagnosticCategory.Error, key: "Type argument candidate '{1}' is not a valid type argument because it is not a supertype of candidate '{0}'." },
271271
Type_alias_0_circularly_references_itself: { code: 2456, category: DiagnosticCategory.Error, key: "Type alias '{0}' circularly references itself." },
272272
Type_alias_name_cannot_be_0: { code: 2457, category: DiagnosticCategory.Error, key: "Type alias name cannot be '{0}'" },
273+
Not_all_code_paths_return_a_value: { code: 2458, category: DiagnosticCategory.Error, key: "Not all code paths return a value" },
274+
Unreachable_code_detected: { code: 2459, category: DiagnosticCategory.Error, key: "Unreachable code detected" },
275+
Unused_label: { code: 2460, category: DiagnosticCategory.Error, key: "Unused label" },
276+
Fallthrough_case_in_switch: { code: 2461, category: DiagnosticCategory.Error, key: "Fallthrough case in switch" },
273277
Import_declaration_0_is_using_private_name_1: { code: 4000, category: DiagnosticCategory.Error, key: "Import declaration '{0}' is using private name '{1}'." },
274278
Type_parameter_0_of_exported_class_has_or_is_using_name_1_from_private_module_2: { code: 4001, category: DiagnosticCategory.Error, key: "Type parameter '{0}' of exported class has or is using name '{1}' from private module '{2}'." },
275279
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}'." },

src/compiler/diagnosticMessages.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1076,7 +1076,22 @@
10761076
"category": "Error",
10771077
"code": 2457
10781078
},
1079-
1079+
"Not all code paths return a value": {
1080+
"category": "Error",
1081+
"code": 2458
1082+
},
1083+
"Unreachable code detected": {
1084+
"category": "Error",
1085+
"code": 2459
1086+
},
1087+
"Unused label": {
1088+
"category": "Error",
1089+
"code": 2460
1090+
},
1091+
"Fallthrough case in switch": {
1092+
"category": "Error",
1093+
"code": 2461
1094+
},
10801095
"Import declaration '{0}' is using private name '{1}'.": {
10811096
"category": "Error",
10821097
"code": 4000

0 commit comments

Comments
 (0)