]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | # Code Path Analysis Details |
2 | ||
3 | ESLint's rules can use code paths. | |
4 | The code path is execution routes of programs. | |
5 | It forks/joins at such as `if` statements. | |
6 | ||
7 | ```js | |
8 | if (a && b) { | |
9 | foo(); | |
10 | } | |
11 | bar(); | |
12 | ``` | |
13 | ||
14 | ![Code Path Example](./code-path-analysis/helo.svg) | |
15 | ||
16 | ## Objects | |
17 | ||
18 | Program is expressed with several code paths. | |
19 | A code path is expressed with objects of two kinds: `CodePath` and `CodePathSegment`. | |
20 | ||
21 | ### `CodePath` | |
22 | ||
23 | `CodePath` expresses whole of one code path. | |
24 | This object exists for each function and the global. | |
25 | This has references of both the initial segment and the final segments of a code path. | |
26 | ||
27 | `CodePath` has the following properties: | |
28 | ||
29 | * `id` (`string`) - A unique string. Respective rules can use `id` to save additional information for each code path. | |
30 | * `initialSegment` (`CodePathSegment`) - The initial segment of this code path. | |
31 | * `finalSegments` (`CodePathSegment[]`) - The final segments which includes both returned and thrown. | |
32 | * `returnedSegments` (`CodePathSegment[]`) - The final segments which includes only returned. | |
33 | * `thrownSegments` (`CodePathSegment[]`) - The final segments which includes only thrown. | |
34 | * `currentSegments` (`CodePathSegment[]`) - Segments of the current position. | |
35 | * `upper` (`CodePath|null`) - The code path of the upper function/global scope. | |
36 | * `childCodePaths` (`CodePath[]`) - Code paths of functions this code path contains. | |
37 | ||
38 | ### `CodePathSegment` | |
39 | ||
40 | `CodePathSegment` is a part of a code path. | |
41 | A code path is expressed with plural `CodePathSegment` objects, it's similar to doubly linked list. | |
42 | Difference from doubly linked list is what there are forking and merging (the next/prev are plural). | |
43 | ||
44 | `CodePathSegment` has the following properties: | |
45 | ||
46 | * `id` (`string`) - A unique string. Respective rules can use `id` to save additional information for each segment. | |
47 | * `nextSegments` (`CodePathSegment[]`) - The next segments. If forking, there are two or more. If final, there is nothing. | |
48 | * `prevSegments` (`CodePathSegment[]`) - The previous segments. If merging, there are two or more. If initial, there is nothing. | |
49 | * `reachable` (`boolean`) - A flag which shows whether or not it's reachable. This becomes `false` when preceded by `return`, `throw`, `break`, or `continue`. | |
50 | ||
51 | ## Events | |
52 | ||
53 | There are five events related to code paths, and you can define event handlers in rules. | |
54 | ||
55 | ```js | |
56 | module.exports = function(context) { | |
57 | return { | |
58 | /** | |
59 | * This is called at the start of analyzing a code path. | |
60 | * In this time, the code path object has only the initial segment. | |
61 | * | |
62 | * @param {CodePath} codePath - The new code path. | |
63 | * @param {ASTNode} node - The current node. | |
64 | * @returns {void} | |
65 | */ | |
66 | "onCodePathStart": function(codePath, node) { | |
67 | // do something with codePath | |
68 | }, | |
69 | ||
70 | /** | |
71 | * This is called at the end of analyzing a code path. | |
72 | * In this time, the code path object is complete. | |
73 | * | |
74 | * @param {CodePath} codePath - The completed code path. | |
75 | * @param {ASTNode} node - The current node. | |
76 | * @returns {void} | |
77 | */ | |
78 | "onCodePathEnd": function(codePath, node) { | |
79 | // do something with codePath | |
80 | }, | |
81 | ||
82 | /** | |
83 | * This is called when a code path segment was created. | |
84 | * It meant the code path is forked or merged. | |
85 | * In this time, the segment has the previous segments and has been | |
86 | * judged reachable or not. | |
87 | * | |
88 | * @param {CodePathSegment} segment - The new code path segment. | |
89 | * @param {ASTNode} node - The current node. | |
90 | * @returns {void} | |
91 | */ | |
92 | "onCodePathSegmentStart": function(segment, node) { | |
93 | // do something with segment | |
94 | }, | |
95 | ||
96 | /** | |
97 | * This is called when a code path segment was leaved. | |
98 | * In this time, the segment does not have the next segments yet. | |
99 | * | |
100 | * @param {CodePathSegment} segment - The leaved code path segment. | |
101 | * @param {ASTNode} node - The current node. | |
102 | * @returns {void} | |
103 | */ | |
104 | "onCodePathSegmentEnd": function(segment, node) { | |
105 | // do something with segment | |
106 | }, | |
107 | ||
108 | /** | |
109 | * This is called when a code path segment was looped. | |
110 | * Usually segments have each previous segments when created, | |
111 | * but when looped, a segment is added as a new previous segment into a | |
112 | * existing segment. | |
113 | * | |
114 | * @param {CodePathSegment} fromSegment - A code path segment of source. | |
115 | * @param {CodePathSegment} toSegment - A code path segment of destination. | |
116 | * @param {ASTNode} node - The current node. | |
117 | * @returns {void} | |
118 | */ | |
119 | "onCodePathSegmentLoop": function(fromSegment, toSegment, node) { | |
120 | // do something with segment | |
121 | } | |
122 | }; | |
123 | }; | |
124 | ``` | |
125 | ||
126 | ### About `onCodePathSegmentLoop` | |
127 | ||
128 | This event is always fired when the next segment has existed already. | |
129 | That timing is the end of loops mainly. | |
130 | ||
131 | For Example 1: | |
132 | ||
133 | ```js | |
134 | while (a) { | |
135 | a = foo(); | |
136 | } | |
137 | bar(); | |
138 | ``` | |
139 | ||
140 | 1. First, the analysis advances to the end of loop. | |
141 | ||
142 | ![Loop Event's Example 1](./code-path-analysis/loop-event-example-while-1.svg) | |
143 | ||
144 | 2. Second, it creates the looping path. | |
145 | At this time, the next segment has existed already, so the `onCodePathSegmentStart` event is not fired. | |
146 | It fires `onCodePathSegmentLoop` instead. | |
147 | ||
148 | ![Loop Event's Example 2](./code-path-analysis/loop-event-example-while-2.svg) | |
149 | ||
150 | 3. Last, it advances to the end. | |
151 | ||
152 | ![Loop Event's Example 3](./code-path-analysis/loop-event-example-while-3.svg) | |
153 | ||
154 | For example 2: | |
155 | ||
156 | ```js | |
157 | for (let i = 0; i < 10; ++i) { | |
158 | foo(i); | |
159 | } | |
160 | bar(); | |
161 | ``` | |
162 | ||
163 | 1. `for` statements are more complex. | |
164 | First, the analysis advances to `ForStatement.update`. | |
165 | The `update` segment is hovered at first. | |
166 | ||
167 | ![Loop Event's Example 1](./code-path-analysis/loop-event-example-for-1.svg) | |
168 | ||
169 | 2. Second, it advances to `ForStatement.body`. | |
170 | Of course the `body` segment is preceded by the `test` segment. | |
171 | It keeps the `update` segment hovering. | |
172 | ||
173 | ![Loop Event's Example 2](./code-path-analysis/loop-event-example-for-2.svg) | |
174 | ||
175 | 3. Third, it creates the looping path from `body` segment to `update` segment. | |
176 | At this time, the next segment has existed already, so the `onCodePathSegmentStart` event is not fired. | |
177 | It fires `onCodePathSegmentLoop` instead. | |
178 | ||
179 | ![Loop Event's Example 3](./code-path-analysis/loop-event-example-for-3.svg) | |
180 | ||
181 | 4. Fourth, also it creates the looping path from `update` segment to `test` segment. | |
182 | At this time, the next segment has existed already, so the `onCodePathSegmentStart` event is not fired. | |
183 | It fires `onCodePathSegmentLoop` instead. | |
184 | ||
185 | ![Loop Event's Example 4](./code-path-analysis/loop-event-example-for-4.svg) | |
186 | ||
187 | 5. Last, it advances to the end. | |
188 | ||
189 | ![Loop Event's Example 5](./code-path-analysis/loop-event-example-for-5.svg) | |
190 | ||
191 | ||
192 | ||
193 | ## Usage Examples | |
194 | ||
195 | ### To check whether or not this is reachable | |
196 | ||
197 | ```js | |
198 | var last = require("lodash").last; | |
199 | ||
200 | function isReachable(segment) { | |
201 | return segment.reachable; | |
202 | } | |
203 | ||
204 | module.exports = function(context) { | |
205 | var codePathStack = []; | |
206 | ||
207 | return { | |
208 | // Stores CodePath objects. | |
209 | "onCodePathStart": function(codePath) { | |
210 | codePathStack.push(codePath); | |
211 | }, | |
212 | "onCodePathEnd": function(codePath) { | |
213 | codePathStack.pop(); | |
214 | }, | |
215 | ||
216 | // Checks reachable or not. | |
217 | "ExpressionStatement": function(node) { | |
218 | var codePath = last(codePathStack); | |
219 | ||
220 | // Checks the current code path segments. | |
221 | if (!codePath.currentSegments.some(isReachable)) { | |
222 | context.report({message: "Unreachable!", node: node}); | |
223 | } | |
224 | } | |
225 | }; | |
226 | }; | |
227 | ``` | |
228 | ||
229 | See Also: | |
230 | [no-unreachable](https://github.com/eslint/eslint/blob/master/lib/rules/no-unreachable.js), | |
231 | [no-fallthrough](https://github.com/eslint/eslint/blob/master/lib/rules/no-fallthrough.js), | |
232 | [consistent-return](https://github.com/eslint/eslint/blob/master/lib/rules/consistent-return.js) | |
233 | ||
234 | ### To check state of a code path | |
235 | ||
236 | This example is checking whether or not the parameter `cb` is called in every path. | |
237 | Instances of `CodePath` and `CodePathSegment` are shared to every rule. | |
238 | So a rule must not modify those instances. | |
239 | Please use a map of information instead. | |
240 | ||
241 | ```js | |
242 | var last = require("lodash").last; | |
243 | ||
244 | function hasCb(node, context) { | |
245 | if (node.type.indexOf("Function") !== -1) { | |
246 | return context.getDeclaredVariables(node).some(function(v) { | |
247 | return v.type === "Parameter" && v.name === "cb"; | |
248 | }); | |
249 | } | |
250 | return false; | |
251 | } | |
252 | ||
253 | function isCbCalled(info) { | |
254 | return info.cbCalled; | |
255 | } | |
256 | ||
257 | module.exports = function(context) { | |
258 | var funcInfoStack = []; | |
259 | var segmentInfoMap = Object.create(null); | |
260 | ||
261 | return { | |
262 | // Checks `cb`. | |
263 | "onCodePathStart": function(codePath, node) { | |
264 | funcInfoStack.push({ | |
265 | codePath: codePath, | |
266 | hasCb: hasCb(node, context) | |
267 | }); | |
268 | }, | |
269 | "onCodePathEnd": function(codePath, node) { | |
270 | funcInfoStack.pop(); | |
271 | ||
272 | // Checks `cb` was called in every paths. | |
273 | var cbCalled = codePath.finalSegments.every(function(segment) { | |
274 | var info = segmentInfoMap[segment.id]; | |
275 | return info.cbCalled; | |
276 | }); | |
277 | ||
278 | if (!cbCalled) { | |
279 | context.report({ | |
280 | message: "`cb` should be called in every path.", | |
281 | node: node | |
282 | }); | |
283 | } | |
284 | }, | |
285 | ||
286 | // Manages state of code paths. | |
287 | "onCodePathSegmentStart": function(segment) { | |
288 | // Ignores if `cb` doesn't exist. | |
289 | if (!last(funcInfoStack).hasCb) { | |
290 | return; | |
291 | } | |
292 | ||
293 | // Initialize state of this path. | |
294 | var info = segmentInfoMap[segment.id] = { | |
295 | cbCalled: false | |
296 | }; | |
297 | ||
298 | // If there are the previous paths, merges state. | |
299 | // Checks `cb` was called in every previous path. | |
300 | if (segment.prevSegments.length > 0) { | |
301 | info.cbCalled = segment.prevSegments.every(isCbCalled); | |
302 | } | |
303 | }, | |
304 | ||
305 | // Checks reachable or not. | |
306 | "CallExpression": function(node) { | |
307 | var funcInfo = last(funcInfoStack); | |
308 | ||
309 | // Ignores if `cb` doesn't exist. | |
310 | if (!funcInfo.hasCb) { | |
311 | return; | |
312 | } | |
313 | ||
314 | // Sets marks that `cb` was called. | |
315 | var callee = node.callee; | |
316 | if (callee.type === "Identifier" && callee.name === "cb") { | |
317 | funcInfo.codePath.currentSegments.forEach(function(segment) { | |
318 | var info = segmentInfoMap[segment.id]; | |
319 | info.cbCalled = true; | |
320 | }); | |
321 | } | |
322 | } | |
323 | }; | |
324 | }; | |
325 | ``` | |
326 | ||
327 | See Also: | |
328 | [constructor-super](https://github.com/eslint/eslint/blob/master/lib/rules/constructor-super.js), | |
329 | [no-this-before-super](https://github.com/eslint/eslint/blob/master/lib/rules/no-this-before-super.js) | |
330 | ||
331 | ## Code Path Examples | |
332 | ||
333 | ### Hello World | |
334 | ||
335 | ```js | |
336 | console.log("Hello world!"); | |
337 | ``` | |
338 | ||
339 | ![Hello World](./code-path-analysis/example-hello-world.svg) | |
340 | ||
341 | ### `IfStatement` | |
342 | ||
343 | ```js | |
344 | if (a) { | |
345 | foo(); | |
346 | } else { | |
347 | bar(); | |
348 | } | |
349 | ``` | |
350 | ||
351 | ![`IfStatement`](./code-path-analysis/example-ifstatement.svg) | |
352 | ||
353 | ### `IfStatement` (chain) | |
354 | ||
355 | ```js | |
356 | if (a) { | |
357 | foo(); | |
358 | } else if (b) { | |
359 | bar(); | |
360 | } else if (c) { | |
361 | hoge(); | |
362 | } | |
363 | ``` | |
364 | ||
365 | ![`IfStatement` (chain)](./code-path-analysis/example-ifstatement-chain.svg) | |
366 | ||
367 | ### `SwitchStatement` | |
368 | ||
369 | ```js | |
370 | switch (a) { | |
371 | case 0: | |
372 | foo(); | |
373 | break; | |
374 | ||
375 | case 1: | |
376 | case 2: | |
377 | bar(); | |
378 | // fallthrough | |
379 | ||
380 | case 3: | |
381 | hoge(); | |
382 | break; | |
383 | } | |
384 | ``` | |
385 | ||
386 | ![`SwitchStatement`](./code-path-analysis/example-switchstatement.svg) | |
387 | ||
388 | ### `SwitchStatement` (has `default`) | |
389 | ||
390 | ```js | |
391 | switch (a) { | |
392 | case 0: | |
393 | foo(); | |
394 | break; | |
395 | ||
396 | case 1: | |
397 | case 2: | |
398 | bar(); | |
399 | // fallthrough | |
400 | ||
401 | case 3: | |
402 | hoge(); | |
403 | break; | |
404 | ||
405 | default: | |
406 | fuga(); | |
407 | break; | |
408 | } | |
409 | ``` | |
410 | ||
411 | ![`SwitchStatement` (has `default`)](./code-path-analysis/example-switchstatement-has-default.svg) | |
412 | ||
413 | ### `TryStatement` (try-catch) | |
414 | ||
415 | ```js | |
416 | try { | |
417 | foo(); | |
418 | if (a) { | |
419 | throw new Error(); | |
420 | } | |
421 | bar(); | |
422 | } catch (err) { | |
423 | hoge(err); | |
424 | } | |
425 | last(); | |
426 | ``` | |
427 | ||
428 | It creates the paths from `try` block to `catch` block at: | |
429 | ||
430 | * `throw` statements. | |
431 | * The first throwable node (e.g. a function call) in the `try` block. | |
432 | * The end of the `try` block. | |
433 | ||
434 | ![`TryStatement` (try-catch)](./code-path-analysis/example-trystatement-try-catch.svg) | |
435 | ||
436 | ### `TryStatement` (try-finally) | |
437 | ||
438 | ```js | |
439 | try { | |
440 | foo(); | |
441 | bar(); | |
442 | } finally { | |
443 | fuga(); | |
444 | } | |
445 | last(); | |
446 | ``` | |
447 | ||
448 | If there is not `catch` block, `finally` block has two current segments. | |
449 | At this time, `CodePath.currentSegments.length` is `2`. | |
450 | One is the normal path, and another is the leaving path (`throw` or `return`). | |
451 | ||
452 | ![`TryStatement` (try-finally)](./code-path-analysis/example-trystatement-try-finally.svg) | |
453 | ||
454 | ### `TryStatement` (try-catch-finally) | |
455 | ||
456 | ```js | |
457 | try { | |
458 | foo(); | |
459 | bar(); | |
460 | } catch (err) { | |
461 | hoge(err); | |
462 | } finally { | |
463 | fuga(); | |
464 | } | |
465 | last(); | |
466 | ``` | |
467 | ||
468 | ![`TryStatement` (try-catch-finally)](./code-path-analysis/example-trystatement-try-catch-finally.svg) | |
469 | ||
470 | ### `WhileStatement` | |
471 | ||
472 | ```js | |
473 | while (a) { | |
474 | foo(); | |
475 | if (b) { | |
476 | continue; | |
477 | } | |
478 | bar(); | |
479 | } | |
480 | ``` | |
481 | ||
482 | ![`WhileStatement`](./code-path-analysis/example-whilestatement.svg) | |
483 | ||
484 | ### `DoWhileStatement` | |
485 | ||
486 | ```js | |
487 | do { | |
488 | foo(); | |
489 | bar(); | |
490 | } while (a); | |
491 | ``` | |
492 | ||
493 | ![`DoWhileStatement`](./code-path-analysis/example-dowhilestatement.svg) | |
494 | ||
495 | ### `ForStatement` | |
496 | ||
497 | ```js | |
498 | for (let i = 0; i < 10; ++i) { | |
499 | foo(); | |
500 | if (b) { | |
501 | break; | |
502 | } | |
503 | bar(); | |
504 | } | |
505 | ``` | |
506 | ||
507 | ![`ForStatement`](./code-path-analysis/example-forstatement.svg) | |
508 | ||
509 | ### `ForStatement` (for ever) | |
510 | ||
511 | ```js | |
512 | for (;;) { | |
513 | foo(); | |
514 | } | |
515 | bar(); | |
516 | ``` | |
517 | ||
518 | ![`ForStatement` (for ever)](./code-path-analysis/example-forstatement-for-ever.svg) | |
519 | ||
520 | ### `ForInStatement` | |
521 | ||
522 | ```js | |
523 | for (let key in obj) { | |
524 | foo(key); | |
525 | } | |
526 | ``` | |
527 | ||
528 | ![`ForInStatement`](./code-path-analysis/example-forinstatement.svg) | |
529 | ||
530 | ### When there is a function | |
531 | ||
532 | ```js | |
533 | function foo(a) { | |
534 | if (a) { | |
535 | return; | |
536 | } | |
537 | bar(); | |
538 | } | |
539 | ||
540 | foo(false); | |
541 | ``` | |
542 | ||
543 | It creates two code paths. | |
544 | ||
545 | * The global's | |
546 | ||
547 | ![When there is a function](./code-path-analysis/example-when-there-is-a-function-g.svg) | |
548 | ||
549 | * The function's | |
550 | ||
551 | ![When there is a function](./code-path-analysis/example-when-there-is-a-function-f.svg) |