[eslint-plugin-react-hooks] updates for component syntax (#33089)
Adds support for Flow's component and hook syntax. [docs](https://flow.org/en/docs/react/component-syntax/)
Jan Kassens committed
May 2, 2025 at 15:04 UTC
4c4a57c4f9f7f7d44e4cbe868c066e3691cd4038
11 files changed
+3152
-11
packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
+81
-8
@@ -34,7 +34,7 @@ function normalizeIndent(strings) {
34
// }
35
// ***************************************************
36
37
-const tests = {
37
+const allTests = {
38
valid: [
39
{
40
code: normalizeIndent`
@@ -44,6 +44,25 @@ const tests = {
44
}
45
`,
46
},
47
+ {
48
+ syntax: 'flow',
49
+ code: normalizeIndent`
50
+ // Component syntax
51
+ component Button() {
52
+ useHook();
53
+ return <div>Button!</div>;
54
+ }
55
+ `,
56
+ },
57
+ {
58
+ syntax: 'flow',
59
+ code: normalizeIndent`
60
+ // Hook syntax
61
+ hook useSampleHook() {
62
+ useHook();
63
+ }
64
+ `,
65
+ },
66
{
67
code: normalizeIndent`
68
// Valid because components can use hooks.
@@ -563,6 +582,28 @@ const tests = {
582
},
583
],
584
invalid: [
585
+ {
586
+ syntax: 'flow',
587
+ code: normalizeIndent`
588
+ component Button(cond: boolean) {
589
+ if (cond) {
590
+ useConditionalHook();
591
+ }
592
+ }
593
+ `,
594
+ errors: [conditionalError('useConditionalHook')],
595
+ },
596
+ {
597
+ syntax: 'flow',
598
+ code: normalizeIndent`
599
+ hook useTest(cond: boolean) {
600
+ if (cond) {
601
+ useConditionalHook();
602
+ }
603
+ }
604
+ `,
605
+ errors: [conditionalError('useConditionalHook')],
606
+ },
607
{
608
code: normalizeIndent`
609
// Invalid because it's dangerous and might not warn otherwise.
@@ -1287,8 +1328,8 @@ const tests = {
1328
};
1329
1330
if (__EXPERIMENTAL__) {
1290
- tests.valid = [
1291
- ...tests.valid,
1331
+ allTests.valid = [
1332
+ ...allTests.valid,
1333
{
1334
code: normalizeIndent`
1335
// Valid because functions created with useEffectEvent can be called in a useEffect.
@@ -1385,8 +1426,8 @@ if (__EXPERIMENTAL__) {
1426
`,
1427
},
1428
];
1388
- tests.invalid = [
1389
- ...tests.invalid,
1429
+ allTests.invalid = [
1430
+ ...allTests.invalid,
1431
{
1432
code: normalizeIndent`
1433
function MyComponent({ theme }) {
@@ -1536,7 +1577,7 @@ function asyncComponentHookError(fn) {
1577
if (!process.env.CI) {
1578
let only = [];
1579
let skipped = [];
1539
- [...tests.valid, ...tests.invalid].forEach(t => {
1580
+ [...allTests.valid, ...allTests.invalid].forEach(t => {
1581
if (t.skip) {
1582
delete t.skip;
1583
skipped.push(t);
@@ -1555,10 +1596,23 @@ if (!process.env.CI) {
1596
}
1597
return true;
1598
};
1558
- tests.valid = tests.valid.filter(predicate);
1559
- tests.invalid = tests.invalid.filter(predicate);
1599
+ allTests.valid = allTests.valid.filter(predicate);
1600
+ allTests.invalid = allTests.invalid.filter(predicate);
1601
+}
1602
+
1603
+function filteredTests(predicate) {
1604
+ return {
1605
+ valid: allTests.valid.filter(predicate),
1606
+ invalid: allTests.invalid.filter(predicate),
1607
+ };
1608
}
1609
1610
+const flowTests = filteredTests(t => t.syntax == null || t.syntax === 'flow');
1611
+const tests = filteredTests(t => t.syntax !== 'flow');
1612
+
1613
+allTests.valid.forEach(t => delete t.syntax);
1614
+allTests.invalid.forEach(t => delete t.syntax);
1615
+
1616
describe('rules-of-hooks/rules-of-hooks', () => {
1617
const parserOptionsV7 = {
1618
ecmaFeatures: {
@@ -1594,6 +1648,25 @@ describe('rules-of-hooks/rules-of-hooks', () => {
1648
tests
1649
);
1650
1651
+ new ESLintTesterV7({
1652
+ parser: require.resolve('hermes-eslint'),
1653
+ parserOptions: {
1654
+ sourceType: 'module',
1655
+ enableExperimentalComponentSyntax: true,
1656
+ },
1657
+ }).run('eslint: v7, parser: hermes-eslint', ReactHooksESLintRule, flowTests);
1658
+
1659
+ new ESLintTesterV9({
1660
+ languageOptions: {
1661
+ ...languageOptionsV9,
1662
+ parser: require('hermes-eslint'),
1663
+ parserOptions: {
1664
+ sourceType: 'module',
1665
+ enableExperimentalComponentSyntax: true,
1666
+ },
1667
+ },
1668
+ }).run('eslint: v9, parser: hermes-eslint', ReactHooksESLintRule, flowTests);
1669
+
1670
new ESLintTesterV7({
1671
parser: require.resolve('@typescript-eslint/parser-v2'),
1672
parserOptions: parserOptionsV7,
packages/eslint-plugin-react-hooks/src/code-path-analysis/LICENSE
new
+19
@@ -0,0 +1,19 @@
1
+Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
2
+
3
+Permission is hereby granted, free of charge, to any person obtaining a copy
4
+of this software and associated documentation files (the "Software"), to deal
5
+in the Software without restriction, including without limitation the rights
6
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+copies of the Software, and to permit persons to whom the Software is
8
+furnished to do so, subject to the following conditions:
9
+
10
+The above copyright notice and this permission notice shall be included in
11
+all copies or substantial portions of the Software.
12
+
13
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+THE SOFTWARE.
packages/eslint-plugin-react-hooks/src/code-path-analysis/README.md
new
+6
@@ -0,0 +1,6 @@
1
+# Code Path Analyzer
2
+
3
+This code is a forked version of ESLints Code Path Analyzer which includes
4
+support for Component Syntax.
5
+
6
+Forked from: https://github.com/eslint/eslint/tree/main/lib/linter/code-path-analysis
packages/eslint-plugin-react-hooks/src/code-path-analysis/assert.js
new
+9
@@ -0,0 +1,9 @@
1
+'use strict';
2
+
3
+function assert(cond) {
4
+ if (!cond) {
5
+ throw new Error('Assertion violated.');
6
+ }
7
+}
8
+
9
+module.exports = assert;
packages/eslint-plugin-react-hooks/src/code-path-analysis/code-path-analyzer.js
new
+802
@@ -0,0 +1,802 @@
1
+'use strict';
2
+
3
+/* eslint-disable react-internal/no-primitive-constructors */
4
+
5
+//------------------------------------------------------------------------------
6
+// Requirements
7
+//------------------------------------------------------------------------------
8
+
9
+// eslint-disable-next-line
10
+const assert = require('./assert');
11
+// eslint-disable-next-line
12
+const CodePath = require('./code-path');
13
+// eslint-disable-next-line
14
+const CodePathSegment = require('./code-path-segment');
15
+// eslint-disable-next-line
16
+const IdGenerator = require('./id-generator');
17
+
18
+const breakableTypePattern =
19
+ /^(?:(?:Do)?While|For(?:In|Of)?|Switch)Statement$/u;
20
+
21
+//------------------------------------------------------------------------------
22
+// Helpers
23
+//------------------------------------------------------------------------------
24
+
25
+/**
26
+ * Checks whether or not a given node is a `case` node (not `default` node).
27
+ * @param {ASTNode} node A `SwitchCase` node to check.
28
+ * @returns {boolean} `true` if the node is a `case` node (not `default` node).
29
+ */
30
+function isCaseNode(node) {
31
+ return Boolean(node.test);
32
+}
33
+
34
+/**
35
+ * Checks if a given node appears as the value of a PropertyDefinition node.
36
+ * @param {ASTNode} node THe node to check.
37
+ * @returns {boolean} `true` if the node is a PropertyDefinition value,
38
+ * false if not.
39
+ */
40
+function isPropertyDefinitionValue(node) {
41
+ const parent = node.parent;
42
+
43
+ return (
44
+ parent && parent.type === 'PropertyDefinition' && parent.value === node
45
+ );
46
+}
47
+
48
+/**
49
+ * Checks whether the given logical operator is taken into account for the code
50
+ * path analysis.
51
+ * @param {string} operator The operator found in the LogicalExpression node
52
+ * @returns {boolean} `true` if the operator is "&&" or "||" or "??"
53
+ */
54
+function isHandledLogicalOperator(operator) {
55
+ return operator === '&&' || operator === '||' || operator === '??';
56
+}
57
+
58
+/**
59
+ * Checks whether the given assignment operator is a logical assignment operator.
60
+ * Logical assignments are taken into account for the code path analysis
61
+ * because of their short-circuiting semantics.
62
+ * @param {string} operator The operator found in the AssignmentExpression node
63
+ * @returns {boolean} `true` if the operator is "&&=" or "||=" or "??="
64
+ */
65
+function isLogicalAssignmentOperator(operator) {
66
+ return operator === '&&=' || operator === '||=' || operator === '??=';
67
+}
68
+
69
+/**
70
+ * Gets the label if the parent node of a given node is a LabeledStatement.
71
+ * @param {ASTNode} node A node to get.
72
+ * @returns {string|null} The label or `null`.
73
+ */
74
+function getLabel(node) {
75
+ if (node.parent.type === 'LabeledStatement') {
76
+ return node.parent.label.name;
77
+ }
78
+ return null;
79
+}
80
+
81
+/**
82
+ * Checks whether or not a given logical expression node goes different path
83
+ * between the `true` case and the `false` case.
84
+ * @param {ASTNode} node A node to check.
85
+ * @returns {boolean} `true` if the node is a test of a choice statement.
86
+ */
87
+function isForkingByTrueOrFalse(node) {
88
+ const parent = node.parent;
89
+
90
+ switch (parent.type) {
91
+ case 'ConditionalExpression':
92
+ case 'IfStatement':
93
+ case 'WhileStatement':
94
+ case 'DoWhileStatement':
95
+ case 'ForStatement':
96
+ return parent.test === node;
97
+
98
+ case 'LogicalExpression':
99
+ return isHandledLogicalOperator(parent.operator);
100
+
101
+ case 'AssignmentExpression':
102
+ return isLogicalAssignmentOperator(parent.operator);
103
+
104
+ default:
105
+ return false;
106
+ }
107
+}
108
+
109
+/**
110
+ * Gets the boolean value of a given literal node.
111
+ *
112
+ * This is used to detect infinity loops (e.g. `while (true) {}`).
113
+ * Statements preceded by an infinity loop are unreachable if the loop didn't
114
+ * have any `break` statement.
115
+ * @param {ASTNode} node A node to get.
116
+ * @returns {boolean|undefined} a boolean value if the node is a Literal node,
117
+ * otherwise `undefined`.
118
+ */
119
+function getBooleanValueIfSimpleConstant(node) {
120
+ if (node.type === 'Literal') {
121
+ return Boolean(node.value);
122
+ }
123
+ return void 0;
124
+}
125
+
126
+/**
127
+ * Checks that a given identifier node is a reference or not.
128
+ *
129
+ * This is used to detect the first throwable node in a `try` block.
130
+ * @param {ASTNode} node An Identifier node to check.
131
+ * @returns {boolean} `true` if the node is a reference.
132
+ */
133
+function isIdentifierReference(node) {
134
+ const parent = node.parent;
135
+
136
+ switch (parent.type) {
137
+ case 'LabeledStatement':
138
+ case 'BreakStatement':
139
+ case 'ContinueStatement':
140
+ case 'ArrayPattern':
141
+ case 'RestElement':
142
+ case 'ImportSpecifier':
143
+ case 'ImportDefaultSpecifier':
144
+ case 'ImportNamespaceSpecifier':
145
+ case 'CatchClause':
146
+ return false;
147
+
148
+ case 'FunctionDeclaration':
149
+ case 'ComponentDeclaration':
150
+ case 'HookDeclaration':
151
+ case 'FunctionExpression':
152
+ case 'ArrowFunctionExpression':
153
+ case 'ClassDeclaration':
154
+ case 'ClassExpression':
155
+ case 'VariableDeclarator':
156
+ return parent.id !== node;
157
+
158
+ case 'Property':
159
+ case 'PropertyDefinition':
160
+ case 'MethodDefinition':
161
+ return parent.key !== node || parent.computed || parent.shorthand;
162
+
163
+ case 'AssignmentPattern':
164
+ return parent.key !== node;
165
+
166
+ default:
167
+ return true;
168
+ }
169
+}
170
+
171
+/**
172
+ * Updates the current segment with the head segment.
173
+ * This is similar to local branches and tracking branches of git.
174
+ *
175
+ * To separate the current and the head is in order to not make useless segments.
176
+ *
177
+ * In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd"
178
+ * events are fired.
179
+ * @param {CodePathAnalyzer} analyzer The instance.
180
+ * @param {ASTNode} node The current AST node.
181
+ * @returns {void}
182
+ */
183
+function forwardCurrentToHead(analyzer, node) {
184
+ const codePath = analyzer.codePath;
185
+ const state = CodePath.getState(codePath);
186
+ const currentSegments = state.currentSegments;
187
+ const headSegments = state.headSegments;
188
+ const end = Math.max(currentSegments.length, headSegments.length);
189
+ let i, currentSegment, headSegment;
190
+
191
+ // Fires leaving events.
192
+ for (i = 0; i < end; ++i) {
193
+ currentSegment = currentSegments[i];
194
+ headSegment = headSegments[i];
195
+
196
+ if (currentSegment !== headSegment && currentSegment) {
197
+ if (currentSegment.reachable) {
198
+ analyzer.emitter.emit('onCodePathSegmentEnd', currentSegment, node);
199
+ }
200
+ }
201
+ }
202
+
203
+ // Update state.
204
+ state.currentSegments = headSegments;
205
+
206
+ // Fires entering events.
207
+ for (i = 0; i < end; ++i) {
208
+ currentSegment = currentSegments[i];
209
+ headSegment = headSegments[i];
210
+
211
+ if (currentSegment !== headSegment && headSegment) {
212
+ CodePathSegment.markUsed(headSegment);
213
+ if (headSegment.reachable) {
214
+ analyzer.emitter.emit('onCodePathSegmentStart', headSegment, node);
215
+ }
216
+ }
217
+ }
218
+}
219
+
220
+/**
221
+ * Updates the current segment with empty.
222
+ * This is called at the last of functions or the program.
223
+ * @param {CodePathAnalyzer} analyzer The instance.
224
+ * @param {ASTNode} node The current AST node.
225
+ * @returns {void}
226
+ */
227
+function leaveFromCurrentSegment(analyzer, node) {
228
+ const state = CodePath.getState(analyzer.codePath);
229
+ const currentSegments = state.currentSegments;
230
+
231
+ for (let i = 0; i < currentSegments.length; ++i) {
232
+ const currentSegment = currentSegments[i];
233
+ if (currentSegment.reachable) {
234
+ analyzer.emitter.emit('onCodePathSegmentEnd', currentSegment, node);
235
+ }
236
+ }
237
+
238
+ state.currentSegments = [];
239
+}
240
+
241
+/**
242
+ * Updates the code path due to the position of a given node in the parent node
243
+ * thereof.
244
+ *
245
+ * For example, if the node is `parent.consequent`, this creates a fork from the
246
+ * current path.
247
+ * @param {CodePathAnalyzer} analyzer The instance.
248
+ * @param {ASTNode} node The current AST node.
249
+ * @returns {void}
250
+ */
251
+function preprocess(analyzer, node) {
252
+ const codePath = analyzer.codePath;
253
+ const state = CodePath.getState(codePath);
254
+ const parent = node.parent;
255
+
256
+ switch (parent.type) {
257
+ // The `arguments.length == 0` case is in `postprocess` function.
258
+ case 'CallExpression':
259
+ if (
260
+ parent.optional === true &&
261
+ parent.arguments.length >= 1 &&
262
+ parent.arguments[0] === node
263
+ ) {
264
+ state.makeOptionalRight();
265
+ }
266
+ break;
267
+ case 'MemberExpression':
268
+ if (parent.optional === true && parent.property === node) {
269
+ state.makeOptionalRight();
270
+ }
271
+ break;
272
+
273
+ case 'LogicalExpression':
274
+ if (parent.right === node && isHandledLogicalOperator(parent.operator)) {
275
+ state.makeLogicalRight();
276
+ }
277
+ break;
278
+
279
+ case 'AssignmentExpression':
280
+ if (
281
+ parent.right === node &&
282
+ isLogicalAssignmentOperator(parent.operator)
283
+ ) {
284
+ state.makeLogicalRight();
285
+ }
286
+ break;
287
+
288
+ case 'ConditionalExpression':
289
+ case 'IfStatement':
290
+ /*
291
+ * Fork if this node is at `consequent`/`alternate`.
292
+ * `popForkContext()` exists at `IfStatement:exit` and
293
+ * `ConditionalExpression:exit`.
294
+ */
295
+ if (parent.consequent === node) {
296
+ state.makeIfConsequent();
297
+ } else if (parent.alternate === node) {
298
+ state.makeIfAlternate();
299
+ }
300
+ break;
301
+
302
+ case 'SwitchCase':
303
+ if (parent.consequent[0] === node) {
304
+ state.makeSwitchCaseBody(false, !parent.test);
305
+ }
306
+ break;
307
+
308
+ case 'TryStatement':
309
+ if (parent.handler === node) {
310
+ state.makeCatchBlock();
311
+ } else if (parent.finalizer === node) {
312
+ state.makeFinallyBlock();
313
+ }
314
+ break;
315
+
316
+ case 'WhileStatement':
317
+ if (parent.test === node) {
318
+ state.makeWhileTest(getBooleanValueIfSimpleConstant(node));
319
+ } else {
320
+ assert(parent.body === node);
321
+ state.makeWhileBody();
322
+ }
323
+ break;
324
+
325
+ case 'DoWhileStatement':
326
+ if (parent.body === node) {
327
+ state.makeDoWhileBody();
328
+ } else {
329
+ assert(parent.test === node);
330
+ state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node));
331
+ }
332
+ break;
333
+
334
+ case 'ForStatement':
335
+ if (parent.test === node) {
336
+ state.makeForTest(getBooleanValueIfSimpleConstant(node));
337
+ } else if (parent.update === node) {
338
+ state.makeForUpdate();
339
+ } else if (parent.body === node) {
340
+ state.makeForBody();
341
+ }
342
+ break;
343
+
344
+ case 'ForInStatement':
345
+ case 'ForOfStatement':
346
+ if (parent.left === node) {
347
+ state.makeForInOfLeft();
348
+ } else if (parent.right === node) {
349
+ state.makeForInOfRight();
350
+ } else {
351
+ assert(parent.body === node);
352
+ state.makeForInOfBody();
353
+ }
354
+ break;
355
+
356
+ case 'AssignmentPattern':
357
+ /*
358
+ * Fork if this node is at `right`.
359
+ * `left` is executed always, so it uses the current path.
360
+ * `popForkContext()` exists at `AssignmentPattern:exit`.
361
+ */
362
+ if (parent.right === node) {
363
+ state.pushForkContext();
364
+ state.forkBypassPath();
365
+ state.forkPath();
366
+ }
367
+ break;
368
+
369
+ default:
370
+ break;
371
+ }
372
+}
373
+
374
+/**
375
+ * Updates the code path due to the type of a given node in entering.
376
+ * @param {CodePathAnalyzer} analyzer The instance.
377
+ * @param {ASTNode} node The current AST node.
378
+ * @returns {void}
379
+ */
380
+function processCodePathToEnter(analyzer, node) {
381
+ let codePath = analyzer.codePath;
382
+ let state = codePath && CodePath.getState(codePath);
383
+ const parent = node.parent;
384
+
385
+ /**
386
+ * Creates a new code path and trigger the onCodePathStart event
387
+ * based on the currently selected node.
388
+ * @param {string} origin The reason the code path was started.
389
+ * @returns {void}
390
+ */
391
+ function startCodePath(origin) {
392
+ if (codePath) {
393
+ // Emits onCodePathSegmentStart events if updated.
394
+ forwardCurrentToHead(analyzer, node);
395
+ }
396
+
397
+ // Create the code path of this scope.
398
+ codePath = analyzer.codePath = new CodePath({
399
+ id: analyzer.idGenerator.next(),
400
+ origin,
401
+ upper: codePath,
402
+ onLooped: analyzer.onLooped,
403
+ });
404
+ state = CodePath.getState(codePath);
405
+
406
+ // Emits onCodePathStart events.
407
+ analyzer.emitter.emit('onCodePathStart', codePath, node);
408
+ }
409
+
410
+ /*
411
+ * Special case: The right side of class field initializer is considered
412
+ * to be its own function, so we need to start a new code path in this
413
+ * case.
414
+ */
415
+ if (isPropertyDefinitionValue(node)) {
416
+ startCodePath('class-field-initializer');
417
+
418
+ /*
419
+ * Intentional fall through because `node` needs to also be
420
+ * processed by the code below. For example, if we have:
421
+ *
422
+ * class Foo {
423
+ * a = () => {}
424
+ * }
425
+ *
426
+ * In this case, we also need start a second code path.
427
+ */
428
+ }
429
+
430
+ switch (node.type) {
431
+ case 'Program':
432
+ startCodePath('program');
433
+ break;
434
+
435
+ case 'FunctionDeclaration':
436
+ case 'ComponentDeclaration':
437
+ case 'HookDeclaration':
438
+ case 'FunctionExpression':
439
+ case 'ArrowFunctionExpression':
440
+ startCodePath('function');
441
+ break;
442
+
443
+ case 'StaticBlock':
444
+ startCodePath('class-static-block');
445
+ break;
446
+
447
+ case 'ChainExpression':
448
+ state.pushChainContext();
449
+ break;
450
+ case 'CallExpression':
451
+ if (node.optional === true) {
452
+ state.makeOptionalNode();
453
+ }
454
+ break;
455
+ case 'MemberExpression':
456
+ if (node.optional === true) {
457
+ state.makeOptionalNode();
458
+ }
459
+ break;
460
+
461
+ case 'LogicalExpression':
462
+ if (isHandledLogicalOperator(node.operator)) {
463
+ state.pushChoiceContext(node.operator, isForkingByTrueOrFalse(node));
464
+ }
465
+ break;
466
+
467
+ case 'AssignmentExpression':
468
+ if (isLogicalAssignmentOperator(node.operator)) {
469
+ state.pushChoiceContext(
470
+ node.operator.slice(0, -1), // removes `=` from the end
471
+ isForkingByTrueOrFalse(node),
472
+ );
473
+ }
474
+ break;
475
+
476
+ case 'ConditionalExpression':
477
+ case 'IfStatement':
478
+ state.pushChoiceContext('test', false);
479
+ break;
480
+
481
+ case 'SwitchStatement':
482
+ state.pushSwitchContext(node.cases.some(isCaseNode), getLabel(node));
483
+ break;
484
+
485
+ case 'TryStatement':
486
+ state.pushTryContext(Boolean(node.finalizer));
487
+ break;
488
+
489
+ case 'SwitchCase':
490
+ /*
491
+ * Fork if this node is after the 2st node in `cases`.
492
+ * It's similar to `else` blocks.
493
+ * The next `test` node is processed in this path.
494
+ */
495
+ if (parent.discriminant !== node && parent.cases[0] !== node) {
496
+ state.forkPath();
497
+ }
498
+ break;
499
+
500
+ case 'WhileStatement':
501
+ case 'DoWhileStatement':
502
+ case 'ForStatement':
503
+ case 'ForInStatement':
504
+ case 'ForOfStatement':
505
+ state.pushLoopContext(node.type, getLabel(node));
506
+ break;
507
+
508
+ case 'LabeledStatement':
509
+ if (!breakableTypePattern.test(node.body.type)) {
510
+ state.pushBreakContext(false, node.label.name);
511
+ }
512
+ break;
513
+
514
+ default:
515
+ break;
516
+ }
517
+
518
+ // Emits onCodePathSegmentStart events if updated.
519
+ forwardCurrentToHead(analyzer, node);
520
+}
521
+
522
+/**
523
+ * Updates the code path due to the type of a given node in leaving.
524
+ * @param {CodePathAnalyzer} analyzer The instance.
525
+ * @param {ASTNode} node The current AST node.
526
+ * @returns {void}
527
+ */
528
+function processCodePathToExit(analyzer, node) {
529
+ const codePath = analyzer.codePath;
530
+ const state = CodePath.getState(codePath);
531
+ let dontForward = false;
532
+
533
+ switch (node.type) {
534
+ case 'ChainExpression':
535
+ state.popChainContext();
536
+ break;
537
+
538
+ case 'IfStatement':
539
+ case 'ConditionalExpression':
540
+ state.popChoiceContext();
541
+ break;
542
+
543
+ case 'LogicalExpression':
544
+ if (isHandledLogicalOperator(node.operator)) {
545
+ state.popChoiceContext();
546
+ }
547
+ break;
548
+
549
+ case 'AssignmentExpression':
550
+ if (isLogicalAssignmentOperator(node.operator)) {
551
+ state.popChoiceContext();
552
+ }
553
+ break;
554
+
555
+ case 'SwitchStatement':
556
+ state.popSwitchContext();
557
+ break;
558
+
559
+ case 'SwitchCase':
560
+ /*
561
+ * This is the same as the process at the 1st `consequent` node in
562
+ * `preprocess` function.
563
+ * Must do if this `consequent` is empty.
564
+ */
565
+ if (node.consequent.length === 0) {
566
+ state.makeSwitchCaseBody(true, !node.test);
567
+ }
568
+ if (state.forkContext.reachable) {
569
+ dontForward = true;
570
+ }
571
+ break;
572
+
573
+ case 'TryStatement':
574
+ state.popTryContext();
575
+ break;
576
+
577
+ case 'BreakStatement':
578
+ forwardCurrentToHead(analyzer, node);
579
+ state.makeBreak(node.label && node.label.name);
580
+ dontForward = true;
581
+ break;
582
+
583
+ case 'ContinueStatement':
584
+ forwardCurrentToHead(analyzer, node);
585
+ state.makeContinue(node.label && node.label.name);
586
+ dontForward = true;
587
+ break;
588
+
589
+ case 'ReturnStatement':
590
+ forwardCurrentToHead(analyzer, node);
591
+ state.makeReturn();
592
+ dontForward = true;
593
+ break;
594
+
595
+ case 'ThrowStatement':
596
+ forwardCurrentToHead(analyzer, node);
597
+ state.makeThrow();
598
+ dontForward = true;
599
+ break;
600
+
601
+ case 'Identifier':
602
+ if (isIdentifierReference(node)) {
603
+ state.makeFirstThrowablePathInTryBlock();
604
+ dontForward = true;
605
+ }
606
+ break;
607
+
608
+ case 'CallExpression':
609
+ case 'ImportExpression':
610
+ case 'MemberExpression':
611
+ case 'NewExpression':
612
+ case 'YieldExpression':
613
+ state.makeFirstThrowablePathInTryBlock();
614
+ break;
615
+
616
+ case 'WhileStatement':
617
+ case 'DoWhileStatement':
618
+ case 'ForStatement':
619
+ case 'ForInStatement':
620
+ case 'ForOfStatement':
621
+ state.popLoopContext();
622
+ break;
623
+
624
+ case 'AssignmentPattern':
625
+ state.popForkContext();
626
+ break;
627
+
628
+ case 'LabeledStatement':
629
+ if (!breakableTypePattern.test(node.body.type)) {
630
+ state.popBreakContext();
631
+ }
632
+ break;
633
+
634
+ default:
635
+ break;
636
+ }
637
+
638
+ // Emits onCodePathSegmentStart events if updated.
639
+ if (!dontForward) {
640
+ forwardCurrentToHead(analyzer, node);
641
+ }
642
+}
643
+
644
+/**
645
+ * Updates the code path to finalize the current code path.
646
+ * @param {CodePathAnalyzer} analyzer The instance.
647
+ * @param {ASTNode} node The current AST node.
648
+ * @returns {void}
649
+ */
650
+function postprocess(analyzer, node) {
651
+ /**
652
+ * Ends the code path for the current node.
653
+ * @returns {void}
654
+ */
655
+ function endCodePath() {
656
+ let codePath = analyzer.codePath;
657
+
658
+ // Mark the current path as the final node.
659
+ CodePath.getState(codePath).makeFinal();
660
+
661
+ // Emits onCodePathSegmentEnd event of the current segments.
662
+ leaveFromCurrentSegment(analyzer, node);
663
+
664
+ // Emits onCodePathEnd event of this code path.
665
+ analyzer.emitter.emit('onCodePathEnd', codePath, node);
666
+
667
+ codePath = analyzer.codePath = analyzer.codePath.upper;
668
+ }
669
+
670
+ switch (node.type) {
671
+ case 'Program':
672
+ case 'FunctionDeclaration':
673
+ case 'ComponentDeclaration':
674
+ case 'HookDeclaration':
675
+ case 'FunctionExpression':
676
+ case 'ArrowFunctionExpression':
677
+ case 'StaticBlock': {
678
+ endCodePath();
679
+ break;
680
+ }
681
+
682
+ // The `arguments.length >= 1` case is in `preprocess` function.
683
+ case 'CallExpression':
684
+ if (node.optional === true && node.arguments.length === 0) {
685
+ CodePath.getState(analyzer.codePath).makeOptionalRight();
686
+ }
687
+ break;
688
+
689
+ default:
690
+ break;
691
+ }
692
+
693
+ /*
694
+ * Special case: The right side of class field initializer is considered
695
+ * to be its own function, so we need to end a code path in this
696
+ * case.
697
+ *
698
+ * We need to check after the other checks in order to close the
699
+ * code paths in the correct order for code like this:
700
+ *
701
+ *
702
+ * class Foo {
703
+ * a = () => {}
704
+ * }
705
+ *
706
+ * In this case, The ArrowFunctionExpression code path is closed first
707
+ * and then we need to close the code path for the PropertyDefinition
708
+ * value.
709
+ */
710
+ if (isPropertyDefinitionValue(node)) {
711
+ endCodePath();
712
+ }
713
+}
714
+
715
+//------------------------------------------------------------------------------
716
+// Public Interface
717
+//------------------------------------------------------------------------------
718
+
719
+/**
720
+ * The class to analyze code paths.
721
+ * This class implements the EventGenerator interface.
722
+ */
723
+class CodePathAnalyzer {
724
+ /**
725
+ * @param {EventGenerator} eventGenerator An event generator to wrap.
726
+ */
727
+ constructor(emitters) {
728
+ this.emitter = {
729
+ emit(event, ...args) {
730
+ emitters[event]?.(...args);
731
+ },
732
+ };
733
+ this.codePath = null;
734
+ this.idGenerator = new IdGenerator('s');
735
+ this.currentNode = null;
736
+ this.onLooped = this.onLooped.bind(this);
737
+ }
738
+
739
+ /**
740
+ * Does the process to enter a given AST node.
741
+ * This updates state of analysis and calls `enterNode` of the wrapped.
742
+ * @param {ASTNode} node A node which is entering.
743
+ * @returns {void}
744
+ */
745
+ enterNode(node) {
746
+ this.currentNode = node;
747
+
748
+ // Updates the code path due to node's position in its parent node.
749
+ if (node.parent) {
750
+ preprocess(this, node);
751
+ }
752
+
753
+ /*
754
+ * Updates the code path.
755
+ * And emits onCodePathStart/onCodePathSegmentStart events.
756
+ */
757
+ processCodePathToEnter(this, node);
758
+
759
+ this.currentNode = null;
760
+ }
761
+
762
+ /**
763
+ * Does the process to leave a given AST node.
764
+ * This updates state of analysis and calls `leaveNode` of the wrapped.
765
+ * @param {ASTNode} node A node which is leaving.
766
+ * @returns {void}
767
+ */
768
+ leaveNode(node) {
769
+ this.currentNode = node;
770
+
771
+ /*
772
+ * Updates the code path.
773
+ * And emits onCodePathStart/onCodePathSegmentStart events.
774
+ */
775
+ processCodePathToExit(this, node);
776
+
777
+ // Emits the last onCodePathStart/onCodePathSegmentStart events.
778
+ postprocess(this, node);
779
+
780
+ this.currentNode = null;
781
+ }
782
+
783
+ /**
784
+ * This is called on a code path looped.
785
+ * Then this raises a looped event.
786
+ * @param {CodePathSegment} fromSegment A segment of prev.
787
+ * @param {CodePathSegment} toSegment A segment of next.
788
+ * @returns {void}
789
+ */
790
+ onLooped(fromSegment, toSegment) {
791
+ if (fromSegment.reachable && toSegment.reachable) {
792
+ this.emitter.emit(
793
+ 'onCodePathSegmentLoop',
794
+ fromSegment,
795
+ toSegment,
796
+ this.currentNode,
797
+ );
798
+ }
799
+ }
800
+}
801
+
802
+module.exports = CodePathAnalyzer;
packages/eslint-plugin-react-hooks/src/code-path-analysis/code-path-segment.js
new
+225
@@ -0,0 +1,225 @@
1
+'use strict';
2
+
3
+//------------------------------------------------------------------------------
4
+// Requirements
5
+//------------------------------------------------------------------------------
6
+
7
+//------------------------------------------------------------------------------
8
+// Helpers
9
+//------------------------------------------------------------------------------
10
+
11
+/**
12
+ * Checks whether or not a given segment is reachable.
13
+ * @param {CodePathSegment} segment A segment to check.
14
+ * @returns {boolean} `true` if the segment is reachable.
15
+ */
16
+function isReachable(segment) {
17
+ return segment.reachable;
18
+}
19
+
20
+//------------------------------------------------------------------------------
21
+// Public Interface
22
+//------------------------------------------------------------------------------
23
+
24
+/**
25
+ * A code path segment.
26
+ */
27
+class CodePathSegment {
28
+ /**
29
+ * @param {string} id An identifier.
30
+ * @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
31
+ * This array includes unreachable segments.
32
+ * @param {boolean} reachable A flag which shows this is reachable.
33
+ */
34
+ constructor(id, allPrevSegments, reachable) {
35
+ /**
36
+ * The identifier of this code path.
37
+ * Rules use it to store additional information of each rule.
38
+ * @type {string}
39
+ */
40
+ this.id = id;
41
+
42
+ /**
43
+ * An array of the next segments.
44
+ * @type {CodePathSegment[]}
45
+ */
46
+ this.nextSegments = [];
47
+
48
+ /**
49
+ * An array of the previous segments.
50
+ * @type {CodePathSegment[]}
51
+ */
52
+ this.prevSegments = allPrevSegments.filter(isReachable);
53
+
54
+ /**
55
+ * An array of the next segments.
56
+ * This array includes unreachable segments.
57
+ * @type {CodePathSegment[]}
58
+ */
59
+ this.allNextSegments = [];
60
+
61
+ /**
62
+ * An array of the previous segments.
63
+ * This array includes unreachable segments.
64
+ * @type {CodePathSegment[]}
65
+ */
66
+ this.allPrevSegments = allPrevSegments;
67
+
68
+ /**
69
+ * A flag which shows this is reachable.
70
+ * @type {boolean}
71
+ */
72
+ this.reachable = reachable;
73
+
74
+ // Internal data.
75
+ Object.defineProperty(this, 'internal', {
76
+ value: {
77
+ used: false,
78
+ loopedPrevSegments: [],
79
+ },
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Checks a given previous segment is coming from the end of a loop.
85
+ * @param {CodePathSegment} segment A previous segment to check.
86
+ * @returns {boolean} `true` if the segment is coming from the end of a loop.
87
+ */
88
+ isLoopedPrevSegment(segment) {
89
+ return this.internal.loopedPrevSegments.includes(segment);
90
+ }
91
+
92
+ /**
93
+ * Creates the root segment.
94
+ * @param {string} id An identifier.
95
+ * @returns {CodePathSegment} The created segment.
96
+ */
97
+ static newRoot(id) {
98
+ return new CodePathSegment(id, [], true);
99
+ }
100
+
101
+ /**
102
+ * Creates a segment that follows given segments.
103
+ * @param {string} id An identifier.
104
+ * @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
105
+ * @returns {CodePathSegment} The created segment.
106
+ */
107
+ static newNext(id, allPrevSegments) {
108
+ return new CodePathSegment(
109
+ id,
110
+ CodePathSegment.flattenUnusedSegments(allPrevSegments),
111
+ allPrevSegments.some(isReachable),
112
+ );
113
+ }
114
+
115
+ /**
116
+ * Creates an unreachable segment that follows given segments.
117
+ * @param {string} id An identifier.
118
+ * @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
119
+ * @returns {CodePathSegment} The created segment.
120
+ */
121
+ static newUnreachable(id, allPrevSegments) {
122
+ const segment = new CodePathSegment(
123
+ id,
124
+ CodePathSegment.flattenUnusedSegments(allPrevSegments),
125
+ false,
126
+ );
127
+
128
+ /*
129
+ * In `if (a) return a; foo();` case, the unreachable segment preceded by
130
+ * the return statement is not used but must not be remove.
131
+ */
132
+ CodePathSegment.markUsed(segment);
133
+
134
+ return segment;
135
+ }
136
+
137
+ /**
138
+ * Creates a segment that follows given segments.
139
+ * This factory method does not connect with `allPrevSegments`.
140
+ * But this inherits `reachable` flag.
141
+ * @param {string} id An identifier.
142
+ * @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
143
+ * @returns {CodePathSegment} The created segment.
144
+ */
145
+ static newDisconnected(id, allPrevSegments) {
146
+ return new CodePathSegment(id, [], allPrevSegments.some(isReachable));
147
+ }
148
+
149
+ /**
150
+ * Makes a given segment being used.
151
+ *
152
+ * And this function registers the segment into the previous segments as a next.
153
+ * @param {CodePathSegment} segment A segment to mark.
154
+ * @returns {void}
155
+ */
156
+ static markUsed(segment) {
157
+ if (segment.internal.used) {
158
+ return;
159
+ }
160
+ segment.internal.used = true;
161
+
162
+ let i;
163
+
164
+ if (segment.reachable) {
165
+ for (i = 0; i < segment.allPrevSegments.length; ++i) {
166
+ const prevSegment = segment.allPrevSegments[i];
167
+
168
+ prevSegment.allNextSegments.push(segment);
169
+ prevSegment.nextSegments.push(segment);
170
+ }
171
+ } else {
172
+ for (i = 0; i < segment.allPrevSegments.length; ++i) {
173
+ segment.allPrevSegments[i].allNextSegments.push(segment);
174
+ }
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Marks a previous segment as looped.
180
+ * @param {CodePathSegment} segment A segment.
181
+ * @param {CodePathSegment} prevSegment A previous segment to mark.
182
+ * @returns {void}
183
+ */
184
+ static markPrevSegmentAsLooped(segment, prevSegment) {
185
+ segment.internal.loopedPrevSegments.push(prevSegment);
186
+ }
187
+
188
+ /**
189
+ * Replaces unused segments with the previous segments of each unused segment.
190
+ * @param {CodePathSegment[]} segments An array of segments to replace.
191
+ * @returns {CodePathSegment[]} The replaced array.
192
+ */
193
+ static flattenUnusedSegments(segments) {
194
+ const done = Object.create(null);
195
+ const retv = [];
196
+
197
+ for (let i = 0; i < segments.length; ++i) {
198
+ const segment = segments[i];
199
+
200
+ // Ignores duplicated.
201
+ if (done[segment.id]) {
202
+ continue;
203
+ }
204
+
205
+ // Use previous segments if unused.
206
+ if (!segment.internal.used) {
207
+ for (let j = 0; j < segment.allPrevSegments.length; ++j) {
208
+ const prevSegment = segment.allPrevSegments[j];
209
+
210
+ if (!done[prevSegment.id]) {
211
+ done[prevSegment.id] = true;
212
+ retv.push(prevSegment);
213
+ }
214
+ }
215
+ } else {
216
+ done[segment.id] = true;
217
+ retv.push(segment);
218
+ }
219
+ }
220
+
221
+ return retv;
222
+ }
223
+}
224
+
225
+module.exports = CodePathSegment;
packages/eslint-plugin-react-hooks/src/code-path-analysis/code-path-state.js
new
+1441
@@ -0,0 +1,1441 @@
1
+'use strict';
2
+
3
+//------------------------------------------------------------------------------
4
+// Requirements
5
+//------------------------------------------------------------------------------
6
+
7
+// eslint-disable-next-line
8
+const CodePathSegment = require('./code-path-segment');
9
+// eslint-disable-next-line
10
+const ForkContext = require('./fork-context');
11
+
12
+//------------------------------------------------------------------------------
13
+// Helpers
14
+//------------------------------------------------------------------------------
15
+
16
+/**
17
+ * Adds given segments into the `dest` array.
18
+ * If the `others` array does not includes the given segments, adds to the `all`
19
+ * array as well.
20
+ *
21
+ * This adds only reachable and used segments.
22
+ * @param {CodePathSegment[]} dest A destination array (`returnedSegments` or `thrownSegments`).
23
+ * @param {CodePathSegment[]} others Another destination array (`returnedSegments` or `thrownSegments`).
24
+ * @param {CodePathSegment[]} all The unified destination array (`finalSegments`).
25
+ * @param {CodePathSegment[]} segments Segments to add.
26
+ * @returns {void}
27
+ */
28
+function addToReturnedOrThrown(dest, others, all, segments) {
29
+ for (let i = 0; i < segments.length; ++i) {
30
+ const segment = segments[i];
31
+
32
+ dest.push(segment);
33
+ if (!others.includes(segment)) {
34
+ all.push(segment);
35
+ }
36
+ }
37
+}
38
+
39
+/**
40
+ * Gets a loop-context for a `continue` statement.
41
+ * @param {CodePathState} state A state to get.
42
+ * @param {string} label The label of a `continue` statement.
43
+ * @returns {LoopContext} A loop-context for a `continue` statement.
44
+ */
45
+function getContinueContext(state, label) {
46
+ if (!label) {
47
+ return state.loopContext;
48
+ }
49
+
50
+ let context = state.loopContext;
51
+
52
+ while (context) {
53
+ if (context.label === label) {
54
+ return context;
55
+ }
56
+ context = context.upper;
57
+ }
58
+
59
+ /* c8 ignore next */
60
+ return null;
61
+}
62
+
63
+/**
64
+ * Gets a context for a `break` statement.
65
+ * @param {CodePathState} state A state to get.
66
+ * @param {string} label The label of a `break` statement.
67
+ * @returns {LoopContext|SwitchContext} A context for a `break` statement.
68
+ */
69
+function getBreakContext(state, label) {
70
+ let context = state.breakContext;
71
+
72
+ while (context) {
73
+ if (label ? context.label === label : context.breakable) {
74
+ return context;
75
+ }
76
+ context = context.upper;
77
+ }
78
+
79
+ /* c8 ignore next */
80
+ return null;
81
+}
82
+
83
+/**
84
+ * Gets a context for a `return` statement.
85
+ * @param {CodePathState} state A state to get.
86
+ * @returns {TryContext|CodePathState} A context for a `return` statement.
87
+ */
88
+function getReturnContext(state) {
89
+ let context = state.tryContext;
90
+
91
+ while (context) {
92
+ if (context.hasFinalizer && context.position !== 'finally') {
93
+ return context;
94
+ }
95
+ context = context.upper;
96
+ }
97
+
98
+ return state;
99
+}
100
+
101
+/**
102
+ * Gets a context for a `throw` statement.
103
+ * @param {CodePathState} state A state to get.
104
+ * @returns {TryContext|CodePathState} A context for a `throw` statement.
105
+ */
106
+function getThrowContext(state) {
107
+ let context = state.tryContext;
108
+
109
+ while (context) {
110
+ if (
111
+ context.position === 'try' ||
112
+ (context.hasFinalizer && context.position === 'catch')
113
+ ) {
114
+ return context;
115
+ }
116
+ context = context.upper;
117
+ }
118
+
119
+ return state;
120
+}
121
+
122
+/**
123
+ * Removes a given element from a given array.
124
+ * @param {any[]} xs An array to remove the specific element.
125
+ * @param {any} x An element to be removed.
126
+ * @returns {void}
127
+ */
128
+function remove(xs, x) {
129
+ xs.splice(xs.indexOf(x), 1);
130
+}
131
+
132
+/**
133
+ * Disconnect given segments.
134
+ *
135
+ * This is used in a process for switch statements.
136
+ * If there is the "default" chunk before other cases, the order is different
137
+ * between node's and running's.
138
+ * @param {CodePathSegment[]} prevSegments Forward segments to disconnect.
139
+ * @param {CodePathSegment[]} nextSegments Backward segments to disconnect.
140
+ * @returns {void}
141
+ */
142
+function removeConnection(prevSegments, nextSegments) {
143
+ for (let i = 0; i < prevSegments.length; ++i) {
144
+ const prevSegment = prevSegments[i];
145
+ const nextSegment = nextSegments[i];
146
+
147
+ remove(prevSegment.nextSegments, nextSegment);
148
+ remove(prevSegment.allNextSegments, nextSegment);
149
+ remove(nextSegment.prevSegments, prevSegment);
150
+ remove(nextSegment.allPrevSegments, prevSegment);
151
+ }
152
+}
153
+
154
+/**
155
+ * Creates looping path.
156
+ * @param {CodePathState} state The instance.
157
+ * @param {CodePathSegment[]} unflattenedFromSegments Segments which are source.
158
+ * @param {CodePathSegment[]} unflattenedToSegments Segments which are destination.
159
+ * @returns {void}
160
+ */
161
+function makeLooped(state, unflattenedFromSegments, unflattenedToSegments) {
162
+ const fromSegments = CodePathSegment.flattenUnusedSegments(
163
+ unflattenedFromSegments,
164
+ );
165
+ const toSegments = CodePathSegment.flattenUnusedSegments(
166
+ unflattenedToSegments,
167
+ );
168
+
169
+ const end = Math.min(fromSegments.length, toSegments.length);
170
+
171
+ for (let i = 0; i < end; ++i) {
172
+ const fromSegment = fromSegments[i];
173
+ const toSegment = toSegments[i];
174
+
175
+ if (toSegment.reachable) {
176
+ fromSegment.nextSegments.push(toSegment);
177
+ }
178
+ if (fromSegment.reachable) {
179
+ toSegment.prevSegments.push(fromSegment);
180
+ }
181
+ fromSegment.allNextSegments.push(toSegment);
182
+ toSegment.allPrevSegments.push(fromSegment);
183
+
184
+ if (toSegment.allPrevSegments.length >= 2) {
185
+ CodePathSegment.markPrevSegmentAsLooped(toSegment, fromSegment);
186
+ }
187
+
188
+ state.notifyLooped(fromSegment, toSegment);
189
+ }
190
+}
191
+
192
+/**
193
+ * Finalizes segments of `test` chunk of a ForStatement.
194
+ *
195
+ * - Adds `false` paths to paths which are leaving from the loop.
196
+ * - Sets `true` paths to paths which go to the body.
197
+ * @param {LoopContext} context A loop context to modify.
198
+ * @param {ChoiceContext} choiceContext A choice context of this loop.
199
+ * @param {CodePathSegment[]} head The current head paths.
200
+ * @returns {void}
201
+ */
202
+function finalizeTestSegmentsOfFor(context, choiceContext, head) {
203
+ if (!choiceContext.processed) {
204
+ choiceContext.trueForkContext.add(head);
205
+ choiceContext.falseForkContext.add(head);
206
+ choiceContext.qqForkContext.add(head);
207
+ }
208
+
209
+ if (context.test !== true) {
210
+ context.brokenForkContext.addAll(choiceContext.falseForkContext);
211
+ }
212
+ context.endOfTestSegments = choiceContext.trueForkContext.makeNext(0, -1);
213
+}
214
+
215
+//------------------------------------------------------------------------------
216
+// Public Interface
217
+//------------------------------------------------------------------------------
218
+
219
+/**
220
+ * A class which manages state to analyze code paths.
221
+ */
222
+class CodePathState {
223
+ /**
224
+ * @param {IdGenerator} idGenerator An id generator to generate id for code
225
+ * path segments.
226
+ * @param {Function} onLooped A callback function to notify looping.
227
+ */
228
+ constructor(idGenerator, onLooped) {
229
+ this.idGenerator = idGenerator;
230
+ this.notifyLooped = onLooped;
231
+ this.forkContext = ForkContext.newRoot(idGenerator);
232
+ this.choiceContext = null;
233
+ this.switchContext = null;
234
+ this.tryContext = null;
235
+ this.loopContext = null;
236
+ this.breakContext = null;
237
+ this.chainContext = null;
238
+
239
+ this.currentSegments = [];
240
+ this.initialSegment = this.forkContext.head[0];
241
+
242
+ // returnedSegments and thrownSegments push elements into finalSegments also.
243
+ const final = (this.finalSegments = []);
244
+ const returned = (this.returnedForkContext = []);
245
+ const thrown = (this.thrownForkContext = []);
246
+
247
+ returned.add = addToReturnedOrThrown.bind(null, returned, thrown, final);
248
+ thrown.add = addToReturnedOrThrown.bind(null, thrown, returned, final);
249
+ }
250
+
251
+ /**
252
+ * The head segments.
253
+ * @type {CodePathSegment[]}
254
+ */
255
+ get headSegments() {
256
+ return this.forkContext.head;
257
+ }
258
+
259
+ /**
260
+ * The parent forking context.
261
+ * This is used for the root of new forks.
262
+ * @type {ForkContext}
263
+ */
264
+ get parentForkContext() {
265
+ const current = this.forkContext;
266
+
267
+ return current && current.upper;
268
+ }
269
+
270
+ /**
271
+ * Creates and stacks new forking context.
272
+ * @param {boolean} forkLeavingPath A flag which shows being in a
273
+ * "finally" block.
274
+ * @returns {ForkContext} The created context.
275
+ */
276
+ pushForkContext(forkLeavingPath) {
277
+ this.forkContext = ForkContext.newEmpty(this.forkContext, forkLeavingPath);
278
+
279
+ return this.forkContext;
280
+ }
281
+
282
+ /**
283
+ * Pops and merges the last forking context.
284
+ * @returns {ForkContext} The last context.
285
+ */
286
+ popForkContext() {
287
+ const lastContext = this.forkContext;
288
+
289
+ this.forkContext = lastContext.upper;
290
+ this.forkContext.replaceHead(lastContext.makeNext(0, -1));
291
+
292
+ return lastContext;
293
+ }
294
+
295
+ /**
296
+ * Creates a new path.
297
+ * @returns {void}
298
+ */
299
+ forkPath() {
300
+ this.forkContext.add(this.parentForkContext.makeNext(-1, -1));
301
+ }
302
+
303
+ /**
304
+ * Creates a bypass path.
305
+ * This is used for such as IfStatement which does not have "else" chunk.
306
+ * @returns {void}
307
+ */
308
+ forkBypassPath() {
309
+ this.forkContext.add(this.parentForkContext.head);
310
+ }
311
+
312
+ //--------------------------------------------------------------------------
313
+ // ConditionalExpression, LogicalExpression, IfStatement
314
+ //--------------------------------------------------------------------------
315
+
316
+ /**
317
+ * Creates a context for ConditionalExpression, LogicalExpression, AssignmentExpression (logical assignments only),
318
+ * IfStatement, WhileStatement, DoWhileStatement, or ForStatement.
319
+ *
320
+ * LogicalExpressions have cases that it goes different paths between the
321
+ * `true` case and the `false` case.
322
+ *
323
+ * For Example:
324
+ *
325
+ * if (a || b) {
326
+ * foo();
327
+ * } else {
328
+ * bar();
329
+ * }
330
+ *
331
+ * In this case, `b` is evaluated always in the code path of the `else`
332
+ * block, but it's not so in the code path of the `if` block.
333
+ * So there are 3 paths.
334
+ *
335
+ * a -> foo();
336
+ * a -> b -> foo();
337
+ * a -> b -> bar();
338
+ * @param {string} kind A kind string.
339
+ * If the new context is LogicalExpression's or AssignmentExpression's, this is `"&&"` or `"||"` or `"??"`.
340
+ * If it's IfStatement's or ConditionalExpression's, this is `"test"`.
341
+ * Otherwise, this is `"loop"`.
342
+ * @param {boolean} isForkingAsResult A flag that shows that goes different
343
+ * paths between `true` and `false`.
344
+ * @returns {void}
345
+ */
346
+ pushChoiceContext(kind, isForkingAsResult) {
347
+ this.choiceContext = {
348
+ upper: this.choiceContext,
349
+ kind,
350
+ isForkingAsResult,
351
+ trueForkContext: ForkContext.newEmpty(this.forkContext),
352
+ falseForkContext: ForkContext.newEmpty(this.forkContext),
353
+ qqForkContext: ForkContext.newEmpty(this.forkContext),
354
+ processed: false,
355
+ };
356
+ }
357
+
358
+ /**
359
+ * Pops the last choice context and finalizes it.
360
+ * @throws {Error} (Unreachable.)
361
+ * @returns {ChoiceContext} The popped context.
362
+ */
363
+ popChoiceContext() {
364
+ const context = this.choiceContext;
365
+
366
+ this.choiceContext = context.upper;
367
+
368
+ const forkContext = this.forkContext;
369
+ const headSegments = forkContext.head;
370
+
371
+ switch (context.kind) {
372
+ case '&&':
373
+ case '||':
374
+ case '??':
375
+ /*
376
+ * If any result were not transferred from child contexts,
377
+ * this sets the head segments to both cases.
378
+ * The head segments are the path of the right-hand operand.
379
+ */
380
+ if (!context.processed) {
381
+ context.trueForkContext.add(headSegments);
382
+ context.falseForkContext.add(headSegments);
383
+ context.qqForkContext.add(headSegments);
384
+ }
385
+
386
+ /*
387
+ * Transfers results to upper context if this context is in
388
+ * test chunk.
389
+ */
390
+ if (context.isForkingAsResult) {
391
+ const parentContext = this.choiceContext;
392
+
393
+ parentContext.trueForkContext.addAll(context.trueForkContext);
394
+ parentContext.falseForkContext.addAll(context.falseForkContext);
395
+ parentContext.qqForkContext.addAll(context.qqForkContext);
396
+ parentContext.processed = true;
397
+
398
+ return context;
399
+ }
400
+
401
+ break;
402
+
403
+ case 'test':
404
+ if (!context.processed) {
405
+ /*
406
+ * The head segments are the path of the `if` block here.
407
+ * Updates the `true` path with the end of the `if` block.
408
+ */
409
+ context.trueForkContext.clear();
410
+ context.trueForkContext.add(headSegments);
411
+ } else {
412
+ /*
413
+ * The head segments are the path of the `else` block here.
414
+ * Updates the `false` path with the end of the `else`
415
+ * block.
416
+ */
417
+ context.falseForkContext.clear();
418
+ context.falseForkContext.add(headSegments);
419
+ }
420
+
421
+ break;
422
+
423
+ case 'loop':
424
+ /*
425
+ * Loops are addressed in popLoopContext().
426
+ * This is called from popLoopContext().
427
+ */
428
+ return context;
429
+
430
+ /* c8 ignore next */
431
+ default:
432
+ throw new Error('unreachable');
433
+ }
434
+
435
+ // Merges all paths.
436
+ const prevForkContext = context.trueForkContext;
437
+
438
+ prevForkContext.addAll(context.falseForkContext);
439
+ forkContext.replaceHead(prevForkContext.makeNext(0, -1));
440
+
441
+ return context;
442
+ }
443
+
444
+ /**
445
+ * Makes a code path segment of the right-hand operand of a logical
446
+ * expression.
447
+ * @throws {Error} (Unreachable.)
448
+ * @returns {void}
449
+ */
450
+ makeLogicalRight() {
451
+ const context = this.choiceContext;
452
+ const forkContext = this.forkContext;
453
+
454
+ if (context.processed) {
455
+ /*
456
+ * This got segments already from the child choice context.
457
+ * Creates the next path from own true/false fork context.
458
+ */
459
+ let prevForkContext;
460
+
461
+ switch (context.kind) {
462
+ case '&&': // if true then go to the right-hand side.
463
+ prevForkContext = context.trueForkContext;
464
+ break;
465
+ case '||': // if false then go to the right-hand side.
466
+ prevForkContext = context.falseForkContext;
467
+ break;
468
+ case '??': // Both true/false can short-circuit, so needs the third path to go to the right-hand side. That's qqForkContext.
469
+ prevForkContext = context.qqForkContext;
470
+ break;
471
+ default:
472
+ throw new Error('unreachable');
473
+ }
474
+
475
+ forkContext.replaceHead(prevForkContext.makeNext(0, -1));
476
+ prevForkContext.clear();
477
+ context.processed = false;
478
+ } else {
479
+ /*
480
+ * This did not get segments from the child choice context.
481
+ * So addresses the head segments.
482
+ * The head segments are the path of the left-hand operand.
483
+ */
484
+ switch (context.kind) {
485
+ case '&&': // the false path can short-circuit.
486
+ context.falseForkContext.add(forkContext.head);
487
+ break;
488
+ case '||': // the true path can short-circuit.
489
+ context.trueForkContext.add(forkContext.head);
490
+ break;
491
+ case '??': // both can short-circuit.
492
+ context.trueForkContext.add(forkContext.head);
493
+ context.falseForkContext.add(forkContext.head);
494
+ break;
495
+ default:
496
+ throw new Error('unreachable');
497
+ }
498
+
499
+ forkContext.replaceHead(forkContext.makeNext(-1, -1));
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Makes a code path segment of the `if` block.
505
+ * @returns {void}
506
+ */
507
+ makeIfConsequent() {
508
+ const context = this.choiceContext;
509
+ const forkContext = this.forkContext;
510
+
511
+ /*
512
+ * If any result were not transferred from child contexts,
513
+ * this sets the head segments to both cases.
514
+ * The head segments are the path of the test expression.
515
+ */
516
+ if (!context.processed) {
517
+ context.trueForkContext.add(forkContext.head);
518
+ context.falseForkContext.add(forkContext.head);
519
+ context.qqForkContext.add(forkContext.head);
520
+ }
521
+
522
+ context.processed = false;
523
+
524
+ // Creates new path from the `true` case.
525
+ forkContext.replaceHead(context.trueForkContext.makeNext(0, -1));
526
+ }
527
+
528
+ /**
529
+ * Makes a code path segment of the `else` block.
530
+ * @returns {void}
531
+ */
532
+ makeIfAlternate() {
533
+ const context = this.choiceContext;
534
+ const forkContext = this.forkContext;
535
+
536
+ /*
537
+ * The head segments are the path of the `if` block.
538
+ * Updates the `true` path with the end of the `if` block.
539
+ */
540
+ context.trueForkContext.clear();
541
+ context.trueForkContext.add(forkContext.head);
542
+ context.processed = true;
543
+
544
+ // Creates new path from the `false` case.
545
+ forkContext.replaceHead(context.falseForkContext.makeNext(0, -1));
546
+ }
547
+
548
+ //--------------------------------------------------------------------------
549
+ // ChainExpression
550
+ //--------------------------------------------------------------------------
551
+
552
+ /**
553
+ * Push a new `ChainExpression` context to the stack.
554
+ * This method is called on entering to each `ChainExpression` node.
555
+ * This context is used to count forking in the optional chain then merge them on the exiting from the `ChainExpression` node.
556
+ * @returns {void}
557
+ */
558
+ pushChainContext() {
559
+ this.chainContext = {
560
+ upper: this.chainContext,
561
+ countChoiceContexts: 0,
562
+ };
563
+ }
564
+
565
+ /**
566
+ * Pop a `ChainExpression` context from the stack.
567
+ * This method is called on exiting from each `ChainExpression` node.
568
+ * This merges all forks of the last optional chaining.
569
+ * @returns {void}
570
+ */
571
+ popChainContext() {
572
+ const context = this.chainContext;
573
+
574
+ this.chainContext = context.upper;
575
+
576
+ // pop all choice contexts of this.
577
+ for (let i = context.countChoiceContexts; i > 0; --i) {
578
+ this.popChoiceContext();
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Create a choice context for optional access.
584
+ * This method is called on entering to each `(Call|Member)Expression[optional=true]` node.
585
+ * This creates a choice context as similar to `LogicalExpression[operator="??"]` node.
586
+ * @returns {void}
587
+ */
588
+ makeOptionalNode() {
589
+ if (this.chainContext) {
590
+ this.chainContext.countChoiceContexts += 1;
591
+ this.pushChoiceContext('??', false);
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Create a fork.
597
+ * This method is called on entering to the `arguments|property` property of each `(Call|Member)Expression` node.
598
+ * @returns {void}
599
+ */
600
+ makeOptionalRight() {
601
+ if (this.chainContext) {
602
+ this.makeLogicalRight();
603
+ }
604
+ }
605
+
606
+ //--------------------------------------------------------------------------
607
+ // SwitchStatement
608
+ //--------------------------------------------------------------------------
609
+
610
+ /**
611
+ * Creates a context object of SwitchStatement and stacks it.
612
+ * @param {boolean} hasCase `true` if the switch statement has one or more
613
+ * case parts.
614
+ * @param {string|null} label The label text.
615
+ * @returns {void}
616
+ */
617
+ pushSwitchContext(hasCase, label) {
618
+ this.switchContext = {
619
+ upper: this.switchContext,
620
+ hasCase,
621
+ defaultSegments: null,
622
+ defaultBodySegments: null,
623
+ foundDefault: false,
624
+ lastIsDefault: false,
625
+ countForks: 0,
626
+ };
627
+
628
+ this.pushBreakContext(true, label);
629
+ }
630
+
631
+ /**
632
+ * Pops the last context of SwitchStatement and finalizes it.
633
+ *
634
+ * - Disposes all forking stack for `case` and `default`.
635
+ * - Creates the next code path segment from `context.brokenForkContext`.
636
+ * - If the last `SwitchCase` node is not a `default` part, creates a path
637
+ * to the `default` body.
638
+ * @returns {void}
639
+ */
640
+ popSwitchContext() {
641
+ const context = this.switchContext;
642
+
643
+ this.switchContext = context.upper;
644
+
645
+ const forkContext = this.forkContext;
646
+ const brokenForkContext = this.popBreakContext().brokenForkContext;
647
+
648
+ if (context.countForks === 0) {
649
+ /*
650
+ * When there is only one `default` chunk and there is one or more
651
+ * `break` statements, even if forks are nothing, it needs to merge
652
+ * those.
653
+ */
654
+ if (!brokenForkContext.empty) {
655
+ brokenForkContext.add(forkContext.makeNext(-1, -1));
656
+ forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
657
+ }
658
+
659
+ return;
660
+ }
661
+
662
+ const lastSegments = forkContext.head;
663
+
664
+ this.forkBypassPath();
665
+ const lastCaseSegments = forkContext.head;
666
+
667
+ /*
668
+ * `brokenForkContext` is used to make the next segment.
669
+ * It must add the last segment into `brokenForkContext`.
670
+ */
671
+ brokenForkContext.add(lastSegments);
672
+
673
+ /*
674
+ * A path which is failed in all case test should be connected to path
675
+ * of `default` chunk.
676
+ */
677
+ if (!context.lastIsDefault) {
678
+ if (context.defaultBodySegments) {
679
+ /*
680
+ * Remove a link from `default` label to its chunk.
681
+ * It's false route.
682
+ */
683
+ removeConnection(context.defaultSegments, context.defaultBodySegments);
684
+ makeLooped(this, lastCaseSegments, context.defaultBodySegments);
685
+ } else {
686
+ /*
687
+ * It handles the last case body as broken if `default` chunk
688
+ * does not exist.
689
+ */
690
+ brokenForkContext.add(lastCaseSegments);
691
+ }
692
+ }
693
+
694
+ // Pops the segment context stack until the entry segment.
695
+ for (let i = 0; i < context.countForks; ++i) {
696
+ this.forkContext = this.forkContext.upper;
697
+ }
698
+
699
+ /*
700
+ * Creates a path from all brokenForkContext paths.
701
+ * This is a path after switch statement.
702
+ */
703
+ this.forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
704
+ }
705
+
706
+ /**
707
+ * Makes a code path segment for a `SwitchCase` node.
708
+ * @param {boolean} isEmpty `true` if the body is empty.
709
+ * @param {boolean} isDefault `true` if the body is the default case.
710
+ * @returns {void}
711
+ */
712
+ makeSwitchCaseBody(isEmpty, isDefault) {
713
+ const context = this.switchContext;
714
+
715
+ if (!context.hasCase) {
716
+ return;
717
+ }
718
+
719
+ /*
720
+ * Merge forks.
721
+ * The parent fork context has two segments.
722
+ * Those are from the current case and the body of the previous case.
723
+ */
724
+ const parentForkContext = this.forkContext;
725
+ const forkContext = this.pushForkContext();
726
+
727
+ forkContext.add(parentForkContext.makeNext(0, -1));
728
+
729
+ /*
730
+ * Save `default` chunk info.
731
+ * If the `default` label is not at the last, we must make a path from
732
+ * the last `case` to the `default` chunk.
733
+ */
734
+ if (isDefault) {
735
+ context.defaultSegments = parentForkContext.head;
736
+ if (isEmpty) {
737
+ context.foundDefault = true;
738
+ } else {
739
+ context.defaultBodySegments = forkContext.head;
740
+ }
741
+ } else {
742
+ if (!isEmpty && context.foundDefault) {
743
+ context.foundDefault = false;
744
+ context.defaultBodySegments = forkContext.head;
745
+ }
746
+ }
747
+
748
+ context.lastIsDefault = isDefault;
749
+ context.countForks += 1;
750
+ }
751
+
752
+ //--------------------------------------------------------------------------
753
+ // TryStatement
754
+ //--------------------------------------------------------------------------
755
+
756
+ /**
757
+ * Creates a context object of TryStatement and stacks it.
758
+ * @param {boolean} hasFinalizer `true` if the try statement has a
759
+ * `finally` block.
760
+ * @returns {void}
761
+ */
762
+ pushTryContext(hasFinalizer) {
763
+ this.tryContext = {
764
+ upper: this.tryContext,
765
+ position: 'try',
766
+ hasFinalizer,
767
+
768
+ returnedForkContext: hasFinalizer
769
+ ? ForkContext.newEmpty(this.forkContext)
770
+ : null,
771
+
772
+ thrownForkContext: ForkContext.newEmpty(this.forkContext),
773
+ lastOfTryIsReachable: false,
774
+ lastOfCatchIsReachable: false,
775
+ };
776
+ }
777
+
778
+ /**
779
+ * Pops the last context of TryStatement and finalizes it.
780
+ * @returns {void}
781
+ */
782
+ popTryContext() {
783
+ const context = this.tryContext;
784
+
785
+ this.tryContext = context.upper;
786
+
787
+ if (context.position === 'catch') {
788
+ // Merges two paths from the `try` block and `catch` block merely.
789
+ this.popForkContext();
790
+ return;
791
+ }
792
+
793
+ /*
794
+ * The following process is executed only when there is the `finally`
795
+ * block.
796
+ */
797
+
798
+ const returned = context.returnedForkContext;
799
+ const thrown = context.thrownForkContext;
800
+
801
+ if (returned.empty && thrown.empty) {
802
+ return;
803
+ }
804
+
805
+ // Separate head to normal paths and leaving paths.
806
+ const headSegments = this.forkContext.head;
807
+
808
+ this.forkContext = this.forkContext.upper;
809
+ const normalSegments = headSegments.slice(0, (headSegments.length / 2) | 0);
810
+ const leavingSegments = headSegments.slice((headSegments.length / 2) | 0);
811
+
812
+ // Forwards the leaving path to upper contexts.
813
+ if (!returned.empty) {
814
+ getReturnContext(this).returnedForkContext.add(leavingSegments);
815
+ }
816
+ if (!thrown.empty) {
817
+ getThrowContext(this).thrownForkContext.add(leavingSegments);
818
+ }
819
+
820
+ // Sets the normal path as the next.
821
+ this.forkContext.replaceHead(normalSegments);
822
+
823
+ /*
824
+ * If both paths of the `try` block and the `catch` block are
825
+ * unreachable, the next path becomes unreachable as well.
826
+ */
827
+ if (!context.lastOfTryIsReachable && !context.lastOfCatchIsReachable) {
828
+ this.forkContext.makeUnreachable();
829
+ }
830
+ }
831
+
832
+ /**
833
+ * Makes a code path segment for a `catch` block.
834
+ * @returns {void}
835
+ */
836
+ makeCatchBlock() {
837
+ const context = this.tryContext;
838
+ const forkContext = this.forkContext;
839
+ const thrown = context.thrownForkContext;
840
+
841
+ // Update state.
842
+ context.position = 'catch';
843
+ context.thrownForkContext = ForkContext.newEmpty(forkContext);
844
+ context.lastOfTryIsReachable = forkContext.reachable;
845
+
846
+ // Merge thrown paths.
847
+ thrown.add(forkContext.head);
848
+ const thrownSegments = thrown.makeNext(0, -1);
849
+
850
+ // Fork to a bypass and the merged thrown path.
851
+ this.pushForkContext();
852
+ this.forkBypassPath();
853
+ this.forkContext.add(thrownSegments);
854
+ }
855
+
856
+ /**
857
+ * Makes a code path segment for a `finally` block.
858
+ *
859
+ * In the `finally` block, parallel paths are created. The parallel paths
860
+ * are used as leaving-paths. The leaving-paths are paths from `return`
861
+ * statements and `throw` statements in a `try` block or a `catch` block.
862
+ * @returns {void}
863
+ */
864
+ makeFinallyBlock() {
865
+ const context = this.tryContext;
866
+ let forkContext = this.forkContext;
867
+ const returned = context.returnedForkContext;
868
+ const thrown = context.thrownForkContext;
869
+ const headOfLeavingSegments = forkContext.head;
870
+
871
+ // Update state.
872
+ if (context.position === 'catch') {
873
+ // Merges two paths from the `try` block and `catch` block.
874
+ this.popForkContext();
875
+ forkContext = this.forkContext;
876
+
877
+ context.lastOfCatchIsReachable = forkContext.reachable;
878
+ } else {
879
+ context.lastOfTryIsReachable = forkContext.reachable;
880
+ }
881
+ context.position = 'finally';
882
+
883
+ if (returned.empty && thrown.empty) {
884
+ // This path does not leave.
885
+ return;
886
+ }
887
+
888
+ /*
889
+ * Create a parallel segment from merging returned and thrown.
890
+ * This segment will leave at the end of this finally block.
891
+ */
892
+ const segments = forkContext.makeNext(-1, -1);
893
+
894
+ for (let i = 0; i < forkContext.count; ++i) {
895
+ const prevSegsOfLeavingSegment = [headOfLeavingSegments[i]];
896
+
897
+ for (let j = 0; j < returned.segmentsList.length; ++j) {
898
+ prevSegsOfLeavingSegment.push(returned.segmentsList[j][i]);
899
+ }
900
+ for (let j = 0; j < thrown.segmentsList.length; ++j) {
901
+ prevSegsOfLeavingSegment.push(thrown.segmentsList[j][i]);
902
+ }
903
+
904
+ segments.push(
905
+ CodePathSegment.newNext(
906
+ this.idGenerator.next(),
907
+ prevSegsOfLeavingSegment,
908
+ ),
909
+ );
910
+ }
911
+
912
+ this.pushForkContext(true);
913
+ this.forkContext.add(segments);
914
+ }
915
+
916
+ /**
917
+ * Makes a code path segment from the first throwable node to the `catch`
918
+ * block or the `finally` block.
919
+ * @returns {void}
920
+ */
921
+ makeFirstThrowablePathInTryBlock() {
922
+ const forkContext = this.forkContext;
923
+
924
+ if (!forkContext.reachable) {
925
+ return;
926
+ }
927
+
928
+ const context = getThrowContext(this);
929
+
930
+ if (
931
+ context === this ||
932
+ context.position !== 'try' ||
933
+ !context.thrownForkContext.empty
934
+ ) {
935
+ return;
936
+ }
937
+
938
+ context.thrownForkContext.add(forkContext.head);
939
+ forkContext.replaceHead(forkContext.makeNext(-1, -1));
940
+ }
941
+
942
+ //--------------------------------------------------------------------------
943
+ // Loop Statements
944
+ //--------------------------------------------------------------------------
945
+
946
+ /**
947
+ * Creates a context object of a loop statement and stacks it.
948
+ * @param {string} type The type of the node which was triggered. One of
949
+ * `WhileStatement`, `DoWhileStatement`, `ForStatement`, `ForInStatement`,
950
+ * and `ForStatement`.
951
+ * @param {string|null} label A label of the node which was triggered.
952
+ * @throws {Error} (Unreachable - unknown type.)
953
+ * @returns {void}
954
+ */
955
+ pushLoopContext(type, label) {
956
+ const forkContext = this.forkContext;
957
+ const breakContext = this.pushBreakContext(true, label);
958
+
959
+ switch (type) {
960
+ case 'WhileStatement':
961
+ this.pushChoiceContext('loop', false);
962
+ this.loopContext = {
963
+ upper: this.loopContext,
964
+ type,
965
+ label,
966
+ test: void 0,
967
+ continueDestSegments: null,
968
+ brokenForkContext: breakContext.brokenForkContext,
969
+ };
970
+ break;
971
+
972
+ case 'DoWhileStatement':
973
+ this.pushChoiceContext('loop', false);
974
+ this.loopContext = {
975
+ upper: this.loopContext,
976
+ type,
977
+ label,
978
+ test: void 0,
979
+ entrySegments: null,
980
+ continueForkContext: ForkContext.newEmpty(forkContext),
981
+ brokenForkContext: breakContext.brokenForkContext,
982
+ };
983
+ break;
984
+
985
+ case 'ForStatement':
986
+ this.pushChoiceContext('loop', false);
987
+ this.loopContext = {
988
+ upper: this.loopContext,
989
+ type,
990
+ label,
991
+ test: void 0,
992
+ endOfInitSegments: null,
993
+ testSegments: null,
994
+ endOfTestSegments: null,
995
+ updateSegments: null,
996
+ endOfUpdateSegments: null,
997
+ continueDestSegments: null,
998
+ brokenForkContext: breakContext.brokenForkContext,
999
+ };
1000
+ break;
1001
+
1002
+ case 'ForInStatement':
1003
+ case 'ForOfStatement':
1004
+ this.loopContext = {
1005
+ upper: this.loopContext,
1006
+ type,
1007
+ label,
1008
+ prevSegments: null,
1009
+ leftSegments: null,
1010
+ endOfLeftSegments: null,
1011
+ continueDestSegments: null,
1012
+ brokenForkContext: breakContext.brokenForkContext,
1013
+ };
1014
+ break;
1015
+
1016
+ /* c8 ignore next */
1017
+ default:
1018
+ throw new Error(`unknown type: "${type}"`);
1019
+ }
1020
+ }
1021
+
1022
+ /**
1023
+ * Pops the last context of a loop statement and finalizes it.
1024
+ * @throws {Error} (Unreachable - unknown type.)
1025
+ * @returns {void}
1026
+ */
1027
+ popLoopContext() {
1028
+ const context = this.loopContext;
1029
+
1030
+ this.loopContext = context.upper;
1031
+
1032
+ const forkContext = this.forkContext;
1033
+ const brokenForkContext = this.popBreakContext().brokenForkContext;
1034
+
1035
+ // Creates a looped path.
1036
+ switch (context.type) {
1037
+ case 'WhileStatement':
1038
+ case 'ForStatement':
1039
+ this.popChoiceContext();
1040
+ makeLooped(this, forkContext.head, context.continueDestSegments);
1041
+ break;
1042
+
1043
+ case 'DoWhileStatement': {
1044
+ const choiceContext = this.popChoiceContext();
1045
+
1046
+ if (!choiceContext.processed) {
1047
+ choiceContext.trueForkContext.add(forkContext.head);
1048
+ choiceContext.falseForkContext.add(forkContext.head);
1049
+ }
1050
+ if (context.test !== true) {
1051
+ brokenForkContext.addAll(choiceContext.falseForkContext);
1052
+ }
1053
+
1054
+ // `true` paths go to looping.
1055
+ const segmentsList = choiceContext.trueForkContext.segmentsList;
1056
+
1057
+ for (let i = 0; i < segmentsList.length; ++i) {
1058
+ makeLooped(this, segmentsList[i], context.entrySegments);
1059
+ }
1060
+ break;
1061
+ }
1062
+
1063
+ case 'ForInStatement':
1064
+ case 'ForOfStatement':
1065
+ brokenForkContext.add(forkContext.head);
1066
+ makeLooped(this, forkContext.head, context.leftSegments);
1067
+ break;
1068
+
1069
+ /* c8 ignore next */
1070
+ default:
1071
+ throw new Error('unreachable');
1072
+ }
1073
+
1074
+ // Go next.
1075
+ if (brokenForkContext.empty) {
1076
+ forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
1077
+ } else {
1078
+ forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
1079
+ }
1080
+ }
1081
+
1082
+ /**
1083
+ * Makes a code path segment for the test part of a WhileStatement.
1084
+ * @param {boolean|undefined} test The test value (only when constant).
1085
+ * @returns {void}
1086
+ */
1087
+ makeWhileTest(test) {
1088
+ const context = this.loopContext;
1089
+ const forkContext = this.forkContext;
1090
+ const testSegments = forkContext.makeNext(0, -1);
1091
+
1092
+ // Update state.
1093
+ context.test = test;
1094
+ context.continueDestSegments = testSegments;
1095
+ forkContext.replaceHead(testSegments);
1096
+ }
1097
+
1098
+ /**
1099
+ * Makes a code path segment for the body part of a WhileStatement.
1100
+ * @returns {void}
1101
+ */
1102
+ makeWhileBody() {
1103
+ const context = this.loopContext;
1104
+ const choiceContext = this.choiceContext;
1105
+ const forkContext = this.forkContext;
1106
+
1107
+ if (!choiceContext.processed) {
1108
+ choiceContext.trueForkContext.add(forkContext.head);
1109
+ choiceContext.falseForkContext.add(forkContext.head);
1110
+ }
1111
+
1112
+ // Update state.
1113
+ if (context.test !== true) {
1114
+ context.brokenForkContext.addAll(choiceContext.falseForkContext);
1115
+ }
1116
+ forkContext.replaceHead(choiceContext.trueForkContext.makeNext(0, -1));
1117
+ }
1118
+
1119
+ /**
1120
+ * Makes a code path segment for the body part of a DoWhileStatement.
1121
+ * @returns {void}
1122
+ */
1123
+ makeDoWhileBody() {
1124
+ const context = this.loopContext;
1125
+ const forkContext = this.forkContext;
1126
+ const bodySegments = forkContext.makeNext(-1, -1);
1127
+
1128
+ // Update state.
1129
+ context.entrySegments = bodySegments;
1130
+ forkContext.replaceHead(bodySegments);
1131
+ }
1132
+
1133
+ /**
1134
+ * Makes a code path segment for the test part of a DoWhileStatement.
1135
+ * @param {boolean|undefined} test The test value (only when constant).
1136
+ * @returns {void}
1137
+ */
1138
+ makeDoWhileTest(test) {
1139
+ const context = this.loopContext;
1140
+ const forkContext = this.forkContext;
1141
+
1142
+ context.test = test;
1143
+
1144
+ // Creates paths of `continue` statements.
1145
+ if (!context.continueForkContext.empty) {
1146
+ context.continueForkContext.add(forkContext.head);
1147
+ const testSegments = context.continueForkContext.makeNext(0, -1);
1148
+
1149
+ forkContext.replaceHead(testSegments);
1150
+ }
1151
+ }
1152
+
1153
+ /**
1154
+ * Makes a code path segment for the test part of a ForStatement.
1155
+ * @param {boolean|undefined} test The test value (only when constant).
1156
+ * @returns {void}
1157
+ */
1158
+ makeForTest(test) {
1159
+ const context = this.loopContext;
1160
+ const forkContext = this.forkContext;
1161
+ const endOfInitSegments = forkContext.head;
1162
+ const testSegments = forkContext.makeNext(-1, -1);
1163
+
1164
+ // Update state.
1165
+ context.test = test;
1166
+ context.endOfInitSegments = endOfInitSegments;
1167
+ context.continueDestSegments = context.testSegments = testSegments;
1168
+ forkContext.replaceHead(testSegments);
1169
+ }
1170
+
1171
+ /**
1172
+ * Makes a code path segment for the update part of a ForStatement.
1173
+ * @returns {void}
1174
+ */
1175
+ makeForUpdate() {
1176
+ const context = this.loopContext;
1177
+ const choiceContext = this.choiceContext;
1178
+ const forkContext = this.forkContext;
1179
+
1180
+ // Make the next paths of the test.
1181
+ if (context.testSegments) {
1182
+ finalizeTestSegmentsOfFor(context, choiceContext, forkContext.head);
1183
+ } else {
1184
+ context.endOfInitSegments = forkContext.head;
1185
+ }
1186
+
1187
+ // Update state.
1188
+ const updateSegments = forkContext.makeDisconnected(-1, -1);
1189
+
1190
+ context.continueDestSegments = context.updateSegments = updateSegments;
1191
+ forkContext.replaceHead(updateSegments);
1192
+ }
1193
+
1194
+ /**
1195
+ * Makes a code path segment for the body part of a ForStatement.
1196
+ * @returns {void}
1197
+ */
1198
+ makeForBody() {
1199
+ const context = this.loopContext;
1200
+ const choiceContext = this.choiceContext;
1201
+ const forkContext = this.forkContext;
1202
+
1203
+ // Update state.
1204
+ if (context.updateSegments) {
1205
+ context.endOfUpdateSegments = forkContext.head;
1206
+
1207
+ // `update` -> `test`
1208
+ if (context.testSegments) {
1209
+ makeLooped(this, context.endOfUpdateSegments, context.testSegments);
1210
+ }
1211
+ } else if (context.testSegments) {
1212
+ finalizeTestSegmentsOfFor(context, choiceContext, forkContext.head);
1213
+ } else {
1214
+ context.endOfInitSegments = forkContext.head;
1215
+ }
1216
+
1217
+ let bodySegments = context.endOfTestSegments;
1218
+
1219
+ if (!bodySegments) {
1220
+ /*
1221
+ * If there is not the `test` part, the `body` path comes from the
1222
+ * `init` part and the `update` part.
1223
+ */
1224
+ const prevForkContext = ForkContext.newEmpty(forkContext);
1225
+
1226
+ prevForkContext.add(context.endOfInitSegments);
1227
+ if (context.endOfUpdateSegments) {
1228
+ prevForkContext.add(context.endOfUpdateSegments);
1229
+ }
1230
+
1231
+ bodySegments = prevForkContext.makeNext(0, -1);
1232
+ }
1233
+ context.continueDestSegments = context.continueDestSegments || bodySegments;
1234
+ forkContext.replaceHead(bodySegments);
1235
+ }
1236
+
1237
+ /**
1238
+ * Makes a code path segment for the left part of a ForInStatement and a
1239
+ * ForOfStatement.
1240
+ * @returns {void}
1241
+ */
1242
+ makeForInOfLeft() {
1243
+ const context = this.loopContext;
1244
+ const forkContext = this.forkContext;
1245
+ const leftSegments = forkContext.makeDisconnected(-1, -1);
1246
+
1247
+ // Update state.
1248
+ context.prevSegments = forkContext.head;
1249
+ context.leftSegments = context.continueDestSegments = leftSegments;
1250
+ forkContext.replaceHead(leftSegments);
1251
+ }
1252
+
1253
+ /**
1254
+ * Makes a code path segment for the right part of a ForInStatement and a
1255
+ * ForOfStatement.
1256
+ * @returns {void}
1257
+ */
1258
+ makeForInOfRight() {
1259
+ const context = this.loopContext;
1260
+ const forkContext = this.forkContext;
1261
+ const temp = ForkContext.newEmpty(forkContext);
1262
+
1263
+ temp.add(context.prevSegments);
1264
+ const rightSegments = temp.makeNext(-1, -1);
1265
+
1266
+ // Update state.
1267
+ context.endOfLeftSegments = forkContext.head;
1268
+ forkContext.replaceHead(rightSegments);
1269
+ }
1270
+
1271
+ /**
1272
+ * Makes a code path segment for the body part of a ForInStatement and a
1273
+ * ForOfStatement.
1274
+ * @returns {void}
1275
+ */
1276
+ makeForInOfBody() {
1277
+ const context = this.loopContext;
1278
+ const forkContext = this.forkContext;
1279
+ const temp = ForkContext.newEmpty(forkContext);
1280
+
1281
+ temp.add(context.endOfLeftSegments);
1282
+ const bodySegments = temp.makeNext(-1, -1);
1283
+
1284
+ // Make a path: `right` -> `left`.
1285
+ makeLooped(this, forkContext.head, context.leftSegments);
1286
+
1287
+ // Update state.
1288
+ context.brokenForkContext.add(forkContext.head);
1289
+ forkContext.replaceHead(bodySegments);
1290
+ }
1291
+
1292
+ //--------------------------------------------------------------------------
1293
+ // Control Statements
1294
+ //--------------------------------------------------------------------------
1295
+
1296
+ /**
1297
+ * Creates new context for BreakStatement.
1298
+ * @param {boolean} breakable The flag to indicate it can break by
1299
+ * an unlabeled BreakStatement.
1300
+ * @param {string|null} label The label of this context.
1301
+ * @returns {Object} The new context.
1302
+ */
1303
+ pushBreakContext(breakable, label) {
1304
+ this.breakContext = {
1305
+ upper: this.breakContext,
1306
+ breakable,
1307
+ label,
1308
+ brokenForkContext: ForkContext.newEmpty(this.forkContext),
1309
+ };
1310
+ return this.breakContext;
1311
+ }
1312
+
1313
+ /**
1314
+ * Removes the top item of the break context stack.
1315
+ * @returns {Object} The removed context.
1316
+ */
1317
+ popBreakContext() {
1318
+ const context = this.breakContext;
1319
+ const forkContext = this.forkContext;
1320
+
1321
+ this.breakContext = context.upper;
1322
+
1323
+ // Process this context here for other than switches and loops.
1324
+ if (!context.breakable) {
1325
+ const brokenForkContext = context.brokenForkContext;
1326
+
1327
+ if (!brokenForkContext.empty) {
1328
+ brokenForkContext.add(forkContext.head);
1329
+ forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
1330
+ }
1331
+ }
1332
+
1333
+ return context;
1334
+ }
1335
+
1336
+ /**
1337
+ * Makes a path for a `break` statement.
1338
+ *
1339
+ * It registers the head segment to a context of `break`.
1340
+ * It makes new unreachable segment, then it set the head with the segment.
1341
+ * @param {string} label A label of the break statement.
1342
+ * @returns {void}
1343
+ */
1344
+ makeBreak(label) {
1345
+ const forkContext = this.forkContext;
1346
+
1347
+ if (!forkContext.reachable) {
1348
+ return;
1349
+ }
1350
+
1351
+ const context = getBreakContext(this, label);
1352
+
1353
+ if (context) {
1354
+ context.brokenForkContext.add(forkContext.head);
1355
+ }
1356
+
1357
+ /* c8 ignore next */
1358
+ forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
1359
+ }
1360
+
1361
+ /**
1362
+ * Makes a path for a `continue` statement.
1363
+ *
1364
+ * It makes a looping path.
1365
+ * It makes new unreachable segment, then it set the head with the segment.
1366
+ * @param {string} label A label of the continue statement.
1367
+ * @returns {void}
1368
+ */
1369
+ makeContinue(label) {
1370
+ const forkContext = this.forkContext;
1371
+
1372
+ if (!forkContext.reachable) {
1373
+ return;
1374
+ }
1375
+
1376
+ const context = getContinueContext(this, label);
1377
+
1378
+ if (context) {
1379
+ if (context.continueDestSegments) {
1380
+ makeLooped(this, forkContext.head, context.continueDestSegments);
1381
+
1382
+ // If the context is a for-in/of loop, this effects a break also.
1383
+ if (
1384
+ context.type === 'ForInStatement' ||
1385
+ context.type === 'ForOfStatement'
1386
+ ) {
1387
+ context.brokenForkContext.add(forkContext.head);
1388
+ }
1389
+ } else {
1390
+ context.continueForkContext.add(forkContext.head);
1391
+ }
1392
+ }
1393
+ forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
1394
+ }
1395
+
1396
+ /**
1397
+ * Makes a path for a `return` statement.
1398
+ *
1399
+ * It registers the head segment to a context of `return`.
1400
+ * It makes new unreachable segment, then it set the head with the segment.
1401
+ * @returns {void}
1402
+ */
1403
+ makeReturn() {
1404
+ const forkContext = this.forkContext;
1405
+
1406
+ if (forkContext.reachable) {
1407
+ getReturnContext(this).returnedForkContext.add(forkContext.head);
1408
+ forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
1409
+ }
1410
+ }
1411
+
1412
+ /**
1413
+ * Makes a path for a `throw` statement.
1414
+ *
1415
+ * It registers the head segment to a context of `throw`.
1416
+ * It makes new unreachable segment, then it set the head with the segment.
1417
+ * @returns {void}
1418
+ */
1419
+ makeThrow() {
1420
+ const forkContext = this.forkContext;
1421
+
1422
+ if (forkContext.reachable) {
1423
+ getThrowContext(this).thrownForkContext.add(forkContext.head);
1424
+ forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
1425
+ }
1426
+ }
1427
+
1428
+ /**
1429
+ * Makes the final path.
1430
+ * @returns {void}
1431
+ */
1432
+ makeFinal() {
1433
+ const segments = this.currentSegments;
1434
+
1435
+ if (segments.length > 0 && segments[0].reachable) {
1436
+ this.returnedForkContext.add(segments);
1437
+ }
1438
+ }
1439
+}
1440
+
1441
+module.exports = CodePathState;
packages/eslint-plugin-react-hooks/src/code-path-analysis/code-path.js
new
+239
@@ -0,0 +1,239 @@
1
+'use strict';
2
+
3
+//------------------------------------------------------------------------------
4
+// Requirements
5
+//------------------------------------------------------------------------------
6
+
7
+// eslint-disable-next-line
8
+const CodePathState = require('./code-path-state');
9
+// eslint-disable-next-line
10
+const IdGenerator = require('./id-generator');
11
+
12
+//------------------------------------------------------------------------------
13
+// Public Interface
14
+//------------------------------------------------------------------------------
15
+
16
+/**
17
+ * A code path.
18
+ */
19
+class CodePath {
20
+ /**
21
+ * Creates a new instance.
22
+ * @param {Object} options Options for the function (see below).
23
+ * @param {string} options.id An identifier.
24
+ * @param {string} options.origin The type of code path origin.
25
+ * @param {CodePath|null} options.upper The code path of the upper function scope.
26
+ * @param {Function} options.onLooped A callback function to notify looping.
27
+ */
28
+ constructor({id, origin, upper, onLooped}) {
29
+ /**
30
+ * The identifier of this code path.
31
+ * Rules use it to store additional information of each rule.
32
+ * @type {string}
33
+ */
34
+ this.id = id;
35
+
36
+ /**
37
+ * The reason that this code path was started. May be "program",
38
+ * "function", "class-field-initializer", or "class-static-block".
39
+ * @type {string}
40
+ */
41
+ this.origin = origin;
42
+
43
+ /**
44
+ * The code path of the upper function scope.
45
+ * @type {CodePath|null}
46
+ */
47
+ this.upper = upper;
48
+
49
+ /**
50
+ * The code paths of nested function scopes.
51
+ * @type {CodePath[]}
52
+ */
53
+ this.childCodePaths = [];
54
+
55
+ // Initializes internal state.
56
+ Object.defineProperty(this, 'internal', {
57
+ value: new CodePathState(new IdGenerator(`${id}_`), onLooped),
58
+ });
59
+
60
+ // Adds this into `childCodePaths` of `upper`.
61
+ if (upper) {
62
+ upper.childCodePaths.push(this);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Gets the state of a given code path.
68
+ * @param {CodePath} codePath A code path to get.
69
+ * @returns {CodePathState} The state of the code path.
70
+ */
71
+ static getState(codePath) {
72
+ return codePath.internal;
73
+ }
74
+
75
+ /**
76
+ * The initial code path segment.
77
+ * @type {CodePathSegment}
78
+ */
79
+ get initialSegment() {
80
+ return this.internal.initialSegment;
81
+ }
82
+
83
+ /**
84
+ * Final code path segments.
85
+ * This array is a mix of `returnedSegments` and `thrownSegments`.
86
+ * @type {CodePathSegment[]}
87
+ */
88
+ get finalSegments() {
89
+ return this.internal.finalSegments;
90
+ }
91
+
92
+ /**
93
+ * Final code path segments which is with `return` statements.
94
+ * This array contains the last path segment if it's reachable.
95
+ * Since the reachable last path returns `undefined`.
96
+ * @type {CodePathSegment[]}
97
+ */
98
+ get returnedSegments() {
99
+ return this.internal.returnedForkContext;
100
+ }
101
+
102
+ /**
103
+ * Final code path segments which is with `throw` statements.
104
+ * @type {CodePathSegment[]}
105
+ */
106
+ get thrownSegments() {
107
+ return this.internal.thrownForkContext;
108
+ }
109
+
110
+ /**
111
+ * Current code path segments.
112
+ * @type {CodePathSegment[]}
113
+ */
114
+ get currentSegments() {
115
+ return this.internal.currentSegments;
116
+ }
117
+
118
+ /**
119
+ * Traverses all segments in this code path.
120
+ *
121
+ * codePath.traverseSegments(function(segment, controller) {
122
+ * // do something.
123
+ * });
124
+ *
125
+ * This method enumerates segments in order from the head.
126
+ *
127
+ * The `controller` object has two methods.
128
+ *
129
+ * - `controller.skip()` - Skip the following segments in this branch.
130
+ * - `controller.break()` - Skip all following segments.
131
+ * @param {Object} [options] Omittable.
132
+ * @param {CodePathSegment} [options.first] The first segment to traverse.
133
+ * @param {CodePathSegment} [options.last] The last segment to traverse.
134
+ * @param {Function} callback A callback function.
135
+ * @returns {void}
136
+ */
137
+ traverseSegments(options, callback) {
138
+ let resolvedOptions;
139
+ let resolvedCallback;
140
+
141
+ if (typeof options === 'function') {
142
+ resolvedCallback = options;
143
+ resolvedOptions = {};
144
+ } else {
145
+ resolvedOptions = options || {};
146
+ resolvedCallback = callback;
147
+ }
148
+
149
+ const startSegment = resolvedOptions.first || this.internal.initialSegment;
150
+ const lastSegment = resolvedOptions.last;
151
+
152
+ let item = null;
153
+ let index = 0;
154
+ let end = 0;
155
+ let segment = null;
156
+ const visited = Object.create(null);
157
+ const stack = [[startSegment, 0]];
158
+ let skippedSegment = null;
159
+ let broken = false;
160
+ const controller = {
161
+ skip() {
162
+ if (stack.length <= 1) {
163
+ broken = true;
164
+ } else {
165
+ skippedSegment = stack[stack.length - 2][0];
166
+ }
167
+ },
168
+ break() {
169
+ broken = true;
170
+ },
171
+ };
172
+
173
+ /**
174
+ * Checks a given previous segment has been visited.
175
+ * @param {CodePathSegment} prevSegment A previous segment to check.
176
+ * @returns {boolean} `true` if the segment has been visited.
177
+ */
178
+ function isVisited(prevSegment) {
179
+ return (
180
+ visited[prevSegment.id] || segment.isLoopedPrevSegment(prevSegment)
181
+ );
182
+ }
183
+
184
+ while (stack.length > 0) {
185
+ item = stack[stack.length - 1];
186
+ segment = item[0];
187
+ index = item[1];
188
+
189
+ if (index === 0) {
190
+ // Skip if this segment has been visited already.
191
+ if (visited[segment.id]) {
192
+ stack.pop();
193
+ continue;
194
+ }
195
+
196
+ // Skip if all previous segments have not been visited.
197
+ if (
198
+ segment !== startSegment &&
199
+ segment.prevSegments.length > 0 &&
200
+ !segment.prevSegments.every(isVisited)
201
+ ) {
202
+ stack.pop();
203
+ continue;
204
+ }
205
+
206
+ // Reset the flag of skipping if all branches have been skipped.
207
+ if (skippedSegment && segment.prevSegments.includes(skippedSegment)) {
208
+ skippedSegment = null;
209
+ }
210
+ visited[segment.id] = true;
211
+
212
+ // Call the callback when the first time.
213
+ if (!skippedSegment) {
214
+ resolvedCallback.call(this, segment, controller);
215
+ if (segment === lastSegment) {
216
+ controller.skip();
217
+ }
218
+ if (broken) {
219
+ break;
220
+ }
221
+ }
222
+ }
223
+
224
+ // Update the stack.
225
+ end = segment.nextSegments.length - 1;
226
+ if (index < end) {
227
+ item[1] += 1;
228
+ stack.push([segment.nextSegments[index], 0]);
229
+ } else if (index === end) {
230
+ item[0] = segment.nextSegments[index];
231
+ item[1] = 0;
232
+ } else {
233
+ stack.pop();
234
+ }
235
+ }
236
+ }
237
+}
238
+
239
+module.exports = CodePath;
packages/eslint-plugin-react-hooks/src/code-path-analysis/fork-context.js
new
+252
@@ -0,0 +1,252 @@
1
+'use strict';
2
+
3
+//------------------------------------------------------------------------------
4
+// Requirements
5
+//------------------------------------------------------------------------------
6
+
7
+// eslint-disable-next-line
8
+const assert = require('./assert');
9
+// eslint-disable-next-line
10
+const CodePathSegment = require('./code-path-segment');
11
+
12
+//------------------------------------------------------------------------------
13
+// Helpers
14
+//------------------------------------------------------------------------------
15
+
16
+/**
17
+ * Gets whether or not a given segment is reachable.
18
+ * @param {CodePathSegment} segment A segment to get.
19
+ * @returns {boolean} `true` if the segment is reachable.
20
+ */
21
+function isReachable(segment) {
22
+ return segment.reachable;
23
+}
24
+
25
+/**
26
+ * Creates new segments from the specific range of `context.segmentsList`.
27
+ *
28
+ * When `context.segmentsList` is `[[a, b], [c, d], [e, f]]`, `begin` is `0`, and
29
+ * `end` is `-1`, this creates `[g, h]`. This `g` is from `a`, `c`, and `e`.
30
+ * This `h` is from `b`, `d`, and `f`.
31
+ * @param {ForkContext} context An instance.
32
+ * @param {number} begin The first index of the previous segments.
33
+ * @param {number} end The last index of the previous segments.
34
+ * @param {Function} create A factory function of new segments.
35
+ * @returns {CodePathSegment[]} New segments.
36
+ */
37
+function makeSegments(context, begin, end, create) {
38
+ const list = context.segmentsList;
39
+
40
+ const normalizedBegin = begin >= 0 ? begin : list.length + begin;
41
+ const normalizedEnd = end >= 0 ? end : list.length + end;
42
+
43
+ const segments = [];
44
+
45
+ for (let i = 0; i < context.count; ++i) {
46
+ const allPrevSegments = [];
47
+
48
+ for (let j = normalizedBegin; j <= normalizedEnd; ++j) {
49
+ allPrevSegments.push(list[j][i]);
50
+ }
51
+
52
+ segments.push(create(context.idGenerator.next(), allPrevSegments));
53
+ }
54
+
55
+ return segments;
56
+}
57
+
58
+/**
59
+ * `segments` becomes doubly in a `finally` block. Then if a code path exits by a
60
+ * control statement (such as `break`, `continue`) from the `finally` block, the
61
+ * destination's segments may be half of the source segments. In that case, this
62
+ * merges segments.
63
+ * @param {ForkContext} context An instance.
64
+ * @param {CodePathSegment[]} segments Segments to merge.
65
+ * @returns {CodePathSegment[]} The merged segments.
66
+ */
67
+function mergeExtraSegments(context, segments) {
68
+ let currentSegments = segments;
69
+
70
+ while (currentSegments.length > context.count) {
71
+ const merged = [];
72
+
73
+ for (
74
+ let i = 0, length = (currentSegments.length / 2) | 0;
75
+ i < length;
76
+ ++i
77
+ ) {
78
+ merged.push(
79
+ CodePathSegment.newNext(context.idGenerator.next(), [
80
+ currentSegments[i],
81
+ currentSegments[i + length],
82
+ ]),
83
+ );
84
+ }
85
+ currentSegments = merged;
86
+ }
87
+ return currentSegments;
88
+}
89
+
90
+//------------------------------------------------------------------------------
91
+// Public Interface
92
+//------------------------------------------------------------------------------
93
+
94
+/**
95
+ * A class to manage forking.
96
+ */
97
+class ForkContext {
98
+ /**
99
+ * @param {IdGenerator} idGenerator An identifier generator for segments.
100
+ * @param {ForkContext|null} upper An upper fork context.
101
+ * @param {number} count A number of parallel segments.
102
+ */
103
+ constructor(idGenerator, upper, count) {
104
+ this.idGenerator = idGenerator;
105
+ this.upper = upper;
106
+ this.count = count;
107
+ this.segmentsList = [];
108
+ }
109
+
110
+ /**
111
+ * The head segments.
112
+ * @type {CodePathSegment[]}
113
+ */
114
+ get head() {
115
+ const list = this.segmentsList;
116
+
117
+ return list.length === 0 ? [] : list[list.length - 1];
118
+ }
119
+
120
+ /**
121
+ * A flag which shows empty.
122
+ * @type {boolean}
123
+ */
124
+ get empty() {
125
+ return this.segmentsList.length === 0;
126
+ }
127
+
128
+ /**
129
+ * A flag which shows reachable.
130
+ * @type {boolean}
131
+ */
132
+ get reachable() {
133
+ const segments = this.head;
134
+
135
+ return segments.length > 0 && segments.some(isReachable);
136
+ }
137
+
138
+ /**
139
+ * Creates new segments from this context.
140
+ * @param {number} begin The first index of previous segments.
141
+ * @param {number} end The last index of previous segments.
142
+ * @returns {CodePathSegment[]} New segments.
143
+ */
144
+ makeNext(begin, end) {
145
+ return makeSegments(this, begin, end, CodePathSegment.newNext);
146
+ }
147
+
148
+ /**
149
+ * Creates new segments from this context.
150
+ * The new segments is always unreachable.
151
+ * @param {number} begin The first index of previous segments.
152
+ * @param {number} end The last index of previous segments.
153
+ * @returns {CodePathSegment[]} New segments.
154
+ */
155
+ makeUnreachable(begin, end) {
156
+ return makeSegments(this, begin, end, CodePathSegment.newUnreachable);
157
+ }
158
+
159
+ /**
160
+ * Creates new segments from this context.
161
+ * The new segments don't have connections for previous segments.
162
+ * But these inherit the reachable flag from this context.
163
+ * @param {number} begin The first index of previous segments.
164
+ * @param {number} end The last index of previous segments.
165
+ * @returns {CodePathSegment[]} New segments.
166
+ */
167
+ makeDisconnected(begin, end) {
168
+ return makeSegments(this, begin, end, CodePathSegment.newDisconnected);
169
+ }
170
+
171
+ /**
172
+ * Adds segments into this context.
173
+ * The added segments become the head.
174
+ * @param {CodePathSegment[]} segments Segments to add.
175
+ * @returns {void}
176
+ */
177
+ add(segments) {
178
+ assert(
179
+ segments.length >= this.count,
180
+ `${segments.length} >= ${this.count}`,
181
+ );
182
+
183
+ this.segmentsList.push(mergeExtraSegments(this, segments));
184
+ }
185
+
186
+ /**
187
+ * Replaces the head segments with given segments.
188
+ * The current head segments are removed.
189
+ * @param {CodePathSegment[]} segments Segments to add.
190
+ * @returns {void}
191
+ */
192
+ replaceHead(segments) {
193
+ assert(
194
+ segments.length >= this.count,
195
+ `${segments.length} >= ${this.count}`,
196
+ );
197
+
198
+ this.segmentsList.splice(-1, 1, mergeExtraSegments(this, segments));
199
+ }
200
+
201
+ /**
202
+ * Adds all segments of a given fork context into this context.
203
+ * @param {ForkContext} context A fork context to add.
204
+ * @returns {void}
205
+ */
206
+ addAll(context) {
207
+ assert(context.count === this.count);
208
+
209
+ const source = context.segmentsList;
210
+
211
+ for (let i = 0; i < source.length; ++i) {
212
+ this.segmentsList.push(source[i]);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Clears all segments in this context.
218
+ * @returns {void}
219
+ */
220
+ clear() {
221
+ this.segmentsList = [];
222
+ }
223
+
224
+ /**
225
+ * Creates the root fork context.
226
+ * @param {IdGenerator} idGenerator An identifier generator for segments.
227
+ * @returns {ForkContext} New fork context.
228
+ */
229
+ static newRoot(idGenerator) {
230
+ const context = new ForkContext(idGenerator, null, 1);
231
+
232
+ context.add([CodePathSegment.newRoot(idGenerator.next())]);
233
+
234
+ return context;
235
+ }
236
+
237
+ /**
238
+ * Creates an empty fork context preceded by a given context.
239
+ * @param {ForkContext} parentContext The parent fork context.
240
+ * @param {boolean} forkLeavingPath A flag which shows inside of `finally` block.
241
+ * @returns {ForkContext} New fork context.
242
+ */
243
+ static newEmpty(parentContext, forkLeavingPath) {
244
+ return new ForkContext(
245
+ parentContext.idGenerator,
246
+ parentContext,
247
+ (forkLeavingPath ? 2 : 1) * parentContext.count,
248
+ );
249
+ }
250
+}
251
+
252
+module.exports = ForkContext;
packages/eslint-plugin-react-hooks/src/code-path-analysis/id-generator.js
new
+37
@@ -0,0 +1,37 @@
1
+'use strict';
2
+
3
+/* eslint-disable react-internal/safe-string-coercion */
4
+
5
+//------------------------------------------------------------------------------
6
+// Public Interface
7
+//------------------------------------------------------------------------------
8
+
9
+/**
10
+ * A generator for unique ids.
11
+ */
12
+class IdGenerator {
13
+ /**
14
+ * @param {string} prefix Optional. A prefix of generated ids.
15
+ */
16
+ constructor(prefix) {
17
+ this.prefix = String(prefix);
18
+ this.n = 0;
19
+ }
20
+
21
+ /**
22
+ * Generates id.
23
+ * @returns {string} A generated id.
24
+ */
25
+ next() {
26
+ this.n = (1 + this.n) | 0;
27
+
28
+ /* c8 ignore start */
29
+ if (this.n < 0) {
30
+ this.n = 1;
31
+ } /* c8 ignore stop */
32
+
33
+ return this.prefix + this.n;
34
+ }
35
+}
36
+
37
+module.exports = IdGenerator;
packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts
+41
-3
@@ -9,6 +9,9 @@
9
import type {Rule, Scope} from 'eslint';
10
import type {CallExpression, DoWhileStatement, Node} from 'estree';
11
12
+// @ts-expect-error untyped module
13
+import CodePathAnalyzer from '../code-path-analysis/code-path-analyzer';
14
+
15
/**
16
* Catch all identifiers that begin with "use" followed by an uppercase Latin
17
* character to exclude identifiers like "user".
@@ -184,9 +187,25 @@ const rule = {
187
return getSourceCode().getScope(node);
188
};
189
187
- return {
190
+ function hasFlowSuppression(node: Node, suppression: string) {
191
+ const sourceCode = getSourceCode();
192
+ const comments = sourceCode.getAllComments();
193
+ const flowSuppressionRegex = new RegExp(
194
+ '\\$FlowFixMe\\[' + suppression + '\\]',
195
+ );
196
+ return comments.some(
197
+ commentNode =>
198
+ flowSuppressionRegex.test(commentNode.value) &&
199
+ commentNode.loc != null &&
200
+ node.loc != null &&
201
+ commentNode.loc.end.line === node.loc.start.line - 1,
202
+ );
203
+ }
204
+
205
+ const analyzer = new CodePathAnalyzer({
206
// Maintain code segment path stack as we traverse.
189
- onCodePathSegmentStart: segment => codePathSegmentStack.push(segment),
207
+ onCodePathSegmentStart: (segment: Rule.CodePathSegment) =>
208
+ codePathSegmentStack.push(segment),
209
onCodePathSegmentEnd: () => codePathSegmentStack.pop(),
210
211
// Maintain code path stack as we traverse.
@@ -199,7 +218,7 @@ const rule = {
218
//
219
// Everything is ok if all React Hooks are both reachable from the initial
220
// segment and reachable from every final segment.
202
- onCodePathEnd(codePath, codePathNode) {
221
+ onCodePathEnd(codePath: any, codePathNode: Node) {
222
const reactHooksMap = codePathReactHooksMapStack.pop();
223
if (reactHooksMap?.size === 0) {
224
return;
@@ -508,6 +527,11 @@ const rule = {
527
const cycled = cyclic.has(segment.id);
528
529
for (const hook of reactHooks) {
530
+ // Skip reporting if this hook already has a relevant flow suppression.
531
+ if (hasFlowSuppression(hook, 'react-rule-hook')) {
532
+ continue;
533
+ }
534
+
535
// Report an error if a hook may be called more then once.
536
// `use(...)` can be called in loops.
537
if (
@@ -611,6 +635,16 @@ const rule = {
635
}
636
}
637
},
638
+ });
639
+
640
+ return {
641
+ '*'(node: any) {
642
+ analyzer.enterNode(node);
643
+ },
644
+
645
+ '*:exit'(node: any) {
646
+ analyzer.leaveNode(node);
647
+ },
648
649
// Missed opportunity...We could visit all `Identifier`s instead of all
650
// `CallExpression`s and check that _every use_ of a hook name is valid.
@@ -696,6 +730,10 @@ const rule = {
730
731
function getFunctionName(node: Node) {
732
if (
733
+ // @ts-expect-error parser-hermes produces these node types
734
+ node.type === 'ComponentDeclaration' ||
735
+ // @ts-expect-error parser-hermes produces these node types
736
+ node.type === 'HookDeclaration' ||
737
node.type === 'FunctionDeclaration' ||
738
(node.type === 'FunctionExpression' && node.id)
739
) {