]>
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 | |
eb39fafa DC |
198 | function isReachable(segment) { |
199 | return segment.reachable; | |
200 | } | |
201 | ||
202 | module.exports = function(context) { | |
203 | var codePathStack = []; | |
204 | ||
205 | return { | |
206 | // Stores CodePath objects. | |
207 | "onCodePathStart": function(codePath) { | |
208 | codePathStack.push(codePath); | |
209 | }, | |
210 | "onCodePathEnd": function(codePath) { | |
211 | codePathStack.pop(); | |
212 | }, | |
213 | ||
214 | // Checks reachable or not. | |
215 | "ExpressionStatement": function(node) { | |
5422a9cc | 216 | var codePath = codePathStack[codePathStack.length - 1]; |
eb39fafa DC |
217 | |
218 | // Checks the current code path segments. | |
219 | if (!codePath.currentSegments.some(isReachable)) { | |
220 | context.report({message: "Unreachable!", node: node}); | |
221 | } | |
222 | } | |
223 | }; | |
224 | }; | |
225 | ``` | |
226 | ||
227 | See Also: | |
228 | [no-unreachable](https://github.com/eslint/eslint/blob/master/lib/rules/no-unreachable.js), | |
229 | [no-fallthrough](https://github.com/eslint/eslint/blob/master/lib/rules/no-fallthrough.js), | |
230 | [consistent-return](https://github.com/eslint/eslint/blob/master/lib/rules/consistent-return.js) | |
231 | ||
232 | ### To check state of a code path | |
233 | ||
234 | This example is checking whether or not the parameter `cb` is called in every path. | |
235 | Instances of `CodePath` and `CodePathSegment` are shared to every rule. | |
236 | So a rule must not modify those instances. | |
237 | Please use a map of information instead. | |
238 | ||
239 | ```js | |
eb39fafa DC |
240 | function hasCb(node, context) { |
241 | if (node.type.indexOf("Function") !== -1) { | |
242 | return context.getDeclaredVariables(node).some(function(v) { | |
243 | return v.type === "Parameter" && v.name === "cb"; | |
244 | }); | |
245 | } | |
246 | return false; | |
247 | } | |
248 | ||
249 | function isCbCalled(info) { | |
250 | return info.cbCalled; | |
251 | } | |
252 | ||
253 | module.exports = function(context) { | |
254 | var funcInfoStack = []; | |
255 | var segmentInfoMap = Object.create(null); | |
256 | ||
257 | return { | |
258 | // Checks `cb`. | |
259 | "onCodePathStart": function(codePath, node) { | |
260 | funcInfoStack.push({ | |
261 | codePath: codePath, | |
262 | hasCb: hasCb(node, context) | |
263 | }); | |
264 | }, | |
265 | "onCodePathEnd": function(codePath, node) { | |
266 | funcInfoStack.pop(); | |
267 | ||
268 | // Checks `cb` was called in every paths. | |
269 | var cbCalled = codePath.finalSegments.every(function(segment) { | |
270 | var info = segmentInfoMap[segment.id]; | |
271 | return info.cbCalled; | |
272 | }); | |
273 | ||
274 | if (!cbCalled) { | |
275 | context.report({ | |
276 | message: "`cb` should be called in every path.", | |
277 | node: node | |
278 | }); | |
279 | } | |
280 | }, | |
281 | ||
282 | // Manages state of code paths. | |
283 | "onCodePathSegmentStart": function(segment) { | |
5422a9cc TL |
284 | var funcInfo = funcInfoStack[funcInfoStack.length - 1]; |
285 | ||
eb39fafa | 286 | // Ignores if `cb` doesn't exist. |
5422a9cc | 287 | if (!funcInfo.hasCb) { |
eb39fafa DC |
288 | return; |
289 | } | |
290 | ||
291 | // Initialize state of this path. | |
292 | var info = segmentInfoMap[segment.id] = { | |
293 | cbCalled: false | |
294 | }; | |
295 | ||
296 | // If there are the previous paths, merges state. | |
297 | // Checks `cb` was called in every previous path. | |
298 | if (segment.prevSegments.length > 0) { | |
299 | info.cbCalled = segment.prevSegments.every(isCbCalled); | |
300 | } | |
301 | }, | |
302 | ||
303 | // Checks reachable or not. | |
304 | "CallExpression": function(node) { | |
5422a9cc | 305 | var funcInfo = funcInfoStack[funcInfoStack.length - 1]; |
eb39fafa DC |
306 | |
307 | // Ignores if `cb` doesn't exist. | |
308 | if (!funcInfo.hasCb) { | |
309 | return; | |
310 | } | |
311 | ||
312 | // Sets marks that `cb` was called. | |
313 | var callee = node.callee; | |
314 | if (callee.type === "Identifier" && callee.name === "cb") { | |
315 | funcInfo.codePath.currentSegments.forEach(function(segment) { | |
316 | var info = segmentInfoMap[segment.id]; | |
317 | info.cbCalled = true; | |
318 | }); | |
319 | } | |
320 | } | |
321 | }; | |
322 | }; | |
323 | ``` | |
324 | ||
325 | See Also: | |
326 | [constructor-super](https://github.com/eslint/eslint/blob/master/lib/rules/constructor-super.js), | |
327 | [no-this-before-super](https://github.com/eslint/eslint/blob/master/lib/rules/no-this-before-super.js) | |
328 | ||
329 | ## Code Path Examples | |
330 | ||
331 | ### Hello World | |
332 | ||
333 | ```js | |
334 | console.log("Hello world!"); | |
335 | ``` | |
336 | ||
337 | ![Hello World](./code-path-analysis/example-hello-world.svg) | |
338 | ||
339 | ### `IfStatement` | |
340 | ||
341 | ```js | |
342 | if (a) { | |
343 | foo(); | |
344 | } else { | |
345 | bar(); | |
346 | } | |
347 | ``` | |
348 | ||
349 | ![`IfStatement`](./code-path-analysis/example-ifstatement.svg) | |
350 | ||
351 | ### `IfStatement` (chain) | |
352 | ||
353 | ```js | |
354 | if (a) { | |
355 | foo(); | |
356 | } else if (b) { | |
357 | bar(); | |
358 | } else if (c) { | |
359 | hoge(); | |
360 | } | |
361 | ``` | |
362 | ||
363 | ![`IfStatement` (chain)](./code-path-analysis/example-ifstatement-chain.svg) | |
364 | ||
365 | ### `SwitchStatement` | |
366 | ||
367 | ```js | |
368 | switch (a) { | |
369 | case 0: | |
370 | foo(); | |
371 | break; | |
372 | ||
373 | case 1: | |
374 | case 2: | |
375 | bar(); | |
376 | // fallthrough | |
377 | ||
378 | case 3: | |
379 | hoge(); | |
380 | break; | |
381 | } | |
382 | ``` | |
383 | ||
384 | ![`SwitchStatement`](./code-path-analysis/example-switchstatement.svg) | |
385 | ||
386 | ### `SwitchStatement` (has `default`) | |
387 | ||
388 | ```js | |
389 | switch (a) { | |
390 | case 0: | |
391 | foo(); | |
392 | break; | |
393 | ||
394 | case 1: | |
395 | case 2: | |
396 | bar(); | |
397 | // fallthrough | |
398 | ||
399 | case 3: | |
400 | hoge(); | |
401 | break; | |
402 | ||
403 | default: | |
404 | fuga(); | |
405 | break; | |
406 | } | |
407 | ``` | |
408 | ||
409 | ![`SwitchStatement` (has `default`)](./code-path-analysis/example-switchstatement-has-default.svg) | |
410 | ||
411 | ### `TryStatement` (try-catch) | |
412 | ||
413 | ```js | |
414 | try { | |
415 | foo(); | |
416 | if (a) { | |
417 | throw new Error(); | |
418 | } | |
419 | bar(); | |
420 | } catch (err) { | |
421 | hoge(err); | |
422 | } | |
423 | last(); | |
424 | ``` | |
425 | ||
426 | It creates the paths from `try` block to `catch` block at: | |
427 | ||
428 | * `throw` statements. | |
429 | * The first throwable node (e.g. a function call) in the `try` block. | |
430 | * The end of the `try` block. | |
431 | ||
432 | ![`TryStatement` (try-catch)](./code-path-analysis/example-trystatement-try-catch.svg) | |
433 | ||
434 | ### `TryStatement` (try-finally) | |
435 | ||
436 | ```js | |
437 | try { | |
438 | foo(); | |
439 | bar(); | |
440 | } finally { | |
441 | fuga(); | |
442 | } | |
443 | last(); | |
444 | ``` | |
445 | ||
446 | If there is not `catch` block, `finally` block has two current segments. | |
447 | At this time, `CodePath.currentSegments.length` is `2`. | |
448 | One is the normal path, and another is the leaving path (`throw` or `return`). | |
449 | ||
450 | ![`TryStatement` (try-finally)](./code-path-analysis/example-trystatement-try-finally.svg) | |
451 | ||
452 | ### `TryStatement` (try-catch-finally) | |
453 | ||
454 | ```js | |
455 | try { | |
456 | foo(); | |
457 | bar(); | |
458 | } catch (err) { | |
459 | hoge(err); | |
460 | } finally { | |
461 | fuga(); | |
462 | } | |
463 | last(); | |
464 | ``` | |
465 | ||
466 | ![`TryStatement` (try-catch-finally)](./code-path-analysis/example-trystatement-try-catch-finally.svg) | |
467 | ||
468 | ### `WhileStatement` | |
469 | ||
470 | ```js | |
471 | while (a) { | |
472 | foo(); | |
473 | if (b) { | |
474 | continue; | |
475 | } | |
476 | bar(); | |
477 | } | |
478 | ``` | |
479 | ||
480 | ![`WhileStatement`](./code-path-analysis/example-whilestatement.svg) | |
481 | ||
482 | ### `DoWhileStatement` | |
483 | ||
484 | ```js | |
485 | do { | |
486 | foo(); | |
487 | bar(); | |
488 | } while (a); | |
489 | ``` | |
490 | ||
491 | ![`DoWhileStatement`](./code-path-analysis/example-dowhilestatement.svg) | |
492 | ||
493 | ### `ForStatement` | |
494 | ||
495 | ```js | |
496 | for (let i = 0; i < 10; ++i) { | |
497 | foo(); | |
498 | if (b) { | |
499 | break; | |
500 | } | |
501 | bar(); | |
502 | } | |
503 | ``` | |
504 | ||
505 | ![`ForStatement`](./code-path-analysis/example-forstatement.svg) | |
506 | ||
507 | ### `ForStatement` (for ever) | |
508 | ||
509 | ```js | |
510 | for (;;) { | |
511 | foo(); | |
512 | } | |
513 | bar(); | |
514 | ``` | |
515 | ||
516 | ![`ForStatement` (for ever)](./code-path-analysis/example-forstatement-for-ever.svg) | |
517 | ||
518 | ### `ForInStatement` | |
519 | ||
520 | ```js | |
521 | for (let key in obj) { | |
522 | foo(key); | |
523 | } | |
524 | ``` | |
525 | ||
526 | ![`ForInStatement`](./code-path-analysis/example-forinstatement.svg) | |
527 | ||
528 | ### When there is a function | |
529 | ||
530 | ```js | |
531 | function foo(a) { | |
532 | if (a) { | |
533 | return; | |
534 | } | |
535 | bar(); | |
536 | } | |
537 | ||
538 | foo(false); | |
539 | ``` | |
540 | ||
541 | It creates two code paths. | |
542 | ||
543 | * The global's | |
544 | ||
545 | ![When there is a function](./code-path-analysis/example-when-there-is-a-function-g.svg) | |
546 | ||
547 | * The function's | |
548 | ||
549 | ![When there is a function](./code-path-analysis/example-when-there-is-a-function-f.svg) |