eslint-plugin-react-x 3.0.0-next.26 → 3.0.0-next.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +2217 -2250
  2. package/package.json +6 -6
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ const rules$7 = {
69
69
  //#endregion
70
70
  //#region package.json
71
71
  var name$6 = "eslint-plugin-react-x";
72
- var version = "3.0.0-next.26";
72
+ var version = "3.0.0-next.28";
73
73
 
74
74
  //#endregion
75
75
  //#region src/utils/create-rule.ts
@@ -334,12 +334,6 @@ function create$63(context) {
334
334
 
335
335
  //#endregion
336
336
  //#region src/rules/exhaustive-deps.ts
337
- /**
338
- * Copyright (c) Meta Platforms, Inc. and affiliates.
339
- *
340
- * This source code is licensed under the MIT license found in the
341
- * LICENSE file in the root directory of this source tree.
342
- */
343
337
  const rule$1 = {
344
338
  meta: {
345
339
  type: "suggestion",
@@ -4400,2432 +4394,2405 @@ function create$5(context) {
4400
4394
  }
4401
4395
 
4402
4396
  //#endregion
4403
- //#region src/utils/code-path-analysis/assert.js
4404
- function assert(cond) {
4405
- if (!cond) throw new Error("Assertion violated.");
4406
- }
4407
-
4408
- //#endregion
4409
- //#region src/utils/code-path-analysis/code-path-segment.js
4397
+ //#region src/rules/rules-of-hooks.ts
4410
4398
  /**
4411
- * Checks whether or not a given segment is reachable.
4412
- * @param {CodePathSegment} segment A segment to check.
4413
- * @returns {boolean} `true` if the segment is reachable.
4399
+ * Catch all identifiers that begin with "use" followed by an uppercase Latin
4400
+ * character to exclude identifiers like "user".
4414
4401
  */
4415
- function isReachable$1(segment) {
4416
- return segment.reachable;
4402
+ function isHookName(s) {
4403
+ return s === "use" || /^use[A-Z0-9]/.test(s);
4417
4404
  }
4418
4405
  /**
4419
- * A code path segment.
4420
- */
4421
- var CodePathSegment = class CodePathSegment {
4422
- /**
4423
- * @param {string} id An identifier.
4424
- * @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
4425
- * This array includes unreachable segments.
4426
- * @param {boolean} reachable A flag which shows this is reachable.
4427
- */
4428
- constructor(id, allPrevSegments, reachable) {
4429
- /**
4430
- * The identifier of this code path.
4431
- * Rules use it to store additional information of each rule.
4432
- * @type {string}
4433
- */
4434
- this.id = id;
4435
- /**
4436
- * An array of the next segments.
4437
- * @type {CodePathSegment[]}
4438
- */
4439
- this.nextSegments = [];
4440
- /**
4441
- * An array of the previous segments.
4442
- * @type {CodePathSegment[]}
4443
- */
4444
- this.prevSegments = allPrevSegments.filter(isReachable$1);
4445
- /**
4446
- * An array of the next segments.
4447
- * This array includes unreachable segments.
4448
- * @type {CodePathSegment[]}
4449
- */
4450
- this.allNextSegments = [];
4451
- /**
4452
- * An array of the previous segments.
4453
- * This array includes unreachable segments.
4454
- * @type {CodePathSegment[]}
4455
- */
4456
- this.allPrevSegments = allPrevSegments;
4457
- /**
4458
- * A flag which shows this is reachable.
4459
- * @type {boolean}
4460
- */
4461
- this.reachable = reachable;
4462
- Object.defineProperty(this, "internal", { value: {
4463
- used: false,
4464
- loopedPrevSegments: []
4465
- } });
4466
- }
4467
- /**
4468
- * Checks a given previous segment is coming from the end of a loop.
4469
- * @param {CodePathSegment} segment A previous segment to check.
4470
- * @returns {boolean} `true` if the segment is coming from the end of a loop.
4471
- */
4472
- isLoopedPrevSegment(segment) {
4473
- return this.internal.loopedPrevSegments.includes(segment);
4474
- }
4475
- /**
4476
- * Creates the root segment.
4477
- * @param {string} id An identifier.
4478
- * @returns {CodePathSegment} The created segment.
4479
- */
4480
- static newRoot(id) {
4481
- return new CodePathSegment(id, [], true);
4482
- }
4483
- /**
4484
- * Creates a segment that follows given segments.
4485
- * @param {string} id An identifier.
4486
- * @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
4487
- * @returns {CodePathSegment} The created segment.
4488
- */
4489
- static newNext(id, allPrevSegments) {
4490
- return new CodePathSegment(id, CodePathSegment.flattenUnusedSegments(allPrevSegments), allPrevSegments.some(isReachable$1));
4491
- }
4492
- /**
4493
- * Creates an unreachable segment that follows given segments.
4494
- * @param {string} id An identifier.
4495
- * @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
4496
- * @returns {CodePathSegment} The created segment.
4497
- */
4498
- static newUnreachable(id, allPrevSegments) {
4499
- const segment = new CodePathSegment(id, CodePathSegment.flattenUnusedSegments(allPrevSegments), false);
4500
- CodePathSegment.markUsed(segment);
4501
- return segment;
4502
- }
4503
- /**
4504
- * Creates a segment that follows given segments.
4505
- * This factory method does not connect with `allPrevSegments`.
4506
- * But this inherits `reachable` flag.
4507
- * @param {string} id An identifier.
4508
- * @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
4509
- * @returns {CodePathSegment} The created segment.
4510
- */
4511
- static newDisconnected(id, allPrevSegments) {
4512
- return new CodePathSegment(id, [], allPrevSegments.some(isReachable$1));
4513
- }
4514
- /**
4515
- * Makes a given segment being used.
4516
- *
4517
- * And this function registers the segment into the previous segments as a next.
4518
- * @param {CodePathSegment} segment A segment to mark.
4519
- * @returns {void}
4520
- */
4521
- static markUsed(segment) {
4522
- if (segment.internal.used) return;
4523
- segment.internal.used = true;
4524
- let i;
4525
- if (segment.reachable) for (i = 0; i < segment.allPrevSegments.length; ++i) {
4526
- const prevSegment = segment.allPrevSegments[i];
4527
- prevSegment.allNextSegments.push(segment);
4528
- prevSegment.nextSegments.push(segment);
4529
- }
4530
- else for (i = 0; i < segment.allPrevSegments.length; ++i) segment.allPrevSegments[i].allNextSegments.push(segment);
4531
- }
4532
- /**
4533
- * Marks a previous segment as looped.
4534
- * @param {CodePathSegment} segment A segment.
4535
- * @param {CodePathSegment} prevSegment A previous segment to mark.
4536
- * @returns {void}
4537
- */
4538
- static markPrevSegmentAsLooped(segment, prevSegment) {
4539
- segment.internal.loopedPrevSegments.push(prevSegment);
4540
- }
4541
- /**
4542
- * Replaces unused segments with the previous segments of each unused segment.
4543
- * @param {CodePathSegment[]} segments An array of segments to replace.
4544
- * @returns {CodePathSegment[]} The replaced array.
4545
- */
4546
- static flattenUnusedSegments(segments) {
4547
- const done = Object.create(null);
4548
- const retv = [];
4549
- for (let i = 0; i < segments.length; ++i) {
4550
- const segment = segments[i];
4551
- if (done[segment.id]) continue;
4552
- if (!segment.internal.used) for (let j = 0; j < segment.allPrevSegments.length; ++j) {
4553
- const prevSegment = segment.allPrevSegments[j];
4554
- if (!done[prevSegment.id]) {
4555
- done[prevSegment.id] = true;
4556
- retv.push(prevSegment);
4557
- }
4558
- }
4559
- else {
4560
- done[segment.id] = true;
4561
- retv.push(segment);
4562
- }
4563
- }
4564
- return retv;
4565
- }
4566
- };
4567
-
4568
- //#endregion
4569
- //#region src/utils/code-path-analysis/fork-context.js
4570
- /**
4571
- * Gets whether or not a given segment is reachable.
4572
- * @param {CodePathSegment} segment A segment to get.
4573
- * @returns {boolean} `true` if the segment is reachable.
4406
+ * We consider hooks to be a hook name identifier or a member expression
4407
+ * containing a hook name.
4574
4408
  */
4575
- function isReachable(segment) {
4576
- return segment.reachable;
4409
+ function isHook(node) {
4410
+ if (node.type === "Identifier") return isHookName(node.name);
4411
+ else if (node.type === "MemberExpression" && !node.computed && isHook(node.property)) {
4412
+ const obj = node.object;
4413
+ return obj.type === "Identifier" && /^[A-Z].*/.test(obj.name);
4414
+ } else return false;
4577
4415
  }
4578
4416
  /**
4579
- * Creates new segments from the specific range of `context.segmentsList`.
4580
- *
4581
- * When `context.segmentsList` is `[[a, b], [c, d], [e, f]]`, `begin` is `0`, and
4582
- * `end` is `-1`, this creates `[g, h]`. This `g` is from `a`, `c`, and `e`.
4583
- * This `h` is from `b`, `d`, and `f`.
4584
- * @param {ForkContext} context An instance.
4585
- * @param {number} begin The first index of the previous segments.
4586
- * @param {number} end The last index of the previous segments.
4587
- * @param {Function} create A factory function of new segments.
4588
- * @returns {CodePathSegment[]} New segments.
4417
+ * Checks if the node is a React component name. React component names must
4418
+ * always start with an uppercase letter.
4589
4419
  */
4590
- function makeSegments(context, begin, end, create) {
4591
- const list = context.segmentsList;
4592
- const normalizedBegin = begin >= 0 ? begin : list.length + begin;
4593
- const normalizedEnd = end >= 0 ? end : list.length + end;
4594
- const segments = [];
4595
- for (let i = 0; i < context.count; ++i) {
4596
- const allPrevSegments = [];
4597
- for (let j = normalizedBegin; j <= normalizedEnd; ++j) allPrevSegments.push(list[j][i]);
4598
- segments.push(create(context.idGenerator.next(), allPrevSegments));
4599
- }
4600
- return segments;
4420
+ function isComponentName(node) {
4421
+ return node.type === "Identifier" && /^[A-Z]/.test(node.name);
4422
+ }
4423
+ function isReactFunction(node, functionName) {
4424
+ return "name" in node && node.name === functionName || node.type === "MemberExpression" && "name" in node.object && node.object.name === "React" && "name" in node.property && node.property.name === functionName;
4601
4425
  }
4602
4426
  /**
4603
- * `segments` becomes doubly in a `finally` block. Then if a code path exits by a
4604
- * control statement (such as `break`, `continue`) from the `finally` block, the
4605
- * destination's segments may be half of the source segments. In that case, this
4606
- * merges segments.
4607
- * @param {ForkContext} context An instance.
4608
- * @param {CodePathSegment[]} segments Segments to merge.
4609
- * @returns {CodePathSegment[]} The merged segments.
4427
+ * Checks if the node is a callback argument of forwardRef. This render function
4428
+ * should follow the rules of hooks.
4610
4429
  */
4611
- function mergeExtraSegments(context, segments) {
4612
- let currentSegments = segments;
4613
- while (currentSegments.length > context.count) {
4614
- const merged = [];
4615
- for (let i = 0, length = currentSegments.length / 2 | 0; i < length; ++i) merged.push(CodePathSegment.newNext(context.idGenerator.next(), [currentSegments[i], currentSegments[i + length]]));
4616
- currentSegments = merged;
4617
- }
4618
- return currentSegments;
4430
+ function isForwardRefCallback(node) {
4431
+ return !!(node.parent && "callee" in node.parent && node.parent.callee && isReactFunction(node.parent.callee, "forwardRef"));
4619
4432
  }
4620
4433
  /**
4621
- * A class to manage forking.
4434
+ * Checks if the node is a callback argument of React.memo. This anonymous
4435
+ * functional component should follow the rules of hooks.
4622
4436
  */
4623
- var ForkContext = class ForkContext {
4624
- /**
4625
- * @param {IdGenerator} idGenerator An identifier generator for segments.
4626
- * @param {ForkContext|null} upper An upper fork context.
4627
- * @param {number} count A number of parallel segments.
4628
- */
4629
- constructor(idGenerator, upper, count) {
4630
- this.idGenerator = idGenerator;
4631
- this.upper = upper;
4632
- this.count = count;
4633
- this.segmentsList = [];
4634
- }
4635
- /**
4636
- * The head segments.
4637
- * @type {CodePathSegment[]}
4638
- */
4639
- get head() {
4640
- const list = this.segmentsList;
4641
- return list.length === 0 ? [] : list[list.length - 1];
4642
- }
4643
- /**
4644
- * A flag which shows empty.
4645
- * @type {boolean}
4646
- */
4647
- get empty() {
4648
- return this.segmentsList.length === 0;
4649
- }
4650
- /**
4651
- * A flag which shows reachable.
4652
- * @type {boolean}
4653
- */
4654
- get reachable() {
4655
- const segments = this.head;
4656
- return segments.length > 0 && segments.some(isReachable);
4657
- }
4658
- /**
4659
- * Creates new segments from this context.
4660
- * @param {number} begin The first index of previous segments.
4661
- * @param {number} end The last index of previous segments.
4662
- * @returns {CodePathSegment[]} New segments.
4663
- */
4664
- makeNext(begin, end) {
4665
- return makeSegments(this, begin, end, CodePathSegment.newNext);
4666
- }
4667
- /**
4668
- * Creates new segments from this context.
4669
- * The new segments is always unreachable.
4670
- * @param {number} begin The first index of previous segments.
4671
- * @param {number} end The last index of previous segments.
4672
- * @returns {CodePathSegment[]} New segments.
4673
- */
4674
- makeUnreachable(begin, end) {
4675
- return makeSegments(this, begin, end, CodePathSegment.newUnreachable);
4676
- }
4677
- /**
4678
- * Creates new segments from this context.
4679
- * The new segments don't have connections for previous segments.
4680
- * But these inherit the reachable flag from this context.
4681
- * @param {number} begin The first index of previous segments.
4682
- * @param {number} end The last index of previous segments.
4683
- * @returns {CodePathSegment[]} New segments.
4684
- */
4685
- makeDisconnected(begin, end) {
4686
- return makeSegments(this, begin, end, CodePathSegment.newDisconnected);
4687
- }
4688
- /**
4689
- * Adds segments into this context.
4690
- * The added segments become the head.
4691
- * @param {CodePathSegment[]} segments Segments to add.
4692
- * @returns {void}
4693
- */
4694
- add(segments) {
4695
- assert(segments.length >= this.count, `${segments.length} >= ${this.count}`);
4696
- this.segmentsList.push(mergeExtraSegments(this, segments));
4697
- }
4698
- /**
4699
- * Replaces the head segments with given segments.
4700
- * The current head segments are removed.
4701
- * @param {CodePathSegment[]} segments Segments to add.
4702
- * @returns {void}
4703
- */
4704
- replaceHead(segments) {
4705
- assert(segments.length >= this.count, `${segments.length} >= ${this.count}`);
4706
- this.segmentsList.splice(-1, 1, mergeExtraSegments(this, segments));
4707
- }
4708
- /**
4709
- * Adds all segments of a given fork context into this context.
4710
- * @param {ForkContext} context A fork context to add.
4711
- * @returns {void}
4712
- */
4713
- addAll(context) {
4714
- assert(context.count === this.count);
4715
- const source = context.segmentsList;
4716
- for (let i = 0; i < source.length; ++i) this.segmentsList.push(source[i]);
4717
- }
4718
- /**
4719
- * Clears all segments in this context.
4720
- * @returns {void}
4721
- */
4722
- clear() {
4723
- this.segmentsList = [];
4724
- }
4725
- /**
4726
- * Creates the root fork context.
4727
- * @param {IdGenerator} idGenerator An identifier generator for segments.
4728
- * @returns {ForkContext} New fork context.
4729
- */
4730
- static newRoot(idGenerator) {
4731
- const context = new ForkContext(idGenerator, null, 1);
4732
- context.add([CodePathSegment.newRoot(idGenerator.next())]);
4733
- return context;
4734
- }
4735
- /**
4736
- * Creates an empty fork context preceded by a given context.
4737
- * @param {ForkContext} parentContext The parent fork context.
4738
- * @param {boolean} forkLeavingPath A flag which shows inside of `finally` block.
4739
- * @returns {ForkContext} New fork context.
4740
- */
4741
- static newEmpty(parentContext, forkLeavingPath) {
4742
- return new ForkContext(parentContext.idGenerator, parentContext, (forkLeavingPath ? 2 : 1) * parentContext.count);
4743
- }
4744
- };
4745
-
4746
- //#endregion
4747
- //#region src/utils/code-path-analysis/code-path-state.js
4748
- /**
4749
- * Adds given segments into the `dest` array.
4750
- * If the `others` array does not includes the given segments, adds to the `all`
4751
- * array as well.
4752
- *
4753
- * This adds only reachable and used segments.
4754
- * @param {CodePathSegment[]} dest A destination array (`returnedSegments` or `thrownSegments`).
4755
- * @param {CodePathSegment[]} others Another destination array (`returnedSegments` or `thrownSegments`).
4756
- * @param {CodePathSegment[]} all The unified destination array (`finalSegments`).
4757
- * @param {CodePathSegment[]} segments Segments to add.
4758
- * @returns {void}
4759
- */
4760
- function addToReturnedOrThrown(dest, others, all, segments) {
4761
- for (let i = 0; i < segments.length; ++i) {
4762
- const segment = segments[i];
4763
- dest.push(segment);
4764
- if (!others.includes(segment)) all.push(segment);
4437
+ function isMemoCallback(node) {
4438
+ return !!(node.parent && "callee" in node.parent && node.parent.callee && isReactFunction(node.parent.callee, "memo"));
4439
+ }
4440
+ function isInsideComponentOrHook(node) {
4441
+ while (node) {
4442
+ const functionName = getFunctionName(node);
4443
+ if (functionName) {
4444
+ if (isComponentName(functionName) || isHook(functionName)) return true;
4445
+ }
4446
+ if (isForwardRefCallback(node) || isMemoCallback(node)) return true;
4447
+ node = node.parent;
4765
4448
  }
4449
+ return false;
4766
4450
  }
4767
- /**
4768
- * Gets a loop-context for a `continue` statement.
4769
- * @param {CodePathState} state A state to get.
4770
- * @param {string} label The label of a `continue` statement.
4771
- * @returns {LoopContext} A loop-context for a `continue` statement.
4772
- */
4773
- function getContinueContext(state, label) {
4774
- if (!label) return state.loopContext;
4775
- let context = state.loopContext;
4776
- while (context) {
4777
- if (context.label === label) return context;
4778
- context = context.upper;
4451
+ function isInsideDoWhileLoop(node) {
4452
+ while (node) {
4453
+ if (node.type === "DoWhileStatement") return true;
4454
+ node = node.parent;
4779
4455
  }
4780
- /* c8 ignore next */
4781
- return null;
4456
+ return false;
4782
4457
  }
4783
- /**
4784
- * Gets a context for a `break` statement.
4785
- * @param {CodePathState} state A state to get.
4786
- * @param {string} label The label of a `break` statement.
4787
- * @returns {LoopContext|SwitchContext} A context for a `break` statement.
4788
- */
4789
- function getBreakContext(state, label) {
4790
- let context = state.breakContext;
4791
- while (context) {
4792
- if (label ? context.label === label : context.breakable) return context;
4793
- context = context.upper;
4458
+ function isInsideTryCatch(node) {
4459
+ while (node) {
4460
+ if (node.type === "TryStatement" || node.type === "CatchClause") return true;
4461
+ node = node.parent;
4794
4462
  }
4795
- /* c8 ignore next */
4796
- return null;
4463
+ return false;
4797
4464
  }
4798
- /**
4799
- * Gets a context for a `return` statement.
4800
- * @param {CodePathState} state A state to get.
4801
- * @returns {TryContext|CodePathState} A context for a `return` statement.
4802
- */
4803
- function getReturnContext(state) {
4804
- let context = state.tryContext;
4805
- while (context) {
4806
- if (context.hasFinalizer && context.position !== "finally") return context;
4807
- context = context.upper;
4808
- }
4809
- return state;
4465
+ function getNodeWithoutReactNamespace(node) {
4466
+ if (node.type === "MemberExpression" && node.object.type === "Identifier" && node.object.name === "React" && node.property.type === "Identifier" && !node.computed) return node.property;
4467
+ return node;
4810
4468
  }
4811
- /**
4812
- * Gets a context for a `throw` statement.
4813
- * @param {CodePathState} state A state to get.
4814
- * @returns {TryContext|CodePathState} A context for a `throw` statement.
4815
- */
4816
- function getThrowContext(state) {
4817
- let context = state.tryContext;
4818
- while (context) {
4819
- if (context.position === "try" || context.hasFinalizer && context.position === "catch") return context;
4820
- context = context.upper;
4821
- }
4822
- return state;
4469
+ function isEffectIdentifier(node, additionalHooks) {
4470
+ if (node.type === "Identifier" && (node.name === "useEffect" || node.name === "useLayoutEffect" || node.name === "useInsertionEffect")) return true;
4471
+ if (additionalHooks && node.type === "Identifier") return additionalHooks.test(node.name);
4472
+ return false;
4823
4473
  }
4824
- /**
4825
- * Removes a given element from a given array.
4826
- * @param {any[]} xs An array to remove the specific element.
4827
- * @param {any} x An element to be removed.
4828
- * @returns {void}
4829
- */
4830
- function remove(xs, x) {
4831
- xs.splice(xs.indexOf(x), 1);
4474
+ function isUseEffectEventIdentifier(node) {
4475
+ return node.type === "Identifier" && node.name === "useEffectEvent";
4832
4476
  }
4833
- /**
4834
- * Disconnect given segments.
4835
- *
4836
- * This is used in a process for switch statements.
4837
- * If there is the "default" chunk before other cases, the order is different
4838
- * between node's and running's.
4839
- * @param {CodePathSegment[]} prevSegments Forward segments to disconnect.
4840
- * @param {CodePathSegment[]} nextSegments Backward segments to disconnect.
4841
- * @returns {void}
4842
- */
4843
- function removeConnection(prevSegments, nextSegments) {
4844
- for (let i = 0; i < prevSegments.length; ++i) {
4845
- const prevSegment = prevSegments[i];
4846
- const nextSegment = nextSegments[i];
4847
- remove(prevSegment.nextSegments, nextSegment);
4848
- remove(prevSegment.allNextSegments, nextSegment);
4849
- remove(nextSegment.prevSegments, prevSegment);
4850
- remove(nextSegment.allPrevSegments, prevSegment);
4851
- }
4477
+ function useEffectEventError(fn, called) {
4478
+ if (fn === null) return "React Hook \"useEffectEvent\" can only be called at the top level of your component. It cannot be passed down.";
4479
+ return `\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from Effects and Effect Events in the same component.` + (called ? "" : " It cannot be assigned to a variable or passed down.");
4852
4480
  }
4853
- /**
4854
- * Creates looping path.
4855
- * @param {CodePathState} state The instance.
4856
- * @param {CodePathSegment[]} unflattenedFromSegments Segments which are source.
4857
- * @param {CodePathSegment[]} unflattenedToSegments Segments which are destination.
4858
- * @returns {void}
4859
- */
4860
- function makeLooped(state, unflattenedFromSegments, unflattenedToSegments) {
4861
- const fromSegments = CodePathSegment.flattenUnusedSegments(unflattenedFromSegments);
4862
- const toSegments = CodePathSegment.flattenUnusedSegments(unflattenedToSegments);
4863
- const end = Math.min(fromSegments.length, toSegments.length);
4864
- for (let i = 0; i < end; ++i) {
4865
- const fromSegment = fromSegments[i];
4866
- const toSegment = toSegments[i];
4867
- if (toSegment.reachable) fromSegment.nextSegments.push(toSegment);
4868
- if (fromSegment.reachable) toSegment.prevSegments.push(fromSegment);
4869
- fromSegment.allNextSegments.push(toSegment);
4870
- toSegment.allPrevSegments.push(fromSegment);
4871
- if (toSegment.allPrevSegments.length >= 2) CodePathSegment.markPrevSegmentAsLooped(toSegment, fromSegment);
4872
- state.notifyLooped(fromSegment, toSegment);
4873
- }
4481
+ function isUseIdentifier(node) {
4482
+ return isReactFunction(node, "use");
4874
4483
  }
4875
- /**
4876
- * Finalizes segments of `test` chunk of a ForStatement.
4877
- *
4878
- * - Adds `false` paths to paths which are leaving from the loop.
4879
- * - Sets `true` paths to paths which go to the body.
4880
- * @param {LoopContext} context A loop context to modify.
4881
- * @param {ChoiceContext} choiceContext A choice context of this loop.
4882
- * @param {CodePathSegment[]} head The current head paths.
4883
- * @returns {void}
4884
- */
4885
- function finalizeTestSegmentsOfFor(context, choiceContext, head) {
4886
- if (!choiceContext.processed) {
4887
- choiceContext.trueForkContext.add(head);
4888
- choiceContext.falseForkContext.add(head);
4889
- choiceContext.qqForkContext.add(head);
4890
- }
4891
- if (context.test !== true) context.brokenForkContext.addAll(choiceContext.falseForkContext);
4892
- context.endOfTestSegments = choiceContext.trueForkContext.makeNext(0, -1);
4893
- }
4894
- /**
4895
- * A class which manages state to analyze code paths.
4896
- */
4897
- var CodePathState = class {
4898
- /**
4899
- * @param {IdGenerator} idGenerator An id generator to generate id for code
4900
- * path segments.
4901
- * @param {Function} onLooped A callback function to notify looping.
4902
- */
4903
- constructor(idGenerator, onLooped) {
4904
- this.idGenerator = idGenerator;
4905
- this.notifyLooped = onLooped;
4906
- this.forkContext = ForkContext.newRoot(idGenerator);
4907
- this.choiceContext = null;
4908
- this.switchContext = null;
4909
- this.tryContext = null;
4910
- this.loopContext = null;
4911
- this.breakContext = null;
4912
- this.chainContext = null;
4913
- this.currentSegments = [];
4914
- this.initialSegment = this.forkContext.head[0];
4915
- const final = this.finalSegments = [];
4916
- const returned = this.returnedForkContext = [];
4917
- const thrown = this.thrownForkContext = [];
4918
- returned.add = addToReturnedOrThrown.bind(null, returned, thrown, final);
4919
- thrown.add = addToReturnedOrThrown.bind(null, thrown, returned, final);
4920
- }
4921
- /**
4922
- * The head segments.
4923
- * @type {CodePathSegment[]}
4924
- */
4925
- get headSegments() {
4926
- return this.forkContext.head;
4927
- }
4928
- /**
4929
- * The parent forking context.
4930
- * This is used for the root of new forks.
4931
- * @type {ForkContext}
4932
- */
4933
- get parentForkContext() {
4934
- const current = this.forkContext;
4935
- return current && current.upper;
4936
- }
4937
- /**
4938
- * Creates and stacks new forking context.
4939
- * @param {boolean} forkLeavingPath A flag which shows being in a
4940
- * "finally" block.
4941
- * @returns {ForkContext} The created context.
4942
- */
4943
- pushForkContext(forkLeavingPath) {
4944
- this.forkContext = ForkContext.newEmpty(this.forkContext, forkLeavingPath);
4945
- return this.forkContext;
4946
- }
4947
- /**
4948
- * Pops and merges the last forking context.
4949
- * @returns {ForkContext} The last context.
4950
- */
4951
- popForkContext() {
4952
- const lastContext = this.forkContext;
4953
- this.forkContext = lastContext.upper;
4954
- this.forkContext.replaceHead(lastContext.makeNext(0, -1));
4955
- return lastContext;
4956
- }
4957
- /**
4958
- * Creates a new path.
4959
- * @returns {void}
4960
- */
4961
- forkPath() {
4962
- this.forkContext.add(this.parentForkContext.makeNext(-1, -1));
4963
- }
4964
- /**
4965
- * Creates a bypass path.
4966
- * This is used for such as IfStatement which does not have "else" chunk.
4967
- * @returns {void}
4968
- */
4969
- forkBypassPath() {
4970
- this.forkContext.add(this.parentForkContext.head);
4971
- }
4972
- /**
4973
- * Creates a context for ConditionalExpression, LogicalExpression, AssignmentExpression (logical assignments only),
4974
- * IfStatement, WhileStatement, DoWhileStatement, or ForStatement.
4975
- *
4976
- * LogicalExpressions have cases that it goes different paths between the
4977
- * `true` case and the `false` case.
4978
- *
4979
- * For Example:
4980
- *
4981
- * if (a || b) {
4982
- * foo();
4983
- * } else {
4984
- * bar();
4985
- * }
4986
- *
4987
- * In this case, `b` is evaluated always in the code path of the `else`
4988
- * block, but it's not so in the code path of the `if` block.
4989
- * So there are 3 paths.
4990
- *
4991
- * a -> foo();
4992
- * a -> b -> foo();
4993
- * a -> b -> bar();
4994
- * @param {string} kind A kind string.
4995
- * If the new context is LogicalExpression's or AssignmentExpression's, this is `"&&"` or `"||"` or `"??"`.
4996
- * If it's IfStatement's or ConditionalExpression's, this is `"test"`.
4997
- * Otherwise, this is `"loop"`.
4998
- * @param {boolean} isForkingAsResult A flag that shows that goes different
4999
- * paths between `true` and `false`.
5000
- * @returns {void}
5001
- */
5002
- pushChoiceContext(kind, isForkingAsResult) {
5003
- this.choiceContext = {
5004
- upper: this.choiceContext,
5005
- kind,
5006
- isForkingAsResult,
5007
- trueForkContext: ForkContext.newEmpty(this.forkContext),
5008
- falseForkContext: ForkContext.newEmpty(this.forkContext),
5009
- qqForkContext: ForkContext.newEmpty(this.forkContext),
5010
- processed: false
5011
- };
5012
- }
5013
- /**
5014
- * Pops the last choice context and finalizes it.
5015
- * @throws {Error} (Unreachable.)
5016
- * @returns {ChoiceContext} The popped context.
5017
- */
5018
- popChoiceContext() {
5019
- const context = this.choiceContext;
5020
- this.choiceContext = context.upper;
5021
- const forkContext = this.forkContext;
5022
- const headSegments = forkContext.head;
5023
- switch (context.kind) {
5024
- case "&&":
5025
- case "||":
5026
- case "??":
5027
- if (!context.processed) {
5028
- context.trueForkContext.add(headSegments);
5029
- context.falseForkContext.add(headSegments);
5030
- context.qqForkContext.add(headSegments);
5031
- }
5032
- if (context.isForkingAsResult) {
5033
- const parentContext = this.choiceContext;
5034
- parentContext.trueForkContext.addAll(context.trueForkContext);
5035
- parentContext.falseForkContext.addAll(context.falseForkContext);
5036
- parentContext.qqForkContext.addAll(context.qqForkContext);
5037
- parentContext.processed = true;
5038
- return context;
5039
- }
5040
- break;
5041
- case "test":
5042
- if (!context.processed) {
5043
- context.trueForkContext.clear();
5044
- context.trueForkContext.add(headSegments);
5045
- } else {
5046
- context.falseForkContext.clear();
5047
- context.falseForkContext.add(headSegments);
4484
+ const rule = {
4485
+ meta: {
4486
+ type: "problem",
4487
+ docs: {
4488
+ description: "Enforces the Rules of Hooks.",
4489
+ recommended: true,
4490
+ url: "https://react.dev/reference/rules/rules-of-hooks"
4491
+ },
4492
+ schema: [{
4493
+ type: "object",
4494
+ additionalProperties: false,
4495
+ properties: { additionalHooks: { type: "string" } }
4496
+ }]
4497
+ },
4498
+ create(context) {
4499
+ context.settings;
4500
+ const rawOptions = context.options && context.options[0];
4501
+ const additionalEffectHooks = rawOptions && rawOptions.additionalHooks ? new RegExp(rawOptions.additionalHooks) : getSettingsFromContext(context).additionalEffectHooks;
4502
+ let lastEffect = null;
4503
+ const codePathReactHooksMapStack = [];
4504
+ const codePathSegmentStack = [];
4505
+ const useEffectEventFunctions = /* @__PURE__ */ new WeakSet();
4506
+ function recordAllUseEffectEventFunctions(scope) {
4507
+ for (const reference of scope.references) {
4508
+ const parent = reference.identifier.parent;
4509
+ if (parent?.type === "VariableDeclarator" && parent.init && parent.init.type === "CallExpression" && parent.init.callee && isUseEffectEventIdentifier(parent.init.callee)) {
4510
+ if (reference.resolved === null) throw new Error("Unexpected null reference.resolved");
4511
+ for (const ref of reference.resolved.references) if (ref !== reference) useEffectEventFunctions.add(ref.identifier);
5048
4512
  }
5049
- break;
5050
- case "loop": return context;
5051
- default: throw new Error("unreachable");
5052
- }
5053
- const prevForkContext = context.trueForkContext;
5054
- prevForkContext.addAll(context.falseForkContext);
5055
- forkContext.replaceHead(prevForkContext.makeNext(0, -1));
5056
- return context;
5057
- }
5058
- /**
5059
- * Makes a code path segment of the right-hand operand of a logical
5060
- * expression.
5061
- * @throws {Error} (Unreachable.)
5062
- * @returns {void}
5063
- */
5064
- makeLogicalRight() {
5065
- const context = this.choiceContext;
5066
- const forkContext = this.forkContext;
5067
- if (context.processed) {
5068
- let prevForkContext;
5069
- switch (context.kind) {
5070
- case "&&":
5071
- prevForkContext = context.trueForkContext;
5072
- break;
5073
- case "||":
5074
- prevForkContext = context.falseForkContext;
5075
- break;
5076
- case "??":
5077
- prevForkContext = context.qqForkContext;
5078
- break;
5079
- default: throw new Error("unreachable");
5080
- }
5081
- forkContext.replaceHead(prevForkContext.makeNext(0, -1));
5082
- prevForkContext.clear();
5083
- context.processed = false;
5084
- } else {
5085
- switch (context.kind) {
5086
- case "&&":
5087
- context.falseForkContext.add(forkContext.head);
5088
- break;
5089
- case "||":
5090
- context.trueForkContext.add(forkContext.head);
5091
- break;
5092
- case "??":
5093
- context.trueForkContext.add(forkContext.head);
5094
- context.falseForkContext.add(forkContext.head);
5095
- break;
5096
- default: throw new Error("unreachable");
5097
4513
  }
5098
- forkContext.replaceHead(forkContext.makeNext(-1, -1));
5099
4514
  }
5100
- }
5101
- /**
5102
- * Makes a code path segment of the `if` block.
5103
- * @returns {void}
5104
- */
5105
- makeIfConsequent() {
5106
- const context = this.choiceContext;
5107
- const forkContext = this.forkContext;
5108
- if (!context.processed) {
5109
- context.trueForkContext.add(forkContext.head);
5110
- context.falseForkContext.add(forkContext.head);
5111
- context.qqForkContext.add(forkContext.head);
5112
- }
5113
- context.processed = false;
5114
- forkContext.replaceHead(context.trueForkContext.makeNext(0, -1));
5115
- }
5116
- /**
5117
- * Makes a code path segment of the `else` block.
5118
- * @returns {void}
5119
- */
5120
- makeIfAlternate() {
5121
- const context = this.choiceContext;
5122
- const forkContext = this.forkContext;
5123
- context.trueForkContext.clear();
5124
- context.trueForkContext.add(forkContext.head);
5125
- context.processed = true;
5126
- forkContext.replaceHead(context.falseForkContext.makeNext(0, -1));
5127
- }
5128
- /**
5129
- * Push a new `ChainExpression` context to the stack.
5130
- * This method is called on entering to each `ChainExpression` node.
5131
- * This context is used to count forking in the optional chain then merge them on the exiting from the `ChainExpression` node.
5132
- * @returns {void}
5133
- */
5134
- pushChainContext() {
5135
- this.chainContext = {
5136
- upper: this.chainContext,
5137
- countChoiceContexts: 0
5138
- };
5139
- }
5140
- /**
5141
- * Pop a `ChainExpression` context from the stack.
5142
- * This method is called on exiting from each `ChainExpression` node.
5143
- * This merges all forks of the last optional chaining.
5144
- * @returns {void}
5145
- */
5146
- popChainContext() {
5147
- const context = this.chainContext;
5148
- this.chainContext = context.upper;
5149
- for (let i = context.countChoiceContexts; i > 0; --i) this.popChoiceContext();
5150
- }
5151
- /**
5152
- * Create a choice context for optional access.
5153
- * This method is called on entering to each `(Call|Member)Expression[optional=true]` node.
5154
- * This creates a choice context as similar to `LogicalExpression[operator="??"]` node.
5155
- * @returns {void}
5156
- */
5157
- makeOptionalNode() {
5158
- if (this.chainContext) {
5159
- this.chainContext.countChoiceContexts += 1;
5160
- this.pushChoiceContext("??", false);
5161
- }
5162
- }
5163
- /**
5164
- * Create a fork.
5165
- * This method is called on entering to the `arguments|property` property of each `(Call|Member)Expression` node.
5166
- * @returns {void}
5167
- */
5168
- makeOptionalRight() {
5169
- if (this.chainContext) this.makeLogicalRight();
5170
- }
5171
- /**
5172
- * Creates a context object of SwitchStatement and stacks it.
5173
- * @param {boolean} hasCase `true` if the switch statement has one or more
5174
- * case parts.
5175
- * @param {string|null} label The label text.
5176
- * @returns {void}
5177
- */
5178
- pushSwitchContext(hasCase, label) {
5179
- this.switchContext = {
5180
- upper: this.switchContext,
5181
- hasCase,
5182
- defaultSegments: null,
5183
- defaultBodySegments: null,
5184
- foundDefault: false,
5185
- lastIsDefault: false,
5186
- countForks: 0
4515
+ /**
4516
+ * SourceCode that also works down to ESLint 3.0.0
4517
+ */
4518
+ const getSourceCode = typeof context.getSourceCode === "function" ? () => {
4519
+ return context.getSourceCode();
4520
+ } : () => {
4521
+ return context.sourceCode;
5187
4522
  };
5188
- this.pushBreakContext(true, label);
5189
- }
5190
- /**
5191
- * Pops the last context of SwitchStatement and finalizes it.
5192
- *
5193
- * - Disposes all forking stack for `case` and `default`.
5194
- * - Creates the next code path segment from `context.brokenForkContext`.
5195
- * - If the last `SwitchCase` node is not a `default` part, creates a path
5196
- * to the `default` body.
5197
- * @returns {void}
5198
- */
5199
- popSwitchContext() {
5200
- const context = this.switchContext;
5201
- this.switchContext = context.upper;
5202
- const forkContext = this.forkContext;
5203
- const brokenForkContext = this.popBreakContext().brokenForkContext;
5204
- if (context.countForks === 0) {
5205
- if (!brokenForkContext.empty) {
5206
- brokenForkContext.add(forkContext.makeNext(-1, -1));
5207
- forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
5208
- }
5209
- return;
5210
- }
5211
- const lastSegments = forkContext.head;
5212
- this.forkBypassPath();
5213
- const lastCaseSegments = forkContext.head;
5214
- brokenForkContext.add(lastSegments);
5215
- if (!context.lastIsDefault) if (context.defaultBodySegments) {
5216
- removeConnection(context.defaultSegments, context.defaultBodySegments);
5217
- makeLooped(this, lastCaseSegments, context.defaultBodySegments);
5218
- } else brokenForkContext.add(lastCaseSegments);
5219
- for (let i = 0; i < context.countForks; ++i) this.forkContext = this.forkContext.upper;
5220
- this.forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
5221
- }
5222
- /**
5223
- * Makes a code path segment for a `SwitchCase` node.
5224
- * @param {boolean} isEmpty `true` if the body is empty.
5225
- * @param {boolean} isDefault `true` if the body is the default case.
5226
- * @returns {void}
5227
- */
5228
- makeSwitchCaseBody(isEmpty, isDefault) {
5229
- const context = this.switchContext;
5230
- if (!context.hasCase) return;
5231
- const parentForkContext = this.forkContext;
5232
- const forkContext = this.pushForkContext();
5233
- forkContext.add(parentForkContext.makeNext(0, -1));
5234
- if (isDefault) {
5235
- context.defaultSegments = parentForkContext.head;
5236
- if (isEmpty) context.foundDefault = true;
5237
- else context.defaultBodySegments = forkContext.head;
5238
- } else if (!isEmpty && context.foundDefault) {
5239
- context.foundDefault = false;
5240
- context.defaultBodySegments = forkContext.head;
5241
- }
5242
- context.lastIsDefault = isDefault;
5243
- context.countForks += 1;
5244
- }
5245
- /**
5246
- * Creates a context object of TryStatement and stacks it.
5247
- * @param {boolean} hasFinalizer `true` if the try statement has a
5248
- * `finally` block.
5249
- * @returns {void}
5250
- */
5251
- pushTryContext(hasFinalizer) {
5252
- this.tryContext = {
5253
- upper: this.tryContext,
5254
- position: "try",
5255
- hasFinalizer,
5256
- returnedForkContext: hasFinalizer ? ForkContext.newEmpty(this.forkContext) : null,
5257
- thrownForkContext: ForkContext.newEmpty(this.forkContext),
5258
- lastOfTryIsReachable: false,
5259
- lastOfCatchIsReachable: false
4523
+ /**
4524
+ * SourceCode#getScope that also works down to ESLint 3.0.0
4525
+ */
4526
+ const getScope = typeof context.getScope === "function" ? () => {
4527
+ return context.getScope();
4528
+ } : (node) => {
4529
+ return getSourceCode().getScope(node);
5260
4530
  };
5261
- }
5262
- /**
5263
- * Pops the last context of TryStatement and finalizes it.
5264
- * @returns {void}
5265
- */
5266
- popTryContext() {
5267
- const context = this.tryContext;
5268
- this.tryContext = context.upper;
5269
- if (context.position === "catch") {
5270
- this.popForkContext();
5271
- return;
5272
- }
5273
- const returned = context.returnedForkContext;
5274
- const thrown = context.thrownForkContext;
5275
- if (returned.empty && thrown.empty) return;
5276
- const headSegments = this.forkContext.head;
5277
- this.forkContext = this.forkContext.upper;
5278
- const normalSegments = headSegments.slice(0, headSegments.length / 2 | 0);
5279
- const leavingSegments = headSegments.slice(headSegments.length / 2 | 0);
5280
- if (!returned.empty) getReturnContext(this).returnedForkContext.add(leavingSegments);
5281
- if (!thrown.empty) getThrowContext(this).thrownForkContext.add(leavingSegments);
5282
- this.forkContext.replaceHead(normalSegments);
5283
- if (!context.lastOfTryIsReachable && !context.lastOfCatchIsReachable) this.forkContext.makeUnreachable();
5284
- }
5285
- /**
5286
- * Makes a code path segment for a `catch` block.
5287
- * @returns {void}
5288
- */
5289
- makeCatchBlock() {
5290
- const context = this.tryContext;
5291
- const forkContext = this.forkContext;
5292
- const thrown = context.thrownForkContext;
5293
- context.position = "catch";
5294
- context.thrownForkContext = ForkContext.newEmpty(forkContext);
5295
- context.lastOfTryIsReachable = forkContext.reachable;
5296
- thrown.add(forkContext.head);
5297
- const thrownSegments = thrown.makeNext(0, -1);
5298
- this.pushForkContext();
5299
- this.forkBypassPath();
5300
- this.forkContext.add(thrownSegments);
5301
- }
5302
- /**
5303
- * Makes a code path segment for a `finally` block.
5304
- *
5305
- * In the `finally` block, parallel paths are created. The parallel paths
5306
- * are used as leaving-paths. The leaving-paths are paths from `return`
5307
- * statements and `throw` statements in a `try` block or a `catch` block.
5308
- * @returns {void}
5309
- */
5310
- makeFinallyBlock() {
5311
- const context = this.tryContext;
5312
- let forkContext = this.forkContext;
5313
- const returned = context.returnedForkContext;
5314
- const thrown = context.thrownForkContext;
5315
- const headOfLeavingSegments = forkContext.head;
5316
- if (context.position === "catch") {
5317
- this.popForkContext();
5318
- forkContext = this.forkContext;
5319
- context.lastOfCatchIsReachable = forkContext.reachable;
5320
- } else context.lastOfTryIsReachable = forkContext.reachable;
5321
- context.position = "finally";
5322
- if (returned.empty && thrown.empty) return;
5323
- const segments = forkContext.makeNext(-1, -1);
5324
- for (let i = 0; i < forkContext.count; ++i) {
5325
- const prevSegsOfLeavingSegment = [headOfLeavingSegments[i]];
5326
- for (let j = 0; j < returned.segmentsList.length; ++j) prevSegsOfLeavingSegment.push(returned.segmentsList[j][i]);
5327
- for (let j = 0; j < thrown.segmentsList.length; ++j) prevSegsOfLeavingSegment.push(thrown.segmentsList[j][i]);
5328
- segments.push(CodePathSegment.newNext(this.idGenerator.next(), prevSegsOfLeavingSegment));
4531
+ function hasFlowSuppression(node, suppression) {
4532
+ const comments = getSourceCode().getAllComments();
4533
+ const flowSuppressionRegex = new RegExp("\\$FlowFixMe\\[" + suppression + "\\]");
4534
+ return comments.some((commentNode) => flowSuppressionRegex.test(commentNode.value) && commentNode.loc != null && node.loc != null && commentNode.loc.end.line === node.loc.start.line - 1);
5329
4535
  }
5330
- this.pushForkContext(true);
5331
- this.forkContext.add(segments);
5332
- }
5333
- /**
5334
- * Makes a code path segment from the first throwable node to the `catch`
5335
- * block or the `finally` block.
5336
- * @returns {void}
5337
- */
5338
- makeFirstThrowablePathInTryBlock() {
5339
- const forkContext = this.forkContext;
5340
- if (!forkContext.reachable) return;
5341
- const context = getThrowContext(this);
5342
- if (context === this || context.position !== "try" || !context.thrownForkContext.empty) return;
5343
- context.thrownForkContext.add(forkContext.head);
5344
- forkContext.replaceHead(forkContext.makeNext(-1, -1));
5345
- }
5346
- /**
5347
- * Creates a context object of a loop statement and stacks it.
5348
- * @param {string} type The type of the node which was triggered. One of
5349
- * `WhileStatement`, `DoWhileStatement`, `ForStatement`, `ForInStatement`,
5350
- * and `ForStatement`.
5351
- * @param {string|null} label A label of the node which was triggered.
5352
- * @throws {Error} (Unreachable - unknown type.)
4536
+ const analyzer = new CodePathAnalyzer({
4537
+ onCodePathSegmentStart: (segment) => codePathSegmentStack.push(segment),
4538
+ onCodePathSegmentEnd: () => codePathSegmentStack.pop(),
4539
+ onCodePathStart: () => codePathReactHooksMapStack.push(/* @__PURE__ */ new Map()),
4540
+ onCodePathEnd(codePath, codePathNode) {
4541
+ const reactHooksMap = codePathReactHooksMapStack.pop();
4542
+ if (reactHooksMap?.size === 0) return;
4543
+ else if (typeof reactHooksMap === "undefined") throw new Error("Unexpected undefined reactHooksMap");
4544
+ const cyclic = /* @__PURE__ */ new Set();
4545
+ /**
4546
+ * Count the number of code paths from the start of the function to this
4547
+ * segment. For example:
4548
+ *
4549
+ * ```js
4550
+ * function MyComponent() {
4551
+ * if (condition) {
4552
+ * // Segment 1
4553
+ * } else {
4554
+ * // Segment 2
4555
+ * }
4556
+ * // Segment 3
4557
+ * }
4558
+ * ```
4559
+ *
4560
+ * Segments 1 and 2 have one path to the beginning of `MyComponent` and
4561
+ * segment 3 has two paths to the beginning of `MyComponent` since we
4562
+ * could have either taken the path of segment 1 or segment 2.
4563
+ *
4564
+ * Populates `cyclic` with cyclic segments.
4565
+ */
4566
+ function countPathsFromStart(segment, pathHistory) {
4567
+ const { cache } = countPathsFromStart;
4568
+ let paths = cache.get(segment.id);
4569
+ const pathList = new Set(pathHistory);
4570
+ if (pathList.has(segment.id)) {
4571
+ const pathArray = [...pathList];
4572
+ const cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
4573
+ for (const cyclicSegment of cyclicSegments) cyclic.add(cyclicSegment);
4574
+ return BigInt("0");
4575
+ }
4576
+ pathList.add(segment.id);
4577
+ if (paths !== void 0) return paths;
4578
+ if (codePath.thrownSegments.includes(segment)) paths = BigInt("0");
4579
+ else if (segment.prevSegments.length === 0) paths = BigInt("1");
4580
+ else {
4581
+ paths = BigInt("0");
4582
+ for (const prevSegment of segment.prevSegments) paths += countPathsFromStart(prevSegment, pathList);
4583
+ }
4584
+ if (segment.reachable && paths === BigInt("0")) cache.delete(segment.id);
4585
+ else cache.set(segment.id, paths);
4586
+ return paths;
4587
+ }
4588
+ /**
4589
+ * Count the number of code paths from this segment to the end of the
4590
+ * function. For example:
4591
+ *
4592
+ * ```js
4593
+ * function MyComponent() {
4594
+ * // Segment 1
4595
+ * if (condition) {
4596
+ * // Segment 2
4597
+ * } else {
4598
+ * // Segment 3
4599
+ * }
4600
+ * }
4601
+ * ```
4602
+ *
4603
+ * Segments 2 and 3 have one path to the end of `MyComponent` and
4604
+ * segment 1 has two paths to the end of `MyComponent` since we could
4605
+ * either take the path of segment 1 or segment 2.
4606
+ *
4607
+ * Populates `cyclic` with cyclic segments.
4608
+ */
4609
+ function countPathsToEnd(segment, pathHistory) {
4610
+ const { cache } = countPathsToEnd;
4611
+ let paths = cache.get(segment.id);
4612
+ const pathList = new Set(pathHistory);
4613
+ if (pathList.has(segment.id)) {
4614
+ const pathArray = Array.from(pathList);
4615
+ const cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
4616
+ for (const cyclicSegment of cyclicSegments) cyclic.add(cyclicSegment);
4617
+ return BigInt("0");
4618
+ }
4619
+ pathList.add(segment.id);
4620
+ if (paths !== void 0) return paths;
4621
+ if (codePath.thrownSegments.includes(segment)) paths = BigInt("0");
4622
+ else if (segment.nextSegments.length === 0) paths = BigInt("1");
4623
+ else {
4624
+ paths = BigInt("0");
4625
+ for (const nextSegment of segment.nextSegments) paths += countPathsToEnd(nextSegment, pathList);
4626
+ }
4627
+ cache.set(segment.id, paths);
4628
+ return paths;
4629
+ }
4630
+ /**
4631
+ * Gets the shortest path length to the start of a code path.
4632
+ * For example:
4633
+ *
4634
+ * ```js
4635
+ * function MyComponent() {
4636
+ * if (condition) {
4637
+ * // Segment 1
4638
+ * }
4639
+ * // Segment 2
4640
+ * }
4641
+ * ```
4642
+ *
4643
+ * There is only one path from segment 1 to the code path start. Its
4644
+ * length is one so that is the shortest path.
4645
+ *
4646
+ * There are two paths from segment 2 to the code path start. One
4647
+ * through segment 1 with a length of two and another directly to the
4648
+ * start with a length of one. The shortest path has a length of one
4649
+ * so we would return that.
4650
+ */
4651
+ function shortestPathLengthToStart(segment) {
4652
+ const { cache } = shortestPathLengthToStart;
4653
+ let length = cache.get(segment.id);
4654
+ if (length === null) return Infinity;
4655
+ if (length !== void 0) return length;
4656
+ cache.set(segment.id, null);
4657
+ if (segment.prevSegments.length === 0) length = 1;
4658
+ else {
4659
+ length = Infinity;
4660
+ for (const prevSegment of segment.prevSegments) {
4661
+ const prevLength = shortestPathLengthToStart(prevSegment);
4662
+ if (prevLength < length) length = prevLength;
4663
+ }
4664
+ length += 1;
4665
+ }
4666
+ cache.set(segment.id, length);
4667
+ return length;
4668
+ }
4669
+ countPathsFromStart.cache = /* @__PURE__ */ new Map();
4670
+ countPathsToEnd.cache = /* @__PURE__ */ new Map();
4671
+ shortestPathLengthToStart.cache = /* @__PURE__ */ new Map();
4672
+ const allPathsFromStartToEnd = countPathsToEnd(codePath.initialSegment);
4673
+ const codePathFunctionName = getFunctionName(codePathNode);
4674
+ const isSomewhereInsideComponentOrHook = isInsideComponentOrHook(codePathNode);
4675
+ const isDirectlyInsideComponentOrHook = codePathFunctionName ? isComponentName(codePathFunctionName) || isHook(codePathFunctionName) : isForwardRefCallback(codePathNode) || isMemoCallback(codePathNode);
4676
+ let shortestFinalPathLength = Infinity;
4677
+ for (const finalSegment of codePath.finalSegments) {
4678
+ if (!finalSegment.reachable) continue;
4679
+ const length = shortestPathLengthToStart(finalSegment);
4680
+ if (length < shortestFinalPathLength) shortestFinalPathLength = length;
4681
+ }
4682
+ for (const [segment, reactHooks] of reactHooksMap) {
4683
+ if (!segment.reachable) continue;
4684
+ const possiblyHasEarlyReturn = segment.nextSegments.length === 0 ? shortestFinalPathLength <= shortestPathLengthToStart(segment) : shortestFinalPathLength < shortestPathLengthToStart(segment);
4685
+ const pathsFromStartToEnd = countPathsFromStart(segment) * countPathsToEnd(segment);
4686
+ const cycled = cyclic.has(segment.id);
4687
+ for (const hook of reactHooks) {
4688
+ if (hasFlowSuppression(hook, "react-rule-hook")) continue;
4689
+ if (isUseIdentifier(hook) && isInsideTryCatch(hook)) context.report({
4690
+ node: hook,
4691
+ message: `React Hook "${getSourceCode().getText(hook)}" cannot be called in a try/catch block.`
4692
+ });
4693
+ if ((cycled || isInsideDoWhileLoop(hook)) && !isUseIdentifier(hook)) context.report({
4694
+ node: hook,
4695
+ message: `React Hook "${getSourceCode().getText(hook)}" may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render.`
4696
+ });
4697
+ if (isDirectlyInsideComponentOrHook) {
4698
+ if (codePathNode.async) context.report({
4699
+ node: hook,
4700
+ message: `React Hook "${getSourceCode().getText(hook)}" cannot be called in an async function.`
4701
+ });
4702
+ if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd && !isUseIdentifier(hook) && !isInsideDoWhileLoop(hook)) {
4703
+ const message = `React Hook "${getSourceCode().getText(hook)}" is called conditionally. React Hooks must be called in the exact same order in every component render.` + (possiblyHasEarlyReturn ? " Did you accidentally call a React Hook after an early return?" : "");
4704
+ context.report({
4705
+ node: hook,
4706
+ message
4707
+ });
4708
+ }
4709
+ } else if (codePathNode.parent != null && (codePathNode.parent.type === "MethodDefinition" || codePathNode.parent.type === "ClassProperty" || codePathNode.parent.type === "PropertyDefinition") && codePathNode.parent.value === codePathNode) {
4710
+ const message = `React Hook "${getSourceCode().getText(hook)}" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.`;
4711
+ context.report({
4712
+ node: hook,
4713
+ message
4714
+ });
4715
+ } else if (codePathFunctionName) {
4716
+ const message = `React Hook "${getSourceCode().getText(hook)}" is called in function "${getSourceCode().getText(codePathFunctionName)}" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use".`;
4717
+ context.report({
4718
+ node: hook,
4719
+ message
4720
+ });
4721
+ } else if (codePathNode.type === "Program") {
4722
+ const message = `React Hook "${getSourceCode().getText(hook)}" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function.`;
4723
+ context.report({
4724
+ node: hook,
4725
+ message
4726
+ });
4727
+ } else if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) {
4728
+ const message = `React Hook "${getSourceCode().getText(hook)}" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.`;
4729
+ context.report({
4730
+ node: hook,
4731
+ message
4732
+ });
4733
+ }
4734
+ }
4735
+ }
4736
+ }
4737
+ });
4738
+ return {
4739
+ "*"(node) {
4740
+ analyzer.enterNode(node);
4741
+ },
4742
+ "*:exit"(node) {
4743
+ analyzer.leaveNode(node);
4744
+ },
4745
+ CallExpression(node) {
4746
+ if (isHook(node.callee)) {
4747
+ const reactHooksMap = last(codePathReactHooksMapStack);
4748
+ const codePathSegment = last(codePathSegmentStack);
4749
+ let reactHooks = reactHooksMap.get(codePathSegment);
4750
+ if (!reactHooks) {
4751
+ reactHooks = [];
4752
+ reactHooksMap.set(codePathSegment, reactHooks);
4753
+ }
4754
+ reactHooks.push(node.callee);
4755
+ }
4756
+ const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee);
4757
+ if ((isEffectIdentifier(nodeWithoutNamespace, additionalEffectHooks) || isUseEffectEventIdentifier(nodeWithoutNamespace)) && node.arguments.length > 0) lastEffect = node;
4758
+ if (isUseEffectEventIdentifier(nodeWithoutNamespace) && node.parent?.type !== "VariableDeclarator" && node.parent?.type !== "ExpressionStatement") {
4759
+ const message = useEffectEventError(null, false);
4760
+ context.report({
4761
+ node,
4762
+ message
4763
+ });
4764
+ }
4765
+ },
4766
+ Identifier(node) {
4767
+ if (lastEffect == null && useEffectEventFunctions.has(node)) {
4768
+ const message = useEffectEventError(getSourceCode().getText(node), node.parent.type === "CallExpression");
4769
+ context.report({
4770
+ node,
4771
+ message
4772
+ });
4773
+ }
4774
+ },
4775
+ "CallExpression:exit"(node) {
4776
+ if (node === lastEffect) lastEffect = null;
4777
+ },
4778
+ FunctionDeclaration(node) {
4779
+ if (isInsideComponentOrHook(node)) recordAllUseEffectEventFunctions(getScope(node));
4780
+ },
4781
+ ArrowFunctionExpression(node) {
4782
+ if (isInsideComponentOrHook(node)) recordAllUseEffectEventFunctions(getScope(node));
4783
+ },
4784
+ ComponentDeclaration(node) {
4785
+ recordAllUseEffectEventFunctions(getScope(node));
4786
+ },
4787
+ HookDeclaration(node) {
4788
+ recordAllUseEffectEventFunctions(getScope(node));
4789
+ }
4790
+ };
4791
+ }
4792
+ };
4793
+ /**
4794
+ * Gets the static name of a function AST node. For function declarations it is
4795
+ * easy. For anonymous function expressions it is much harder. If you search for
4796
+ * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
4797
+ * where JS gives anonymous function expressions names. We roughly detect the
4798
+ * same AST nodes with some exceptions to better fit our use case.
4799
+ */
4800
+ function getFunctionName(node) {
4801
+ if (node.type === "ComponentDeclaration" || node.type === "HookDeclaration" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression" && node.id) return node.id;
4802
+ else if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") if (node.parent?.type === "VariableDeclarator" && node.parent.init === node) return node.parent.id;
4803
+ else if (node.parent?.type === "AssignmentExpression" && node.parent.right === node && node.parent.operator === "=") return node.parent.left;
4804
+ else if (node.parent?.type === "Property" && node.parent.value === node && !node.parent.computed) return node.parent.key;
4805
+ else if (node.parent?.type === "AssignmentPattern" && node.parent.right === node && !node.parent.computed) return node.parent.left;
4806
+ else return;
4807
+ else return;
4808
+ }
4809
+ /**
4810
+ * Convenience function for peeking the last item in a stack.
4811
+ */
4812
+ function last(array) {
4813
+ return array[array.length - 1];
4814
+ }
4815
+ function assert(cond) {
4816
+ if (!cond) throw new Error("Assertion violated.");
4817
+ }
4818
+ /**
4819
+ * Checks whether or not a given segment is reachable.
4820
+ * @param {CodePathSegment} segment A segment to check.
4821
+ * @returns {boolean} `true` if the segment is reachable.
4822
+ */
4823
+ function isReachable$1(segment) {
4824
+ return segment.reachable;
4825
+ }
4826
+ /**
4827
+ * A code path segment.
4828
+ */
4829
+ var CodePathSegment = class CodePathSegment {
4830
+ /**
4831
+ * @param {string} id An identifier.
4832
+ * @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
4833
+ * This array includes unreachable segments.
4834
+ * @param {boolean} reachable A flag which shows this is reachable.
4835
+ */
4836
+ constructor(id, allPrevSegments, reachable) {
4837
+ /**
4838
+ * The identifier of this code path.
4839
+ * Rules use it to store additional information of each rule.
4840
+ * @type {string}
4841
+ */
4842
+ this.id = id;
4843
+ /**
4844
+ * An array of the next segments.
4845
+ * @type {CodePathSegment[]}
4846
+ */
4847
+ this.nextSegments = [];
4848
+ /**
4849
+ * An array of the previous segments.
4850
+ * @type {CodePathSegment[]}
4851
+ */
4852
+ this.prevSegments = allPrevSegments.filter(isReachable$1);
4853
+ /**
4854
+ * An array of the next segments.
4855
+ * This array includes unreachable segments.
4856
+ * @type {CodePathSegment[]}
4857
+ */
4858
+ this.allNextSegments = [];
4859
+ /**
4860
+ * An array of the previous segments.
4861
+ * This array includes unreachable segments.
4862
+ * @type {CodePathSegment[]}
4863
+ */
4864
+ this.allPrevSegments = allPrevSegments;
4865
+ /**
4866
+ * A flag which shows this is reachable.
4867
+ * @type {boolean}
4868
+ */
4869
+ this.reachable = reachable;
4870
+ Object.defineProperty(this, "internal", { value: {
4871
+ used: false,
4872
+ loopedPrevSegments: []
4873
+ } });
4874
+ }
4875
+ /**
4876
+ * Checks a given previous segment is coming from the end of a loop.
4877
+ * @param {CodePathSegment} segment A previous segment to check.
4878
+ * @returns {boolean} `true` if the segment is coming from the end of a loop.
4879
+ */
4880
+ isLoopedPrevSegment(segment) {
4881
+ return this.internal.loopedPrevSegments.includes(segment);
4882
+ }
4883
+ /**
4884
+ * Creates the root segment.
4885
+ * @param {string} id An identifier.
4886
+ * @returns {CodePathSegment} The created segment.
4887
+ */
4888
+ static newRoot(id) {
4889
+ return new CodePathSegment(id, [], true);
4890
+ }
4891
+ /**
4892
+ * Creates a segment that follows given segments.
4893
+ * @param {string} id An identifier.
4894
+ * @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
4895
+ * @returns {CodePathSegment} The created segment.
4896
+ */
4897
+ static newNext(id, allPrevSegments) {
4898
+ return new CodePathSegment(id, CodePathSegment.flattenUnusedSegments(allPrevSegments), allPrevSegments.some(isReachable$1));
4899
+ }
4900
+ /**
4901
+ * Creates an unreachable segment that follows given segments.
4902
+ * @param {string} id An identifier.
4903
+ * @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
4904
+ * @returns {CodePathSegment} The created segment.
4905
+ */
4906
+ static newUnreachable(id, allPrevSegments) {
4907
+ const segment = new CodePathSegment(id, CodePathSegment.flattenUnusedSegments(allPrevSegments), false);
4908
+ CodePathSegment.markUsed(segment);
4909
+ return segment;
4910
+ }
4911
+ /**
4912
+ * Creates a segment that follows given segments.
4913
+ * This factory method does not connect with `allPrevSegments`.
4914
+ * But this inherits `reachable` flag.
4915
+ * @param {string} id An identifier.
4916
+ * @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
4917
+ * @returns {CodePathSegment} The created segment.
4918
+ */
4919
+ static newDisconnected(id, allPrevSegments) {
4920
+ return new CodePathSegment(id, [], allPrevSegments.some(isReachable$1));
4921
+ }
4922
+ /**
4923
+ * Makes a given segment being used.
4924
+ *
4925
+ * And this function registers the segment into the previous segments as a next.
4926
+ * @param {CodePathSegment} segment A segment to mark.
5353
4927
  * @returns {void}
5354
4928
  */
5355
- pushLoopContext(type, label) {
5356
- const forkContext = this.forkContext;
5357
- const breakContext = this.pushBreakContext(true, label);
5358
- switch (type) {
5359
- case "WhileStatement":
5360
- this.pushChoiceContext("loop", false);
5361
- this.loopContext = {
5362
- upper: this.loopContext,
5363
- type,
5364
- label,
5365
- test: void 0,
5366
- continueDestSegments: null,
5367
- brokenForkContext: breakContext.brokenForkContext
5368
- };
5369
- break;
5370
- case "DoWhileStatement":
5371
- this.pushChoiceContext("loop", false);
5372
- this.loopContext = {
5373
- upper: this.loopContext,
5374
- type,
5375
- label,
5376
- test: void 0,
5377
- entrySegments: null,
5378
- continueForkContext: ForkContext.newEmpty(forkContext),
5379
- brokenForkContext: breakContext.brokenForkContext
5380
- };
5381
- break;
5382
- case "ForStatement":
5383
- this.pushChoiceContext("loop", false);
5384
- this.loopContext = {
5385
- upper: this.loopContext,
5386
- type,
5387
- label,
5388
- test: void 0,
5389
- endOfInitSegments: null,
5390
- testSegments: null,
5391
- endOfTestSegments: null,
5392
- updateSegments: null,
5393
- endOfUpdateSegments: null,
5394
- continueDestSegments: null,
5395
- brokenForkContext: breakContext.brokenForkContext
5396
- };
5397
- break;
5398
- case "ForInStatement":
5399
- case "ForOfStatement":
5400
- this.loopContext = {
5401
- upper: this.loopContext,
5402
- type,
5403
- label,
5404
- prevSegments: null,
5405
- leftSegments: null,
5406
- endOfLeftSegments: null,
5407
- continueDestSegments: null,
5408
- brokenForkContext: breakContext.brokenForkContext
5409
- };
5410
- break;
5411
- default: throw new Error(`unknown type: "${type}"`);
4929
+ static markUsed(segment) {
4930
+ if (segment.internal.used) return;
4931
+ segment.internal.used = true;
4932
+ let i;
4933
+ if (segment.reachable) for (i = 0; i < segment.allPrevSegments.length; ++i) {
4934
+ const prevSegment = segment.allPrevSegments[i];
4935
+ prevSegment.allNextSegments.push(segment);
4936
+ prevSegment.nextSegments.push(segment);
4937
+ }
4938
+ else for (i = 0; i < segment.allPrevSegments.length; ++i) segment.allPrevSegments[i].allNextSegments.push(segment);
4939
+ }
4940
+ /**
4941
+ * Marks a previous segment as looped.
4942
+ * @param {CodePathSegment} segment A segment.
4943
+ * @param {CodePathSegment} prevSegment A previous segment to mark.
4944
+ * @returns {void}
4945
+ */
4946
+ static markPrevSegmentAsLooped(segment, prevSegment) {
4947
+ segment.internal.loopedPrevSegments.push(prevSegment);
4948
+ }
4949
+ /**
4950
+ * Replaces unused segments with the previous segments of each unused segment.
4951
+ * @param {CodePathSegment[]} segments An array of segments to replace.
4952
+ * @returns {CodePathSegment[]} The replaced array.
4953
+ */
4954
+ static flattenUnusedSegments(segments) {
4955
+ const done = Object.create(null);
4956
+ const retv = [];
4957
+ for (let i = 0; i < segments.length; ++i) {
4958
+ const segment = segments[i];
4959
+ if (done[segment.id]) continue;
4960
+ if (!segment.internal.used) for (let j = 0; j < segment.allPrevSegments.length; ++j) {
4961
+ const prevSegment = segment.allPrevSegments[j];
4962
+ if (!done[prevSegment.id]) {
4963
+ done[prevSegment.id] = true;
4964
+ retv.push(prevSegment);
4965
+ }
4966
+ }
4967
+ else {
4968
+ done[segment.id] = true;
4969
+ retv.push(segment);
4970
+ }
5412
4971
  }
4972
+ return retv;
4973
+ }
4974
+ };
4975
+ /**
4976
+ * Gets whether or not a given segment is reachable.
4977
+ * @param {CodePathSegment} segment A segment to get.
4978
+ * @returns {boolean} `true` if the segment is reachable.
4979
+ */
4980
+ function isReachable(segment) {
4981
+ return segment.reachable;
4982
+ }
4983
+ /**
4984
+ * Creates new segments from the specific range of `context.segmentsList`.
4985
+ *
4986
+ * When `context.segmentsList` is `[[a, b], [c, d], [e, f]]`, `begin` is `0`, and
4987
+ * `end` is `-1`, this creates `[g, h]`. This `g` is from `a`, `c`, and `e`.
4988
+ * This `h` is from `b`, `d`, and `f`.
4989
+ * @param {ForkContext} context An instance.
4990
+ * @param {number} begin The first index of the previous segments.
4991
+ * @param {number} end The last index of the previous segments.
4992
+ * @param {Function} create A factory function of new segments.
4993
+ * @returns {CodePathSegment[]} New segments.
4994
+ */
4995
+ function makeSegments(context, begin, end, create) {
4996
+ const list = context.segmentsList;
4997
+ const normalizedBegin = begin >= 0 ? begin : list.length + begin;
4998
+ const normalizedEnd = end >= 0 ? end : list.length + end;
4999
+ const segments = [];
5000
+ for (let i = 0; i < context.count; ++i) {
5001
+ const allPrevSegments = [];
5002
+ for (let j = normalizedBegin; j <= normalizedEnd; ++j) allPrevSegments.push(list[j][i]);
5003
+ segments.push(create(context.idGenerator.next(), allPrevSegments));
5004
+ }
5005
+ return segments;
5006
+ }
5007
+ /**
5008
+ * `segments` becomes doubly in a `finally` block. Then if a code path exits by a
5009
+ * control statement (such as `break`, `continue`) from the `finally` block, the
5010
+ * destination's segments may be half of the source segments. In that case, this
5011
+ * merges segments.
5012
+ * @param {ForkContext} context An instance.
5013
+ * @param {CodePathSegment[]} segments Segments to merge.
5014
+ * @returns {CodePathSegment[]} The merged segments.
5015
+ */
5016
+ function mergeExtraSegments(context, segments) {
5017
+ let currentSegments = segments;
5018
+ while (currentSegments.length > context.count) {
5019
+ const merged = [];
5020
+ for (let i = 0, length = currentSegments.length / 2 | 0; i < length; ++i) merged.push(CodePathSegment.newNext(context.idGenerator.next(), [currentSegments[i], currentSegments[i + length]]));
5021
+ currentSegments = merged;
5022
+ }
5023
+ return currentSegments;
5024
+ }
5025
+ /**
5026
+ * A class to manage forking.
5027
+ */
5028
+ var ForkContext = class ForkContext {
5029
+ /**
5030
+ * @param {IdGenerator} idGenerator An identifier generator for segments.
5031
+ * @param {ForkContext|null} upper An upper fork context.
5032
+ * @param {number} count A number of parallel segments.
5033
+ */
5034
+ constructor(idGenerator, upper, count) {
5035
+ this.idGenerator = idGenerator;
5036
+ this.upper = upper;
5037
+ this.count = count;
5038
+ this.segmentsList = [];
5039
+ }
5040
+ /**
5041
+ * The head segments.
5042
+ * @type {CodePathSegment[]}
5043
+ */
5044
+ get head() {
5045
+ const list = this.segmentsList;
5046
+ return list.length === 0 ? [] : list[list.length - 1];
5047
+ }
5048
+ /**
5049
+ * A flag which shows empty.
5050
+ * @type {boolean}
5051
+ */
5052
+ get empty() {
5053
+ return this.segmentsList.length === 0;
5054
+ }
5055
+ /**
5056
+ * A flag which shows reachable.
5057
+ * @type {boolean}
5058
+ */
5059
+ get reachable() {
5060
+ const segments = this.head;
5061
+ return segments.length > 0 && segments.some(isReachable);
5062
+ }
5063
+ /**
5064
+ * Creates new segments from this context.
5065
+ * @param {number} begin The first index of previous segments.
5066
+ * @param {number} end The last index of previous segments.
5067
+ * @returns {CodePathSegment[]} New segments.
5068
+ */
5069
+ makeNext(begin, end) {
5070
+ return makeSegments(this, begin, end, CodePathSegment.newNext);
5071
+ }
5072
+ /**
5073
+ * Creates new segments from this context.
5074
+ * The new segments is always unreachable.
5075
+ * @param {number} begin The first index of previous segments.
5076
+ * @param {number} end The last index of previous segments.
5077
+ * @returns {CodePathSegment[]} New segments.
5078
+ */
5079
+ makeUnreachable(begin, end) {
5080
+ return makeSegments(this, begin, end, CodePathSegment.newUnreachable);
5081
+ }
5082
+ /**
5083
+ * Creates new segments from this context.
5084
+ * The new segments don't have connections for previous segments.
5085
+ * But these inherit the reachable flag from this context.
5086
+ * @param {number} begin The first index of previous segments.
5087
+ * @param {number} end The last index of previous segments.
5088
+ * @returns {CodePathSegment[]} New segments.
5089
+ */
5090
+ makeDisconnected(begin, end) {
5091
+ return makeSegments(this, begin, end, CodePathSegment.newDisconnected);
5413
5092
  }
5414
5093
  /**
5415
- * Pops the last context of a loop statement and finalizes it.
5416
- * @throws {Error} (Unreachable - unknown type.)
5094
+ * Adds segments into this context.
5095
+ * The added segments become the head.
5096
+ * @param {CodePathSegment[]} segments Segments to add.
5417
5097
  * @returns {void}
5418
5098
  */
5419
- popLoopContext() {
5420
- const context = this.loopContext;
5421
- this.loopContext = context.upper;
5422
- const forkContext = this.forkContext;
5423
- const brokenForkContext = this.popBreakContext().brokenForkContext;
5424
- switch (context.type) {
5425
- case "WhileStatement":
5426
- case "ForStatement":
5427
- this.popChoiceContext();
5428
- makeLooped(this, forkContext.head, context.continueDestSegments);
5429
- break;
5430
- case "DoWhileStatement": {
5431
- const choiceContext = this.popChoiceContext();
5432
- if (!choiceContext.processed) {
5433
- choiceContext.trueForkContext.add(forkContext.head);
5434
- choiceContext.falseForkContext.add(forkContext.head);
5435
- }
5436
- if (context.test !== true) brokenForkContext.addAll(choiceContext.falseForkContext);
5437
- const segmentsList = choiceContext.trueForkContext.segmentsList;
5438
- for (let i = 0; i < segmentsList.length; ++i) makeLooped(this, segmentsList[i], context.entrySegments);
5439
- break;
5440
- }
5441
- case "ForInStatement":
5442
- case "ForOfStatement":
5443
- brokenForkContext.add(forkContext.head);
5444
- makeLooped(this, forkContext.head, context.leftSegments);
5445
- break;
5446
- default: throw new Error("unreachable");
5447
- }
5448
- if (brokenForkContext.empty) forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
5449
- else forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
5099
+ add(segments) {
5100
+ assert(segments.length >= this.count, `${segments.length} >= ${this.count}`);
5101
+ this.segmentsList.push(mergeExtraSegments(this, segments));
5450
5102
  }
5451
5103
  /**
5452
- * Makes a code path segment for the test part of a WhileStatement.
5453
- * @param {boolean|undefined} test The test value (only when constant).
5104
+ * Replaces the head segments with given segments.
5105
+ * The current head segments are removed.
5106
+ * @param {CodePathSegment[]} segments Segments to add.
5454
5107
  * @returns {void}
5455
5108
  */
5456
- makeWhileTest(test) {
5457
- const context = this.loopContext;
5458
- const forkContext = this.forkContext;
5459
- const testSegments = forkContext.makeNext(0, -1);
5460
- context.test = test;
5461
- context.continueDestSegments = testSegments;
5462
- forkContext.replaceHead(testSegments);
5109
+ replaceHead(segments) {
5110
+ assert(segments.length >= this.count, `${segments.length} >= ${this.count}`);
5111
+ this.segmentsList.splice(-1, 1, mergeExtraSegments(this, segments));
5463
5112
  }
5464
5113
  /**
5465
- * Makes a code path segment for the body part of a WhileStatement.
5114
+ * Adds all segments of a given fork context into this context.
5115
+ * @param {ForkContext} context A fork context to add.
5466
5116
  * @returns {void}
5467
5117
  */
5468
- makeWhileBody() {
5469
- const context = this.loopContext;
5470
- const choiceContext = this.choiceContext;
5471
- const forkContext = this.forkContext;
5472
- if (!choiceContext.processed) {
5473
- choiceContext.trueForkContext.add(forkContext.head);
5474
- choiceContext.falseForkContext.add(forkContext.head);
5475
- }
5476
- if (context.test !== true) context.brokenForkContext.addAll(choiceContext.falseForkContext);
5477
- forkContext.replaceHead(choiceContext.trueForkContext.makeNext(0, -1));
5118
+ addAll(context) {
5119
+ assert(context.count === this.count);
5120
+ const source = context.segmentsList;
5121
+ for (let i = 0; i < source.length; ++i) this.segmentsList.push(source[i]);
5478
5122
  }
5479
5123
  /**
5480
- * Makes a code path segment for the body part of a DoWhileStatement.
5124
+ * Clears all segments in this context.
5481
5125
  * @returns {void}
5482
5126
  */
5483
- makeDoWhileBody() {
5484
- const context = this.loopContext;
5485
- const forkContext = this.forkContext;
5486
- const bodySegments = forkContext.makeNext(-1, -1);
5487
- context.entrySegments = bodySegments;
5488
- forkContext.replaceHead(bodySegments);
5127
+ clear() {
5128
+ this.segmentsList = [];
5489
5129
  }
5490
5130
  /**
5491
- * Makes a code path segment for the test part of a DoWhileStatement.
5492
- * @param {boolean|undefined} test The test value (only when constant).
5493
- * @returns {void}
5131
+ * Creates the root fork context.
5132
+ * @param {IdGenerator} idGenerator An identifier generator for segments.
5133
+ * @returns {ForkContext} New fork context.
5494
5134
  */
5495
- makeDoWhileTest(test) {
5496
- const context = this.loopContext;
5497
- const forkContext = this.forkContext;
5498
- context.test = test;
5499
- if (!context.continueForkContext.empty) {
5500
- context.continueForkContext.add(forkContext.head);
5501
- const testSegments = context.continueForkContext.makeNext(0, -1);
5502
- forkContext.replaceHead(testSegments);
5503
- }
5135
+ static newRoot(idGenerator) {
5136
+ const context = new ForkContext(idGenerator, null, 1);
5137
+ context.add([CodePathSegment.newRoot(idGenerator.next())]);
5138
+ return context;
5504
5139
  }
5505
5140
  /**
5506
- * Makes a code path segment for the test part of a ForStatement.
5507
- * @param {boolean|undefined} test The test value (only when constant).
5508
- * @returns {void}
5141
+ * Creates an empty fork context preceded by a given context.
5142
+ * @param {ForkContext} parentContext The parent fork context.
5143
+ * @param {boolean} forkLeavingPath A flag which shows inside of `finally` block.
5144
+ * @returns {ForkContext} New fork context.
5509
5145
  */
5510
- makeForTest(test) {
5511
- const context = this.loopContext;
5512
- const forkContext = this.forkContext;
5513
- const endOfInitSegments = forkContext.head;
5514
- const testSegments = forkContext.makeNext(-1, -1);
5515
- context.test = test;
5516
- context.endOfInitSegments = endOfInitSegments;
5517
- context.continueDestSegments = context.testSegments = testSegments;
5518
- forkContext.replaceHead(testSegments);
5146
+ static newEmpty(parentContext, forkLeavingPath) {
5147
+ return new ForkContext(parentContext.idGenerator, parentContext, (forkLeavingPath ? 2 : 1) * parentContext.count);
5148
+ }
5149
+ };
5150
+ /**
5151
+ * Adds given segments into the `dest` array.
5152
+ * If the `others` array does not includes the given segments, adds to the `all`
5153
+ * array as well.
5154
+ *
5155
+ * This adds only reachable and used segments.
5156
+ * @param {CodePathSegment[]} dest A destination array (`returnedSegments` or `thrownSegments`).
5157
+ * @param {CodePathSegment[]} others Another destination array (`returnedSegments` or `thrownSegments`).
5158
+ * @param {CodePathSegment[]} all The unified destination array (`finalSegments`).
5159
+ * @param {CodePathSegment[]} segments Segments to add.
5160
+ * @returns {void}
5161
+ */
5162
+ function addToReturnedOrThrown(dest, others, all, segments) {
5163
+ for (let i = 0; i < segments.length; ++i) {
5164
+ const segment = segments[i];
5165
+ dest.push(segment);
5166
+ if (!others.includes(segment)) all.push(segment);
5167
+ }
5168
+ }
5169
+ /**
5170
+ * Gets a loop-context for a `continue` statement.
5171
+ * @param {CodePathState} state A state to get.
5172
+ * @param {string} label The label of a `continue` statement.
5173
+ * @returns {LoopContext} A loop-context for a `continue` statement.
5174
+ */
5175
+ function getContinueContext(state, label) {
5176
+ if (!label) return state.loopContext;
5177
+ let context = state.loopContext;
5178
+ while (context) {
5179
+ if (context.label === label) return context;
5180
+ context = context.upper;
5181
+ }
5182
+ /* c8 ignore next */
5183
+ return null;
5184
+ }
5185
+ /**
5186
+ * Gets a context for a `break` statement.
5187
+ * @param {CodePathState} state A state to get.
5188
+ * @param {string} label The label of a `break` statement.
5189
+ * @returns {LoopContext|SwitchContext} A context for a `break` statement.
5190
+ */
5191
+ function getBreakContext(state, label) {
5192
+ let context = state.breakContext;
5193
+ while (context) {
5194
+ if (label ? context.label === label : context.breakable) return context;
5195
+ context = context.upper;
5196
+ }
5197
+ /* c8 ignore next */
5198
+ return null;
5199
+ }
5200
+ /**
5201
+ * Gets a context for a `return` statement.
5202
+ * @param {CodePathState} state A state to get.
5203
+ * @returns {TryContext|CodePathState} A context for a `return` statement.
5204
+ */
5205
+ function getReturnContext(state) {
5206
+ let context = state.tryContext;
5207
+ while (context) {
5208
+ if (context.hasFinalizer && context.position !== "finally") return context;
5209
+ context = context.upper;
5210
+ }
5211
+ return state;
5212
+ }
5213
+ /**
5214
+ * Gets a context for a `throw` statement.
5215
+ * @param {CodePathState} state A state to get.
5216
+ * @returns {TryContext|CodePathState} A context for a `throw` statement.
5217
+ */
5218
+ function getThrowContext(state) {
5219
+ let context = state.tryContext;
5220
+ while (context) {
5221
+ if (context.position === "try" || context.hasFinalizer && context.position === "catch") return context;
5222
+ context = context.upper;
5223
+ }
5224
+ return state;
5225
+ }
5226
+ /**
5227
+ * Removes a given element from a given array.
5228
+ * @param {any[]} xs An array to remove the specific element.
5229
+ * @param {any} x An element to be removed.
5230
+ * @returns {void}
5231
+ */
5232
+ function remove(xs, x) {
5233
+ xs.splice(xs.indexOf(x), 1);
5234
+ }
5235
+ /**
5236
+ * Disconnect given segments.
5237
+ *
5238
+ * This is used in a process for switch statements.
5239
+ * If there is the "default" chunk before other cases, the order is different
5240
+ * between node's and running's.
5241
+ * @param {CodePathSegment[]} prevSegments Forward segments to disconnect.
5242
+ * @param {CodePathSegment[]} nextSegments Backward segments to disconnect.
5243
+ * @returns {void}
5244
+ */
5245
+ function removeConnection(prevSegments, nextSegments) {
5246
+ for (let i = 0; i < prevSegments.length; ++i) {
5247
+ const prevSegment = prevSegments[i];
5248
+ const nextSegment = nextSegments[i];
5249
+ remove(prevSegment.nextSegments, nextSegment);
5250
+ remove(prevSegment.allNextSegments, nextSegment);
5251
+ remove(nextSegment.prevSegments, prevSegment);
5252
+ remove(nextSegment.allPrevSegments, prevSegment);
5253
+ }
5254
+ }
5255
+ /**
5256
+ * Creates looping path.
5257
+ * @param {CodePathState} state The instance.
5258
+ * @param {CodePathSegment[]} unflattenedFromSegments Segments which are source.
5259
+ * @param {CodePathSegment[]} unflattenedToSegments Segments which are destination.
5260
+ * @returns {void}
5261
+ */
5262
+ function makeLooped(state, unflattenedFromSegments, unflattenedToSegments) {
5263
+ const fromSegments = CodePathSegment.flattenUnusedSegments(unflattenedFromSegments);
5264
+ const toSegments = CodePathSegment.flattenUnusedSegments(unflattenedToSegments);
5265
+ const end = Math.min(fromSegments.length, toSegments.length);
5266
+ for (let i = 0; i < end; ++i) {
5267
+ const fromSegment = fromSegments[i];
5268
+ const toSegment = toSegments[i];
5269
+ if (toSegment.reachable) fromSegment.nextSegments.push(toSegment);
5270
+ if (fromSegment.reachable) toSegment.prevSegments.push(fromSegment);
5271
+ fromSegment.allNextSegments.push(toSegment);
5272
+ toSegment.allPrevSegments.push(fromSegment);
5273
+ if (toSegment.allPrevSegments.length >= 2) CodePathSegment.markPrevSegmentAsLooped(toSegment, fromSegment);
5274
+ state.notifyLooped(fromSegment, toSegment);
5275
+ }
5276
+ }
5277
+ /**
5278
+ * Finalizes segments of `test` chunk of a ForStatement.
5279
+ *
5280
+ * - Adds `false` paths to paths which are leaving from the loop.
5281
+ * - Sets `true` paths to paths which go to the body.
5282
+ * @param {LoopContext} context A loop context to modify.
5283
+ * @param {ChoiceContext} choiceContext A choice context of this loop.
5284
+ * @param {CodePathSegment[]} head The current head paths.
5285
+ * @returns {void}
5286
+ */
5287
+ function finalizeTestSegmentsOfFor(context, choiceContext, head) {
5288
+ if (!choiceContext.processed) {
5289
+ choiceContext.trueForkContext.add(head);
5290
+ choiceContext.falseForkContext.add(head);
5291
+ choiceContext.qqForkContext.add(head);
5519
5292
  }
5293
+ if (context.test !== true) context.brokenForkContext.addAll(choiceContext.falseForkContext);
5294
+ context.endOfTestSegments = choiceContext.trueForkContext.makeNext(0, -1);
5295
+ }
5296
+ /**
5297
+ * A class which manages state to analyze code paths.
5298
+ */
5299
+ var CodePathState = class {
5520
5300
  /**
5521
- * Makes a code path segment for the update part of a ForStatement.
5522
- * @returns {void}
5301
+ * @param {IdGenerator} idGenerator An id generator to generate id for code
5302
+ * path segments.
5303
+ * @param {Function} onLooped A callback function to notify looping.
5523
5304
  */
5524
- makeForUpdate() {
5525
- const context = this.loopContext;
5526
- const choiceContext = this.choiceContext;
5527
- const forkContext = this.forkContext;
5528
- if (context.testSegments) finalizeTestSegmentsOfFor(context, choiceContext, forkContext.head);
5529
- else context.endOfInitSegments = forkContext.head;
5530
- const updateSegments = forkContext.makeDisconnected(-1, -1);
5531
- context.continueDestSegments = context.updateSegments = updateSegments;
5532
- forkContext.replaceHead(updateSegments);
5305
+ constructor(idGenerator, onLooped) {
5306
+ this.idGenerator = idGenerator;
5307
+ this.notifyLooped = onLooped;
5308
+ this.forkContext = ForkContext.newRoot(idGenerator);
5309
+ this.choiceContext = null;
5310
+ this.switchContext = null;
5311
+ this.tryContext = null;
5312
+ this.loopContext = null;
5313
+ this.breakContext = null;
5314
+ this.chainContext = null;
5315
+ this.currentSegments = [];
5316
+ this.initialSegment = this.forkContext.head[0];
5317
+ const final = this.finalSegments = [];
5318
+ const returned = this.returnedForkContext = [];
5319
+ const thrown = this.thrownForkContext = [];
5320
+ returned.add = addToReturnedOrThrown.bind(null, returned, thrown, final);
5321
+ thrown.add = addToReturnedOrThrown.bind(null, thrown, returned, final);
5533
5322
  }
5534
5323
  /**
5535
- * Makes a code path segment for the body part of a ForStatement.
5536
- * @returns {void}
5324
+ * The head segments.
5325
+ * @type {CodePathSegment[]}
5537
5326
  */
5538
- makeForBody() {
5539
- const context = this.loopContext;
5540
- const choiceContext = this.choiceContext;
5541
- const forkContext = this.forkContext;
5542
- if (context.updateSegments) {
5543
- context.endOfUpdateSegments = forkContext.head;
5544
- if (context.testSegments) makeLooped(this, context.endOfUpdateSegments, context.testSegments);
5545
- } else if (context.testSegments) finalizeTestSegmentsOfFor(context, choiceContext, forkContext.head);
5546
- else context.endOfInitSegments = forkContext.head;
5547
- let bodySegments = context.endOfTestSegments;
5548
- if (!bodySegments) {
5549
- const prevForkContext = ForkContext.newEmpty(forkContext);
5550
- prevForkContext.add(context.endOfInitSegments);
5551
- if (context.endOfUpdateSegments) prevForkContext.add(context.endOfUpdateSegments);
5552
- bodySegments = prevForkContext.makeNext(0, -1);
5553
- }
5554
- context.continueDestSegments = context.continueDestSegments || bodySegments;
5555
- forkContext.replaceHead(bodySegments);
5327
+ get headSegments() {
5328
+ return this.forkContext.head;
5556
5329
  }
5557
5330
  /**
5558
- * Makes a code path segment for the left part of a ForInStatement and a
5559
- * ForOfStatement.
5560
- * @returns {void}
5331
+ * The parent forking context.
5332
+ * This is used for the root of new forks.
5333
+ * @type {ForkContext}
5561
5334
  */
5562
- makeForInOfLeft() {
5563
- const context = this.loopContext;
5564
- const forkContext = this.forkContext;
5565
- const leftSegments = forkContext.makeDisconnected(-1, -1);
5566
- context.prevSegments = forkContext.head;
5567
- context.leftSegments = context.continueDestSegments = leftSegments;
5568
- forkContext.replaceHead(leftSegments);
5335
+ get parentForkContext() {
5336
+ const current = this.forkContext;
5337
+ return current && current.upper;
5569
5338
  }
5570
5339
  /**
5571
- * Makes a code path segment for the right part of a ForInStatement and a
5572
- * ForOfStatement.
5573
- * @returns {void}
5340
+ * Creates and stacks new forking context.
5341
+ * @param {boolean} forkLeavingPath A flag which shows being in a
5342
+ * "finally" block.
5343
+ * @returns {ForkContext} The created context.
5574
5344
  */
5575
- makeForInOfRight() {
5576
- const context = this.loopContext;
5577
- const forkContext = this.forkContext;
5578
- const temp = ForkContext.newEmpty(forkContext);
5579
- temp.add(context.prevSegments);
5580
- const rightSegments = temp.makeNext(-1, -1);
5581
- context.endOfLeftSegments = forkContext.head;
5582
- forkContext.replaceHead(rightSegments);
5345
+ pushForkContext(forkLeavingPath) {
5346
+ this.forkContext = ForkContext.newEmpty(this.forkContext, forkLeavingPath);
5347
+ return this.forkContext;
5583
5348
  }
5584
5349
  /**
5585
- * Makes a code path segment for the body part of a ForInStatement and a
5586
- * ForOfStatement.
5587
- * @returns {void}
5350
+ * Pops and merges the last forking context.
5351
+ * @returns {ForkContext} The last context.
5588
5352
  */
5589
- makeForInOfBody() {
5590
- const context = this.loopContext;
5591
- const forkContext = this.forkContext;
5592
- const temp = ForkContext.newEmpty(forkContext);
5593
- temp.add(context.endOfLeftSegments);
5594
- const bodySegments = temp.makeNext(-1, -1);
5595
- makeLooped(this, forkContext.head, context.leftSegments);
5596
- context.brokenForkContext.add(forkContext.head);
5597
- forkContext.replaceHead(bodySegments);
5353
+ popForkContext() {
5354
+ const lastContext = this.forkContext;
5355
+ this.forkContext = lastContext.upper;
5356
+ this.forkContext.replaceHead(lastContext.makeNext(0, -1));
5357
+ return lastContext;
5598
5358
  }
5599
5359
  /**
5600
- * Creates new context for BreakStatement.
5601
- * @param {boolean} breakable The flag to indicate it can break by
5602
- * an unlabeled BreakStatement.
5603
- * @param {string|null} label The label of this context.
5604
- * @returns {Object} The new context.
5360
+ * Creates a new path.
5361
+ * @returns {void}
5605
5362
  */
5606
- pushBreakContext(breakable, label) {
5607
- this.breakContext = {
5608
- upper: this.breakContext,
5609
- breakable,
5610
- label,
5611
- brokenForkContext: ForkContext.newEmpty(this.forkContext)
5612
- };
5613
- return this.breakContext;
5363
+ forkPath() {
5364
+ this.forkContext.add(this.parentForkContext.makeNext(-1, -1));
5614
5365
  }
5615
5366
  /**
5616
- * Removes the top item of the break context stack.
5617
- * @returns {Object} The removed context.
5367
+ * Creates a bypass path.
5368
+ * This is used for such as IfStatement which does not have "else" chunk.
5369
+ * @returns {void}
5618
5370
  */
5619
- popBreakContext() {
5620
- const context = this.breakContext;
5621
- const forkContext = this.forkContext;
5622
- this.breakContext = context.upper;
5623
- if (!context.breakable) {
5624
- const brokenForkContext = context.brokenForkContext;
5625
- if (!brokenForkContext.empty) {
5626
- brokenForkContext.add(forkContext.head);
5627
- forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
5628
- }
5629
- }
5630
- return context;
5371
+ forkBypassPath() {
5372
+ this.forkContext.add(this.parentForkContext.head);
5631
5373
  }
5632
5374
  /**
5633
- * Makes a path for a `break` statement.
5375
+ * Creates a context for ConditionalExpression, LogicalExpression, AssignmentExpression (logical assignments only),
5376
+ * IfStatement, WhileStatement, DoWhileStatement, or ForStatement.
5634
5377
  *
5635
- * It registers the head segment to a context of `break`.
5636
- * It makes new unreachable segment, then it set the head with the segment.
5637
- * @param {string} label A label of the break statement.
5378
+ * LogicalExpressions have cases that it goes different paths between the
5379
+ * `true` case and the `false` case.
5380
+ *
5381
+ * For Example:
5382
+ *
5383
+ * if (a || b) {
5384
+ * foo();
5385
+ * } else {
5386
+ * bar();
5387
+ * }
5388
+ *
5389
+ * In this case, `b` is evaluated always in the code path of the `else`
5390
+ * block, but it's not so in the code path of the `if` block.
5391
+ * So there are 3 paths.
5392
+ *
5393
+ * a -> foo();
5394
+ * a -> b -> foo();
5395
+ * a -> b -> bar();
5396
+ * @param {string} kind A kind string.
5397
+ * If the new context is LogicalExpression's or AssignmentExpression's, this is `"&&"` or `"||"` or `"??"`.
5398
+ * If it's IfStatement's or ConditionalExpression's, this is `"test"`.
5399
+ * Otherwise, this is `"loop"`.
5400
+ * @param {boolean} isForkingAsResult A flag that shows that goes different
5401
+ * paths between `true` and `false`.
5638
5402
  * @returns {void}
5639
5403
  */
5640
- makeBreak(label) {
5641
- const forkContext = this.forkContext;
5642
- if (!forkContext.reachable) return;
5643
- const context = getBreakContext(this, label);
5644
- if (context) context.brokenForkContext.add(forkContext.head);
5645
- /* c8 ignore next */
5646
- forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
5404
+ pushChoiceContext(kind, isForkingAsResult) {
5405
+ this.choiceContext = {
5406
+ upper: this.choiceContext,
5407
+ kind,
5408
+ isForkingAsResult,
5409
+ trueForkContext: ForkContext.newEmpty(this.forkContext),
5410
+ falseForkContext: ForkContext.newEmpty(this.forkContext),
5411
+ qqForkContext: ForkContext.newEmpty(this.forkContext),
5412
+ processed: false
5413
+ };
5647
5414
  }
5648
5415
  /**
5649
- * Makes a path for a `continue` statement.
5650
- *
5651
- * It makes a looping path.
5652
- * It makes new unreachable segment, then it set the head with the segment.
5653
- * @param {string} label A label of the continue statement.
5654
- * @returns {void}
5416
+ * Pops the last choice context and finalizes it.
5417
+ * @throws {Error} (Unreachable.)
5418
+ * @returns {ChoiceContext} The popped context.
5655
5419
  */
5656
- makeContinue(label) {
5420
+ popChoiceContext() {
5421
+ const context = this.choiceContext;
5422
+ this.choiceContext = context.upper;
5657
5423
  const forkContext = this.forkContext;
5658
- if (!forkContext.reachable) return;
5659
- const context = getContinueContext(this, label);
5660
- if (context) if (context.continueDestSegments) {
5661
- makeLooped(this, forkContext.head, context.continueDestSegments);
5662
- if (context.type === "ForInStatement" || context.type === "ForOfStatement") context.brokenForkContext.add(forkContext.head);
5663
- } else context.continueForkContext.add(forkContext.head);
5664
- forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
5424
+ const headSegments = forkContext.head;
5425
+ switch (context.kind) {
5426
+ case "&&":
5427
+ case "||":
5428
+ case "??":
5429
+ if (!context.processed) {
5430
+ context.trueForkContext.add(headSegments);
5431
+ context.falseForkContext.add(headSegments);
5432
+ context.qqForkContext.add(headSegments);
5433
+ }
5434
+ if (context.isForkingAsResult) {
5435
+ const parentContext = this.choiceContext;
5436
+ parentContext.trueForkContext.addAll(context.trueForkContext);
5437
+ parentContext.falseForkContext.addAll(context.falseForkContext);
5438
+ parentContext.qqForkContext.addAll(context.qqForkContext);
5439
+ parentContext.processed = true;
5440
+ return context;
5441
+ }
5442
+ break;
5443
+ case "test":
5444
+ if (!context.processed) {
5445
+ context.trueForkContext.clear();
5446
+ context.trueForkContext.add(headSegments);
5447
+ } else {
5448
+ context.falseForkContext.clear();
5449
+ context.falseForkContext.add(headSegments);
5450
+ }
5451
+ break;
5452
+ case "loop": return context;
5453
+ default: throw new Error("unreachable");
5454
+ }
5455
+ const prevForkContext = context.trueForkContext;
5456
+ prevForkContext.addAll(context.falseForkContext);
5457
+ forkContext.replaceHead(prevForkContext.makeNext(0, -1));
5458
+ return context;
5665
5459
  }
5666
5460
  /**
5667
- * Makes a path for a `return` statement.
5668
- *
5669
- * It registers the head segment to a context of `return`.
5670
- * It makes new unreachable segment, then it set the head with the segment.
5461
+ * Makes a code path segment of the right-hand operand of a logical
5462
+ * expression.
5463
+ * @throws {Error} (Unreachable.)
5671
5464
  * @returns {void}
5672
5465
  */
5673
- makeReturn() {
5466
+ makeLogicalRight() {
5467
+ const context = this.choiceContext;
5674
5468
  const forkContext = this.forkContext;
5675
- if (forkContext.reachable) {
5676
- getReturnContext(this).returnedForkContext.add(forkContext.head);
5677
- forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
5469
+ if (context.processed) {
5470
+ let prevForkContext;
5471
+ switch (context.kind) {
5472
+ case "&&":
5473
+ prevForkContext = context.trueForkContext;
5474
+ break;
5475
+ case "||":
5476
+ prevForkContext = context.falseForkContext;
5477
+ break;
5478
+ case "??":
5479
+ prevForkContext = context.qqForkContext;
5480
+ break;
5481
+ default: throw new Error("unreachable");
5482
+ }
5483
+ forkContext.replaceHead(prevForkContext.makeNext(0, -1));
5484
+ prevForkContext.clear();
5485
+ context.processed = false;
5486
+ } else {
5487
+ switch (context.kind) {
5488
+ case "&&":
5489
+ context.falseForkContext.add(forkContext.head);
5490
+ break;
5491
+ case "||":
5492
+ context.trueForkContext.add(forkContext.head);
5493
+ break;
5494
+ case "??":
5495
+ context.trueForkContext.add(forkContext.head);
5496
+ context.falseForkContext.add(forkContext.head);
5497
+ break;
5498
+ default: throw new Error("unreachable");
5499
+ }
5500
+ forkContext.replaceHead(forkContext.makeNext(-1, -1));
5678
5501
  }
5679
5502
  }
5680
5503
  /**
5681
- * Makes a path for a `throw` statement.
5682
- *
5683
- * It registers the head segment to a context of `throw`.
5684
- * It makes new unreachable segment, then it set the head with the segment.
5504
+ * Makes a code path segment of the `if` block.
5685
5505
  * @returns {void}
5686
5506
  */
5687
- makeThrow() {
5507
+ makeIfConsequent() {
5508
+ const context = this.choiceContext;
5688
5509
  const forkContext = this.forkContext;
5689
- if (forkContext.reachable) {
5690
- getThrowContext(this).thrownForkContext.add(forkContext.head);
5691
- forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
5510
+ if (!context.processed) {
5511
+ context.trueForkContext.add(forkContext.head);
5512
+ context.falseForkContext.add(forkContext.head);
5513
+ context.qqForkContext.add(forkContext.head);
5692
5514
  }
5515
+ context.processed = false;
5516
+ forkContext.replaceHead(context.trueForkContext.makeNext(0, -1));
5693
5517
  }
5694
5518
  /**
5695
- * Makes the final path.
5519
+ * Makes a code path segment of the `else` block.
5696
5520
  * @returns {void}
5697
5521
  */
5698
- makeFinal() {
5699
- const segments = this.currentSegments;
5700
- if (segments.length > 0 && segments[0].reachable) this.returnedForkContext.add(segments);
5701
- }
5702
- };
5703
-
5704
- //#endregion
5705
- //#region src/utils/code-path-analysis/id-generator.js
5706
- /**
5707
- * A generator for unique ids.
5708
- */
5709
- var IdGenerator = class {
5710
- /**
5711
- * @param {string} prefix Optional. A prefix of generated ids.
5712
- */
5713
- constructor(prefix) {
5714
- this.prefix = String(prefix);
5715
- this.n = 0;
5522
+ makeIfAlternate() {
5523
+ const context = this.choiceContext;
5524
+ const forkContext = this.forkContext;
5525
+ context.trueForkContext.clear();
5526
+ context.trueForkContext.add(forkContext.head);
5527
+ context.processed = true;
5528
+ forkContext.replaceHead(context.falseForkContext.makeNext(0, -1));
5716
5529
  }
5717
5530
  /**
5718
- * Generates id.
5719
- * @returns {string} A generated id.
5531
+ * Push a new `ChainExpression` context to the stack.
5532
+ * This method is called on entering to each `ChainExpression` node.
5533
+ * This context is used to count forking in the optional chain then merge them on the exiting from the `ChainExpression` node.
5534
+ * @returns {void}
5720
5535
  */
5721
- next() {
5722
- this.n = 1 + this.n | 0;
5723
- /* c8 ignore start */
5724
- if (this.n < 0) this.n = 1;
5725
- return this.prefix + this.n;
5536
+ pushChainContext() {
5537
+ this.chainContext = {
5538
+ upper: this.chainContext,
5539
+ countChoiceContexts: 0
5540
+ };
5726
5541
  }
5727
- };
5728
-
5729
- //#endregion
5730
- //#region src/utils/code-path-analysis/code-path.js
5731
- /**
5732
- * A code path.
5733
- */
5734
- var CodePath = class {
5735
5542
  /**
5736
- * Creates a new instance.
5737
- * @param {Object} options Options for the function (see below).
5738
- * @param {string} options.id An identifier.
5739
- * @param {string} options.origin The type of code path origin.
5740
- * @param {CodePath|null} options.upper The code path of the upper function scope.
5741
- * @param {Function} options.onLooped A callback function to notify looping.
5543
+ * Pop a `ChainExpression` context from the stack.
5544
+ * This method is called on exiting from each `ChainExpression` node.
5545
+ * This merges all forks of the last optional chaining.
5546
+ * @returns {void}
5742
5547
  */
5743
- constructor({ id, origin, upper, onLooped }) {
5744
- /**
5745
- * The identifier of this code path.
5746
- * Rules use it to store additional information of each rule.
5747
- * @type {string}
5748
- */
5749
- this.id = id;
5750
- /**
5751
- * The reason that this code path was started. May be "program",
5752
- * "function", "class-field-initializer", or "class-static-block".
5753
- * @type {string}
5754
- */
5755
- this.origin = origin;
5756
- /**
5757
- * The code path of the upper function scope.
5758
- * @type {CodePath|null}
5759
- */
5760
- this.upper = upper;
5761
- /**
5762
- * The code paths of nested function scopes.
5763
- * @type {CodePath[]}
5764
- */
5765
- this.childCodePaths = [];
5766
- Object.defineProperty(this, "internal", { value: new CodePathState(new IdGenerator(`${id}_`), onLooped) });
5767
- if (upper) upper.childCodePaths.push(this);
5548
+ popChainContext() {
5549
+ const context = this.chainContext;
5550
+ this.chainContext = context.upper;
5551
+ for (let i = context.countChoiceContexts; i > 0; --i) this.popChoiceContext();
5768
5552
  }
5769
5553
  /**
5770
- * Gets the state of a given code path.
5771
- * @param {CodePath} codePath A code path to get.
5772
- * @returns {CodePathState} The state of the code path.
5554
+ * Create a choice context for optional access.
5555
+ * This method is called on entering to each `(Call|Member)Expression[optional=true]` node.
5556
+ * This creates a choice context as similar to `LogicalExpression[operator="??"]` node.
5557
+ * @returns {void}
5773
5558
  */
5774
- static getState(codePath) {
5775
- return codePath.internal;
5559
+ makeOptionalNode() {
5560
+ if (this.chainContext) {
5561
+ this.chainContext.countChoiceContexts += 1;
5562
+ this.pushChoiceContext("??", false);
5563
+ }
5776
5564
  }
5777
5565
  /**
5778
- * The initial code path segment.
5779
- * @type {CodePathSegment}
5566
+ * Create a fork.
5567
+ * This method is called on entering to the `arguments|property` property of each `(Call|Member)Expression` node.
5568
+ * @returns {void}
5780
5569
  */
5781
- get initialSegment() {
5782
- return this.internal.initialSegment;
5570
+ makeOptionalRight() {
5571
+ if (this.chainContext) this.makeLogicalRight();
5783
5572
  }
5784
5573
  /**
5785
- * Final code path segments.
5786
- * This array is a mix of `returnedSegments` and `thrownSegments`.
5787
- * @type {CodePathSegment[]}
5574
+ * Creates a context object of SwitchStatement and stacks it.
5575
+ * @param {boolean} hasCase `true` if the switch statement has one or more
5576
+ * case parts.
5577
+ * @param {string|null} label The label text.
5578
+ * @returns {void}
5788
5579
  */
5789
- get finalSegments() {
5790
- return this.internal.finalSegments;
5580
+ pushSwitchContext(hasCase, label) {
5581
+ this.switchContext = {
5582
+ upper: this.switchContext,
5583
+ hasCase,
5584
+ defaultSegments: null,
5585
+ defaultBodySegments: null,
5586
+ foundDefault: false,
5587
+ lastIsDefault: false,
5588
+ countForks: 0
5589
+ };
5590
+ this.pushBreakContext(true, label);
5791
5591
  }
5792
5592
  /**
5793
- * Final code path segments which is with `return` statements.
5794
- * This array contains the last path segment if it's reachable.
5795
- * Since the reachable last path returns `undefined`.
5796
- * @type {CodePathSegment[]}
5593
+ * Pops the last context of SwitchStatement and finalizes it.
5594
+ *
5595
+ * - Disposes all forking stack for `case` and `default`.
5596
+ * - Creates the next code path segment from `context.brokenForkContext`.
5597
+ * - If the last `SwitchCase` node is not a `default` part, creates a path
5598
+ * to the `default` body.
5599
+ * @returns {void}
5797
5600
  */
5798
- get returnedSegments() {
5799
- return this.internal.returnedForkContext;
5601
+ popSwitchContext() {
5602
+ const context = this.switchContext;
5603
+ this.switchContext = context.upper;
5604
+ const forkContext = this.forkContext;
5605
+ const brokenForkContext = this.popBreakContext().brokenForkContext;
5606
+ if (context.countForks === 0) {
5607
+ if (!brokenForkContext.empty) {
5608
+ brokenForkContext.add(forkContext.makeNext(-1, -1));
5609
+ forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
5610
+ }
5611
+ return;
5612
+ }
5613
+ const lastSegments = forkContext.head;
5614
+ this.forkBypassPath();
5615
+ const lastCaseSegments = forkContext.head;
5616
+ brokenForkContext.add(lastSegments);
5617
+ if (!context.lastIsDefault) if (context.defaultBodySegments) {
5618
+ removeConnection(context.defaultSegments, context.defaultBodySegments);
5619
+ makeLooped(this, lastCaseSegments, context.defaultBodySegments);
5620
+ } else brokenForkContext.add(lastCaseSegments);
5621
+ for (let i = 0; i < context.countForks; ++i) this.forkContext = this.forkContext.upper;
5622
+ this.forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
5800
5623
  }
5801
5624
  /**
5802
- * Final code path segments which is with `throw` statements.
5803
- * @type {CodePathSegment[]}
5625
+ * Makes a code path segment for a `SwitchCase` node.
5626
+ * @param {boolean} isEmpty `true` if the body is empty.
5627
+ * @param {boolean} isDefault `true` if the body is the default case.
5628
+ * @returns {void}
5804
5629
  */
5805
- get thrownSegments() {
5806
- return this.internal.thrownForkContext;
5630
+ makeSwitchCaseBody(isEmpty, isDefault) {
5631
+ const context = this.switchContext;
5632
+ if (!context.hasCase) return;
5633
+ const parentForkContext = this.forkContext;
5634
+ const forkContext = this.pushForkContext();
5635
+ forkContext.add(parentForkContext.makeNext(0, -1));
5636
+ if (isDefault) {
5637
+ context.defaultSegments = parentForkContext.head;
5638
+ if (isEmpty) context.foundDefault = true;
5639
+ else context.defaultBodySegments = forkContext.head;
5640
+ } else if (!isEmpty && context.foundDefault) {
5641
+ context.foundDefault = false;
5642
+ context.defaultBodySegments = forkContext.head;
5643
+ }
5644
+ context.lastIsDefault = isDefault;
5645
+ context.countForks += 1;
5807
5646
  }
5808
5647
  /**
5809
- * Current code path segments.
5810
- * @type {CodePathSegment[]}
5648
+ * Creates a context object of TryStatement and stacks it.
5649
+ * @param {boolean} hasFinalizer `true` if the try statement has a
5650
+ * `finally` block.
5651
+ * @returns {void}
5811
5652
  */
5812
- get currentSegments() {
5813
- return this.internal.currentSegments;
5653
+ pushTryContext(hasFinalizer) {
5654
+ this.tryContext = {
5655
+ upper: this.tryContext,
5656
+ position: "try",
5657
+ hasFinalizer,
5658
+ returnedForkContext: hasFinalizer ? ForkContext.newEmpty(this.forkContext) : null,
5659
+ thrownForkContext: ForkContext.newEmpty(this.forkContext),
5660
+ lastOfTryIsReachable: false,
5661
+ lastOfCatchIsReachable: false
5662
+ };
5814
5663
  }
5815
5664
  /**
5816
- * Traverses all segments in this code path.
5817
- *
5818
- * codePath.traverseSegments(function(segment, controller) {
5819
- * // do something.
5820
- * });
5821
- *
5822
- * This method enumerates segments in order from the head.
5823
- *
5824
- * The `controller` object has two methods.
5825
- *
5826
- * - `controller.skip()` - Skip the following segments in this branch.
5827
- * - `controller.break()` - Skip all following segments.
5828
- * @param {Object} [options] Omittable.
5829
- * @param {CodePathSegment} [options.first] The first segment to traverse.
5830
- * @param {CodePathSegment} [options.last] The last segment to traverse.
5831
- * @param {Function} callback A callback function.
5665
+ * Pops the last context of TryStatement and finalizes it.
5832
5666
  * @returns {void}
5833
5667
  */
5834
- traverseSegments(options, callback) {
5835
- let resolvedOptions;
5836
- let resolvedCallback;
5837
- if (typeof options === "function") {
5838
- resolvedCallback = options;
5839
- resolvedOptions = {};
5840
- } else {
5841
- resolvedOptions = options || {};
5842
- resolvedCallback = callback;
5843
- }
5844
- const startSegment = resolvedOptions.first || this.internal.initialSegment;
5845
- const lastSegment = resolvedOptions.last;
5846
- let item = null;
5847
- let index = 0;
5848
- let end = 0;
5849
- let segment = null;
5850
- const visited = Object.create(null);
5851
- const stack = [[startSegment, 0]];
5852
- let skippedSegment = null;
5853
- let broken = false;
5854
- const controller = {
5855
- skip() {
5856
- if (stack.length <= 1) broken = true;
5857
- else skippedSegment = stack[stack.length - 2][0];
5858
- },
5859
- break() {
5860
- broken = true;
5861
- }
5862
- };
5863
- /**
5864
- * Checks a given previous segment has been visited.
5865
- * @param {CodePathSegment} prevSegment A previous segment to check.
5866
- * @returns {boolean} `true` if the segment has been visited.
5867
- */
5868
- function isVisited(prevSegment) {
5869
- return visited[prevSegment.id] || segment.isLoopedPrevSegment(prevSegment);
5870
- }
5871
- while (stack.length > 0) {
5872
- item = stack[stack.length - 1];
5873
- segment = item[0];
5874
- index = item[1];
5875
- if (index === 0) {
5876
- if (visited[segment.id]) {
5877
- stack.pop();
5878
- continue;
5879
- }
5880
- if (segment !== startSegment && segment.prevSegments.length > 0 && !segment.prevSegments.every(isVisited)) {
5881
- stack.pop();
5882
- continue;
5883
- }
5884
- if (skippedSegment && segment.prevSegments.includes(skippedSegment)) skippedSegment = null;
5885
- visited[segment.id] = true;
5886
- if (!skippedSegment) {
5887
- resolvedCallback.call(this, segment, controller);
5888
- if (segment === lastSegment) controller.skip();
5889
- if (broken) break;
5890
- }
5891
- }
5892
- end = segment.nextSegments.length - 1;
5893
- if (index < end) {
5894
- item[1] += 1;
5895
- stack.push([segment.nextSegments[index], 0]);
5896
- } else if (index === end) {
5897
- item[0] = segment.nextSegments[index];
5898
- item[1] = 0;
5899
- } else stack.pop();
5900
- }
5901
- }
5902
- };
5903
-
5904
- //#endregion
5905
- //#region src/utils/code-path-analysis/code-path-analyzer.js
5906
- const breakableTypePattern = /^(?:(?:Do)?While|For(?:In|Of)?|Switch)Statement$/u;
5907
- /**
5908
- * Checks whether or not a given node is a `case` node (not `default` node).
5909
- * @param {ASTNode} node A `SwitchCase` node to check.
5910
- * @returns {boolean} `true` if the node is a `case` node (not `default` node).
5911
- */
5912
- function isCaseNode(node) {
5913
- return Boolean(node.test);
5914
- }
5915
- /**
5916
- * Checks if a given node appears as the value of a PropertyDefinition node.
5917
- * @param {ASTNode} node THe node to check.
5918
- * @returns {boolean} `true` if the node is a PropertyDefinition value,
5919
- * false if not.
5920
- */
5921
- function isPropertyDefinitionValue(node) {
5922
- const parent = node.parent;
5923
- return parent && parent.type === "PropertyDefinition" && parent.value === node;
5924
- }
5925
- /**
5926
- * Checks whether the given logical operator is taken into account for the code
5927
- * path analysis.
5928
- * @param {string} operator The operator found in the LogicalExpression node
5929
- * @returns {boolean} `true` if the operator is "&&" or "||" or "??"
5930
- */
5931
- function isHandledLogicalOperator(operator) {
5932
- return operator === "&&" || operator === "||" || operator === "??";
5933
- }
5934
- /**
5935
- * Checks whether the given assignment operator is a logical assignment operator.
5936
- * Logical assignments are taken into account for the code path analysis
5937
- * because of their short-circuiting semantics.
5938
- * @param {string} operator The operator found in the AssignmentExpression node
5939
- * @returns {boolean} `true` if the operator is "&&=" or "||=" or "??="
5940
- */
5941
- function isLogicalAssignmentOperator(operator) {
5942
- return operator === "&&=" || operator === "||=" || operator === "??=";
5943
- }
5944
- /**
5945
- * Gets the label if the parent node of a given node is a LabeledStatement.
5946
- * @param {ASTNode} node A node to get.
5947
- * @returns {string|null} The label or `null`.
5948
- */
5949
- function getLabel(node) {
5950
- if (node.parent.type === "LabeledStatement") return node.parent.label.name;
5951
- return null;
5952
- }
5953
- /**
5954
- * Checks whether or not a given logical expression node goes different path
5955
- * between the `true` case and the `false` case.
5956
- * @param {ASTNode} node A node to check.
5957
- * @returns {boolean} `true` if the node is a test of a choice statement.
5958
- */
5959
- function isForkingByTrueOrFalse(node) {
5960
- const parent = node.parent;
5961
- switch (parent.type) {
5962
- case "ConditionalExpression":
5963
- case "IfStatement":
5964
- case "WhileStatement":
5965
- case "DoWhileStatement":
5966
- case "ForStatement": return parent.test === node;
5967
- case "LogicalExpression": return isHandledLogicalOperator(parent.operator);
5968
- case "AssignmentExpression": return isLogicalAssignmentOperator(parent.operator);
5969
- default: return false;
5970
- }
5971
- }
5972
- /**
5973
- * Gets the boolean value of a given literal node.
5974
- *
5975
- * This is used to detect infinity loops (e.g. `while (true) {}`).
5976
- * Statements preceded by an infinity loop are unreachable if the loop didn't
5977
- * have any `break` statement.
5978
- * @param {ASTNode} node A node to get.
5979
- * @returns {boolean|undefined} a boolean value if the node is a Literal node,
5980
- * otherwise `undefined`.
5981
- */
5982
- function getBooleanValueIfSimpleConstant(node) {
5983
- if (node.type === "Literal") return Boolean(node.value);
5984
- }
5985
- /**
5986
- * Checks that a given identifier node is a reference or not.
5987
- *
5988
- * This is used to detect the first throwable node in a `try` block.
5989
- * @param {ASTNode} node An Identifier node to check.
5990
- * @returns {boolean} `true` if the node is a reference.
5991
- */
5992
- function isIdentifierReference(node) {
5993
- const parent = node.parent;
5994
- switch (parent.type) {
5995
- case "LabeledStatement":
5996
- case "BreakStatement":
5997
- case "ContinueStatement":
5998
- case "ArrayPattern":
5999
- case "RestElement":
6000
- case "ImportSpecifier":
6001
- case "ImportDefaultSpecifier":
6002
- case "ImportNamespaceSpecifier":
6003
- case "CatchClause": return false;
6004
- case "FunctionDeclaration":
6005
- case "ComponentDeclaration":
6006
- case "HookDeclaration":
6007
- case "FunctionExpression":
6008
- case "ArrowFunctionExpression":
6009
- case "ClassDeclaration":
6010
- case "ClassExpression":
6011
- case "VariableDeclarator": return parent.id !== node;
6012
- case "Property":
6013
- case "PropertyDefinition":
6014
- case "MethodDefinition": return parent.key !== node || parent.computed || parent.shorthand;
6015
- case "AssignmentPattern": return parent.key !== node;
6016
- default: return true;
6017
- }
6018
- }
6019
- /**
6020
- * Updates the current segment with the head segment.
6021
- * This is similar to local branches and tracking branches of git.
6022
- *
6023
- * To separate the current and the head is in order to not make useless segments.
6024
- *
6025
- * In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd"
6026
- * events are fired.
6027
- * @param {CodePathAnalyzer} analyzer The instance.
6028
- * @param {ASTNode} node The current AST node.
6029
- * @returns {void}
6030
- */
6031
- function forwardCurrentToHead(analyzer, node) {
6032
- const codePath = analyzer.codePath;
6033
- const state = CodePath.getState(codePath);
6034
- const currentSegments = state.currentSegments;
6035
- const headSegments = state.headSegments;
6036
- const end = Math.max(currentSegments.length, headSegments.length);
6037
- let i, currentSegment, headSegment;
6038
- for (i = 0; i < end; ++i) {
6039
- currentSegment = currentSegments[i];
6040
- headSegment = headSegments[i];
6041
- if (currentSegment !== headSegment && currentSegment) {
6042
- if (currentSegment.reachable) analyzer.emitter.emit("onCodePathSegmentEnd", currentSegment, node);
5668
+ popTryContext() {
5669
+ const context = this.tryContext;
5670
+ this.tryContext = context.upper;
5671
+ if (context.position === "catch") {
5672
+ this.popForkContext();
5673
+ return;
6043
5674
  }
5675
+ const returned = context.returnedForkContext;
5676
+ const thrown = context.thrownForkContext;
5677
+ if (returned.empty && thrown.empty) return;
5678
+ const headSegments = this.forkContext.head;
5679
+ this.forkContext = this.forkContext.upper;
5680
+ const normalSegments = headSegments.slice(0, headSegments.length / 2 | 0);
5681
+ const leavingSegments = headSegments.slice(headSegments.length / 2 | 0);
5682
+ if (!returned.empty) getReturnContext(this).returnedForkContext.add(leavingSegments);
5683
+ if (!thrown.empty) getThrowContext(this).thrownForkContext.add(leavingSegments);
5684
+ this.forkContext.replaceHead(normalSegments);
5685
+ if (!context.lastOfTryIsReachable && !context.lastOfCatchIsReachable) this.forkContext.makeUnreachable();
6044
5686
  }
6045
- state.currentSegments = headSegments;
6046
- for (i = 0; i < end; ++i) {
6047
- currentSegment = currentSegments[i];
6048
- headSegment = headSegments[i];
6049
- if (currentSegment !== headSegment && headSegment) {
6050
- CodePathSegment.markUsed(headSegment);
6051
- if (headSegment.reachable) analyzer.emitter.emit("onCodePathSegmentStart", headSegment, node);
5687
+ /**
5688
+ * Makes a code path segment for a `catch` block.
5689
+ * @returns {void}
5690
+ */
5691
+ makeCatchBlock() {
5692
+ const context = this.tryContext;
5693
+ const forkContext = this.forkContext;
5694
+ const thrown = context.thrownForkContext;
5695
+ context.position = "catch";
5696
+ context.thrownForkContext = ForkContext.newEmpty(forkContext);
5697
+ context.lastOfTryIsReachable = forkContext.reachable;
5698
+ thrown.add(forkContext.head);
5699
+ const thrownSegments = thrown.makeNext(0, -1);
5700
+ this.pushForkContext();
5701
+ this.forkBypassPath();
5702
+ this.forkContext.add(thrownSegments);
5703
+ }
5704
+ /**
5705
+ * Makes a code path segment for a `finally` block.
5706
+ *
5707
+ * In the `finally` block, parallel paths are created. The parallel paths
5708
+ * are used as leaving-paths. The leaving-paths are paths from `return`
5709
+ * statements and `throw` statements in a `try` block or a `catch` block.
5710
+ * @returns {void}
5711
+ */
5712
+ makeFinallyBlock() {
5713
+ const context = this.tryContext;
5714
+ let forkContext = this.forkContext;
5715
+ const returned = context.returnedForkContext;
5716
+ const thrown = context.thrownForkContext;
5717
+ const headOfLeavingSegments = forkContext.head;
5718
+ if (context.position === "catch") {
5719
+ this.popForkContext();
5720
+ forkContext = this.forkContext;
5721
+ context.lastOfCatchIsReachable = forkContext.reachable;
5722
+ } else context.lastOfTryIsReachable = forkContext.reachable;
5723
+ context.position = "finally";
5724
+ if (returned.empty && thrown.empty) return;
5725
+ const segments = forkContext.makeNext(-1, -1);
5726
+ for (let i = 0; i < forkContext.count; ++i) {
5727
+ const prevSegsOfLeavingSegment = [headOfLeavingSegments[i]];
5728
+ for (let j = 0; j < returned.segmentsList.length; ++j) prevSegsOfLeavingSegment.push(returned.segmentsList[j][i]);
5729
+ for (let j = 0; j < thrown.segmentsList.length; ++j) prevSegsOfLeavingSegment.push(thrown.segmentsList[j][i]);
5730
+ segments.push(CodePathSegment.newNext(this.idGenerator.next(), prevSegsOfLeavingSegment));
6052
5731
  }
5732
+ this.pushForkContext(true);
5733
+ this.forkContext.add(segments);
6053
5734
  }
6054
- }
6055
- /**
6056
- * Updates the current segment with empty.
6057
- * This is called at the last of functions or the program.
6058
- * @param {CodePathAnalyzer} analyzer The instance.
6059
- * @param {ASTNode} node The current AST node.
6060
- * @returns {void}
6061
- */
6062
- function leaveFromCurrentSegment(analyzer, node) {
6063
- const state = CodePath.getState(analyzer.codePath);
6064
- const currentSegments = state.currentSegments;
6065
- for (let i = 0; i < currentSegments.length; ++i) {
6066
- const currentSegment = currentSegments[i];
6067
- if (currentSegment.reachable) analyzer.emitter.emit("onCodePathSegmentEnd", currentSegment, node);
5735
+ /**
5736
+ * Makes a code path segment from the first throwable node to the `catch`
5737
+ * block or the `finally` block.
5738
+ * @returns {void}
5739
+ */
5740
+ makeFirstThrowablePathInTryBlock() {
5741
+ const forkContext = this.forkContext;
5742
+ if (!forkContext.reachable) return;
5743
+ const context = getThrowContext(this);
5744
+ if (context === this || context.position !== "try" || !context.thrownForkContext.empty) return;
5745
+ context.thrownForkContext.add(forkContext.head);
5746
+ forkContext.replaceHead(forkContext.makeNext(-1, -1));
6068
5747
  }
6069
- state.currentSegments = [];
6070
- }
6071
- /**
6072
- * Updates the code path due to the position of a given node in the parent node
6073
- * thereof.
6074
- *
6075
- * For example, if the node is `parent.consequent`, this creates a fork from the
6076
- * current path.
6077
- * @param {CodePathAnalyzer} analyzer The instance.
6078
- * @param {ASTNode} node The current AST node.
6079
- * @returns {void}
6080
- */
6081
- function preprocess(analyzer, node) {
6082
- const codePath = analyzer.codePath;
6083
- const state = CodePath.getState(codePath);
6084
- const parent = node.parent;
6085
- switch (parent.type) {
6086
- case "CallExpression":
6087
- if (parent.optional === true && parent.arguments.length >= 1 && parent.arguments[0] === node) state.makeOptionalRight();
6088
- break;
6089
- case "MemberExpression":
6090
- if (parent.optional === true && parent.property === node) state.makeOptionalRight();
6091
- break;
6092
- case "LogicalExpression":
6093
- if (parent.right === node && isHandledLogicalOperator(parent.operator)) state.makeLogicalRight();
6094
- break;
6095
- case "AssignmentExpression":
6096
- if (parent.right === node && isLogicalAssignmentOperator(parent.operator)) state.makeLogicalRight();
6097
- break;
6098
- case "ConditionalExpression":
6099
- case "IfStatement":
6100
- if (parent.consequent === node) state.makeIfConsequent();
6101
- else if (parent.alternate === node) state.makeIfAlternate();
6102
- break;
6103
- case "SwitchCase":
6104
- if (parent.consequent[0] === node) state.makeSwitchCaseBody(false, !parent.test);
6105
- break;
6106
- case "TryStatement":
6107
- if (parent.handler === node) state.makeCatchBlock();
6108
- else if (parent.finalizer === node) state.makeFinallyBlock();
6109
- break;
6110
- case "WhileStatement":
6111
- if (parent.test === node) state.makeWhileTest(getBooleanValueIfSimpleConstant(node));
6112
- else {
6113
- assert(parent.body === node);
6114
- state.makeWhileBody();
6115
- }
6116
- break;
6117
- case "DoWhileStatement":
6118
- if (parent.body === node) state.makeDoWhileBody();
6119
- else {
6120
- assert(parent.test === node);
6121
- state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node));
6122
- }
6123
- break;
6124
- case "ForStatement":
6125
- if (parent.test === node) state.makeForTest(getBooleanValueIfSimpleConstant(node));
6126
- else if (parent.update === node) state.makeForUpdate();
6127
- else if (parent.body === node) state.makeForBody();
6128
- break;
6129
- case "ForInStatement":
6130
- case "ForOfStatement":
6131
- if (parent.left === node) state.makeForInOfLeft();
6132
- else if (parent.right === node) state.makeForInOfRight();
6133
- else {
6134
- assert(parent.body === node);
6135
- state.makeForInOfBody();
6136
- }
6137
- break;
6138
- case "AssignmentPattern":
6139
- if (parent.right === node) {
6140
- state.pushForkContext();
6141
- state.forkBypassPath();
6142
- state.forkPath();
5748
+ /**
5749
+ * Creates a context object of a loop statement and stacks it.
5750
+ * @param {string} type The type of the node which was triggered. One of
5751
+ * `WhileStatement`, `DoWhileStatement`, `ForStatement`, `ForInStatement`,
5752
+ * and `ForStatement`.
5753
+ * @param {string|null} label A label of the node which was triggered.
5754
+ * @throws {Error} (Unreachable - unknown type.)
5755
+ * @returns {void}
5756
+ */
5757
+ pushLoopContext(type, label) {
5758
+ const forkContext = this.forkContext;
5759
+ const breakContext = this.pushBreakContext(true, label);
5760
+ switch (type) {
5761
+ case "WhileStatement":
5762
+ this.pushChoiceContext("loop", false);
5763
+ this.loopContext = {
5764
+ upper: this.loopContext,
5765
+ type,
5766
+ label,
5767
+ test: void 0,
5768
+ continueDestSegments: null,
5769
+ brokenForkContext: breakContext.brokenForkContext
5770
+ };
5771
+ break;
5772
+ case "DoWhileStatement":
5773
+ this.pushChoiceContext("loop", false);
5774
+ this.loopContext = {
5775
+ upper: this.loopContext,
5776
+ type,
5777
+ label,
5778
+ test: void 0,
5779
+ entrySegments: null,
5780
+ continueForkContext: ForkContext.newEmpty(forkContext),
5781
+ brokenForkContext: breakContext.brokenForkContext
5782
+ };
5783
+ break;
5784
+ case "ForStatement":
5785
+ this.pushChoiceContext("loop", false);
5786
+ this.loopContext = {
5787
+ upper: this.loopContext,
5788
+ type,
5789
+ label,
5790
+ test: void 0,
5791
+ endOfInitSegments: null,
5792
+ testSegments: null,
5793
+ endOfTestSegments: null,
5794
+ updateSegments: null,
5795
+ endOfUpdateSegments: null,
5796
+ continueDestSegments: null,
5797
+ brokenForkContext: breakContext.brokenForkContext
5798
+ };
5799
+ break;
5800
+ case "ForInStatement":
5801
+ case "ForOfStatement":
5802
+ this.loopContext = {
5803
+ upper: this.loopContext,
5804
+ type,
5805
+ label,
5806
+ prevSegments: null,
5807
+ leftSegments: null,
5808
+ endOfLeftSegments: null,
5809
+ continueDestSegments: null,
5810
+ brokenForkContext: breakContext.brokenForkContext
5811
+ };
5812
+ break;
5813
+ default: throw new Error(`unknown type: "${type}"`);
5814
+ }
5815
+ }
5816
+ /**
5817
+ * Pops the last context of a loop statement and finalizes it.
5818
+ * @throws {Error} (Unreachable - unknown type.)
5819
+ * @returns {void}
5820
+ */
5821
+ popLoopContext() {
5822
+ const context = this.loopContext;
5823
+ this.loopContext = context.upper;
5824
+ const forkContext = this.forkContext;
5825
+ const brokenForkContext = this.popBreakContext().brokenForkContext;
5826
+ switch (context.type) {
5827
+ case "WhileStatement":
5828
+ case "ForStatement":
5829
+ this.popChoiceContext();
5830
+ makeLooped(this, forkContext.head, context.continueDestSegments);
5831
+ break;
5832
+ case "DoWhileStatement": {
5833
+ const choiceContext = this.popChoiceContext();
5834
+ if (!choiceContext.processed) {
5835
+ choiceContext.trueForkContext.add(forkContext.head);
5836
+ choiceContext.falseForkContext.add(forkContext.head);
5837
+ }
5838
+ if (context.test !== true) brokenForkContext.addAll(choiceContext.falseForkContext);
5839
+ const segmentsList = choiceContext.trueForkContext.segmentsList;
5840
+ for (let i = 0; i < segmentsList.length; ++i) makeLooped(this, segmentsList[i], context.entrySegments);
5841
+ break;
6143
5842
  }
6144
- break;
6145
- default: break;
5843
+ case "ForInStatement":
5844
+ case "ForOfStatement":
5845
+ brokenForkContext.add(forkContext.head);
5846
+ makeLooped(this, forkContext.head, context.leftSegments);
5847
+ break;
5848
+ default: throw new Error("unreachable");
5849
+ }
5850
+ if (brokenForkContext.empty) forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
5851
+ else forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
6146
5852
  }
6147
- }
6148
- /**
6149
- * Updates the code path due to the type of a given node in entering.
6150
- * @param {CodePathAnalyzer} analyzer The instance.
6151
- * @param {ASTNode} node The current AST node.
6152
- * @returns {void}
6153
- */
6154
- function processCodePathToEnter(analyzer, node) {
6155
- let codePath = analyzer.codePath;
6156
- let state = codePath && CodePath.getState(codePath);
6157
- const parent = node.parent;
6158
5853
  /**
6159
- * Creates a new code path and trigger the onCodePathStart event
6160
- * based on the currently selected node.
6161
- * @param {string} origin The reason the code path was started.
5854
+ * Makes a code path segment for the test part of a WhileStatement.
5855
+ * @param {boolean|undefined} test The test value (only when constant).
6162
5856
  * @returns {void}
6163
5857
  */
6164
- function startCodePath(origin) {
6165
- if (codePath) forwardCurrentToHead(analyzer, node);
6166
- codePath = analyzer.codePath = new CodePath({
6167
- id: analyzer.idGenerator.next(),
6168
- origin,
6169
- upper: codePath,
6170
- onLooped: analyzer.onLooped
6171
- });
6172
- state = CodePath.getState(codePath);
6173
- analyzer.emitter.emit("onCodePathStart", codePath, node);
5858
+ makeWhileTest(test) {
5859
+ const context = this.loopContext;
5860
+ const forkContext = this.forkContext;
5861
+ const testSegments = forkContext.makeNext(0, -1);
5862
+ context.test = test;
5863
+ context.continueDestSegments = testSegments;
5864
+ forkContext.replaceHead(testSegments);
6174
5865
  }
6175
- if (isPropertyDefinitionValue(node)) startCodePath("class-field-initializer");
6176
- switch (node.type) {
6177
- case "Program":
6178
- startCodePath("program");
6179
- break;
6180
- case "FunctionDeclaration":
6181
- case "ComponentDeclaration":
6182
- case "HookDeclaration":
6183
- case "FunctionExpression":
6184
- case "ArrowFunctionExpression":
6185
- startCodePath("function");
6186
- break;
6187
- case "StaticBlock":
6188
- startCodePath("class-static-block");
6189
- break;
6190
- case "ChainExpression":
6191
- state.pushChainContext();
6192
- break;
6193
- case "CallExpression":
6194
- if (node.optional === true) state.makeOptionalNode();
6195
- break;
6196
- case "MemberExpression":
6197
- if (node.optional === true) state.makeOptionalNode();
6198
- break;
6199
- case "LogicalExpression":
6200
- if (isHandledLogicalOperator(node.operator)) state.pushChoiceContext(node.operator, isForkingByTrueOrFalse(node));
6201
- break;
6202
- case "AssignmentExpression":
6203
- if (isLogicalAssignmentOperator(node.operator)) state.pushChoiceContext(node.operator.slice(0, -1), isForkingByTrueOrFalse(node));
6204
- break;
6205
- case "ConditionalExpression":
6206
- case "IfStatement":
6207
- state.pushChoiceContext("test", false);
6208
- break;
6209
- case "SwitchStatement":
6210
- state.pushSwitchContext(node.cases.some(isCaseNode), getLabel(node));
6211
- break;
6212
- case "TryStatement":
6213
- state.pushTryContext(Boolean(node.finalizer));
6214
- break;
6215
- case "SwitchCase":
6216
- if (parent.discriminant !== node && parent.cases[0] !== node) state.forkPath();
6217
- break;
6218
- case "WhileStatement":
6219
- case "DoWhileStatement":
6220
- case "ForStatement":
6221
- case "ForInStatement":
6222
- case "ForOfStatement":
6223
- state.pushLoopContext(node.type, getLabel(node));
6224
- break;
6225
- case "LabeledStatement":
6226
- if (!breakableTypePattern.test(node.body.type)) state.pushBreakContext(false, node.label.name);
6227
- break;
6228
- default: break;
5866
+ /**
5867
+ * Makes a code path segment for the body part of a WhileStatement.
5868
+ * @returns {void}
5869
+ */
5870
+ makeWhileBody() {
5871
+ const context = this.loopContext;
5872
+ const choiceContext = this.choiceContext;
5873
+ const forkContext = this.forkContext;
5874
+ if (!choiceContext.processed) {
5875
+ choiceContext.trueForkContext.add(forkContext.head);
5876
+ choiceContext.falseForkContext.add(forkContext.head);
5877
+ }
5878
+ if (context.test !== true) context.brokenForkContext.addAll(choiceContext.falseForkContext);
5879
+ forkContext.replaceHead(choiceContext.trueForkContext.makeNext(0, -1));
6229
5880
  }
6230
- forwardCurrentToHead(analyzer, node);
6231
- }
6232
- /**
6233
- * Updates the code path due to the type of a given node in leaving.
6234
- * @param {CodePathAnalyzer} analyzer The instance.
6235
- * @param {ASTNode} node The current AST node.
6236
- * @returns {void}
6237
- */
6238
- function processCodePathToExit(analyzer, node) {
6239
- const codePath = analyzer.codePath;
6240
- const state = CodePath.getState(codePath);
6241
- let dontForward = false;
6242
- switch (node.type) {
6243
- case "ChainExpression":
6244
- state.popChainContext();
6245
- break;
6246
- case "IfStatement":
6247
- case "ConditionalExpression":
6248
- state.popChoiceContext();
6249
- break;
6250
- case "LogicalExpression":
6251
- if (isHandledLogicalOperator(node.operator)) state.popChoiceContext();
6252
- break;
6253
- case "AssignmentExpression":
6254
- if (isLogicalAssignmentOperator(node.operator)) state.popChoiceContext();
6255
- break;
6256
- case "SwitchStatement":
6257
- state.popSwitchContext();
6258
- break;
6259
- case "SwitchCase":
6260
- if (node.consequent.length === 0) state.makeSwitchCaseBody(true, !node.test);
6261
- if (state.forkContext.reachable) dontForward = true;
6262
- break;
6263
- case "TryStatement":
6264
- state.popTryContext();
6265
- break;
6266
- case "BreakStatement":
6267
- forwardCurrentToHead(analyzer, node);
6268
- state.makeBreak(node.label && node.label.name);
6269
- dontForward = true;
6270
- break;
6271
- case "ContinueStatement":
6272
- forwardCurrentToHead(analyzer, node);
6273
- state.makeContinue(node.label && node.label.name);
6274
- dontForward = true;
6275
- break;
6276
- case "ReturnStatement":
6277
- forwardCurrentToHead(analyzer, node);
6278
- state.makeReturn();
6279
- dontForward = true;
6280
- break;
6281
- case "ThrowStatement":
6282
- forwardCurrentToHead(analyzer, node);
6283
- state.makeThrow();
6284
- dontForward = true;
6285
- break;
6286
- case "Identifier":
6287
- if (isIdentifierReference(node)) {
6288
- state.makeFirstThrowablePathInTryBlock();
6289
- dontForward = true;
6290
- }
6291
- break;
6292
- case "CallExpression":
6293
- case "ImportExpression":
6294
- case "MemberExpression":
6295
- case "NewExpression":
6296
- case "YieldExpression":
6297
- state.makeFirstThrowablePathInTryBlock();
6298
- break;
6299
- case "WhileStatement":
6300
- case "DoWhileStatement":
6301
- case "ForStatement":
6302
- case "ForInStatement":
6303
- case "ForOfStatement":
6304
- state.popLoopContext();
6305
- break;
6306
- case "AssignmentPattern":
6307
- state.popForkContext();
6308
- break;
6309
- case "LabeledStatement":
6310
- if (!breakableTypePattern.test(node.body.type)) state.popBreakContext();
6311
- break;
6312
- default: break;
5881
+ /**
5882
+ * Makes a code path segment for the body part of a DoWhileStatement.
5883
+ * @returns {void}
5884
+ */
5885
+ makeDoWhileBody() {
5886
+ const context = this.loopContext;
5887
+ const forkContext = this.forkContext;
5888
+ const bodySegments = forkContext.makeNext(-1, -1);
5889
+ context.entrySegments = bodySegments;
5890
+ forkContext.replaceHead(bodySegments);
5891
+ }
5892
+ /**
5893
+ * Makes a code path segment for the test part of a DoWhileStatement.
5894
+ * @param {boolean|undefined} test The test value (only when constant).
5895
+ * @returns {void}
5896
+ */
5897
+ makeDoWhileTest(test) {
5898
+ const context = this.loopContext;
5899
+ const forkContext = this.forkContext;
5900
+ context.test = test;
5901
+ if (!context.continueForkContext.empty) {
5902
+ context.continueForkContext.add(forkContext.head);
5903
+ const testSegments = context.continueForkContext.makeNext(0, -1);
5904
+ forkContext.replaceHead(testSegments);
5905
+ }
5906
+ }
5907
+ /**
5908
+ * Makes a code path segment for the test part of a ForStatement.
5909
+ * @param {boolean|undefined} test The test value (only when constant).
5910
+ * @returns {void}
5911
+ */
5912
+ makeForTest(test) {
5913
+ const context = this.loopContext;
5914
+ const forkContext = this.forkContext;
5915
+ const endOfInitSegments = forkContext.head;
5916
+ const testSegments = forkContext.makeNext(-1, -1);
5917
+ context.test = test;
5918
+ context.endOfInitSegments = endOfInitSegments;
5919
+ context.continueDestSegments = context.testSegments = testSegments;
5920
+ forkContext.replaceHead(testSegments);
5921
+ }
5922
+ /**
5923
+ * Makes a code path segment for the update part of a ForStatement.
5924
+ * @returns {void}
5925
+ */
5926
+ makeForUpdate() {
5927
+ const context = this.loopContext;
5928
+ const choiceContext = this.choiceContext;
5929
+ const forkContext = this.forkContext;
5930
+ if (context.testSegments) finalizeTestSegmentsOfFor(context, choiceContext, forkContext.head);
5931
+ else context.endOfInitSegments = forkContext.head;
5932
+ const updateSegments = forkContext.makeDisconnected(-1, -1);
5933
+ context.continueDestSegments = context.updateSegments = updateSegments;
5934
+ forkContext.replaceHead(updateSegments);
5935
+ }
5936
+ /**
5937
+ * Makes a code path segment for the body part of a ForStatement.
5938
+ * @returns {void}
5939
+ */
5940
+ makeForBody() {
5941
+ const context = this.loopContext;
5942
+ const choiceContext = this.choiceContext;
5943
+ const forkContext = this.forkContext;
5944
+ if (context.updateSegments) {
5945
+ context.endOfUpdateSegments = forkContext.head;
5946
+ if (context.testSegments) makeLooped(this, context.endOfUpdateSegments, context.testSegments);
5947
+ } else if (context.testSegments) finalizeTestSegmentsOfFor(context, choiceContext, forkContext.head);
5948
+ else context.endOfInitSegments = forkContext.head;
5949
+ let bodySegments = context.endOfTestSegments;
5950
+ if (!bodySegments) {
5951
+ const prevForkContext = ForkContext.newEmpty(forkContext);
5952
+ prevForkContext.add(context.endOfInitSegments);
5953
+ if (context.endOfUpdateSegments) prevForkContext.add(context.endOfUpdateSegments);
5954
+ bodySegments = prevForkContext.makeNext(0, -1);
5955
+ }
5956
+ context.continueDestSegments = context.continueDestSegments || bodySegments;
5957
+ forkContext.replaceHead(bodySegments);
6313
5958
  }
6314
- if (!dontForward) forwardCurrentToHead(analyzer, node);
6315
- }
6316
- /**
6317
- * Updates the code path to finalize the current code path.
6318
- * @param {CodePathAnalyzer} analyzer The instance.
6319
- * @param {ASTNode} node The current AST node.
6320
- * @returns {void}
6321
- */
6322
- function postprocess(analyzer, node) {
6323
5959
  /**
6324
- * Ends the code path for the current node.
5960
+ * Makes a code path segment for the left part of a ForInStatement and a
5961
+ * ForOfStatement.
6325
5962
  * @returns {void}
6326
5963
  */
6327
- function endCodePath() {
6328
- let codePath = analyzer.codePath;
6329
- CodePath.getState(codePath).makeFinal();
6330
- leaveFromCurrentSegment(analyzer, node);
6331
- analyzer.emitter.emit("onCodePathEnd", codePath, node);
6332
- codePath = analyzer.codePath = analyzer.codePath.upper;
5964
+ makeForInOfLeft() {
5965
+ const context = this.loopContext;
5966
+ const forkContext = this.forkContext;
5967
+ const leftSegments = forkContext.makeDisconnected(-1, -1);
5968
+ context.prevSegments = forkContext.head;
5969
+ context.leftSegments = context.continueDestSegments = leftSegments;
5970
+ forkContext.replaceHead(leftSegments);
6333
5971
  }
6334
- switch (node.type) {
6335
- case "Program":
6336
- case "FunctionDeclaration":
6337
- case "ComponentDeclaration":
6338
- case "HookDeclaration":
6339
- case "FunctionExpression":
6340
- case "ArrowFunctionExpression":
6341
- case "StaticBlock":
6342
- endCodePath();
6343
- break;
6344
- case "CallExpression":
6345
- if (node.optional === true && node.arguments.length === 0) CodePath.getState(analyzer.codePath).makeOptionalRight();
6346
- break;
6347
- default: break;
5972
+ /**
5973
+ * Makes a code path segment for the right part of a ForInStatement and a
5974
+ * ForOfStatement.
5975
+ * @returns {void}
5976
+ */
5977
+ makeForInOfRight() {
5978
+ const context = this.loopContext;
5979
+ const forkContext = this.forkContext;
5980
+ const temp = ForkContext.newEmpty(forkContext);
5981
+ temp.add(context.prevSegments);
5982
+ const rightSegments = temp.makeNext(-1, -1);
5983
+ context.endOfLeftSegments = forkContext.head;
5984
+ forkContext.replaceHead(rightSegments);
6348
5985
  }
6349
- if (isPropertyDefinitionValue(node)) endCodePath();
6350
- }
6351
- /**
6352
- * The class to analyze code paths.
6353
- * This class implements the EventGenerator interface.
6354
- */
6355
- var CodePathAnalyzer = class {
6356
5986
  /**
6357
- * @param {EventGenerator} eventGenerator An event generator to wrap.
5987
+ * Makes a code path segment for the body part of a ForInStatement and a
5988
+ * ForOfStatement.
5989
+ * @returns {void}
6358
5990
  */
6359
- constructor(emitters) {
6360
- this.emitter = { emit(event, ...args) {
6361
- emitters[event]?.(...args);
6362
- } };
6363
- this.codePath = null;
6364
- this.idGenerator = new IdGenerator("s");
6365
- this.currentNode = null;
6366
- this.onLooped = this.onLooped.bind(this);
5991
+ makeForInOfBody() {
5992
+ const context = this.loopContext;
5993
+ const forkContext = this.forkContext;
5994
+ const temp = ForkContext.newEmpty(forkContext);
5995
+ temp.add(context.endOfLeftSegments);
5996
+ const bodySegments = temp.makeNext(-1, -1);
5997
+ makeLooped(this, forkContext.head, context.leftSegments);
5998
+ context.brokenForkContext.add(forkContext.head);
5999
+ forkContext.replaceHead(bodySegments);
6367
6000
  }
6368
6001
  /**
6369
- * Does the process to enter a given AST node.
6370
- * This updates state of analysis and calls `enterNode` of the wrapped.
6371
- * @param {ASTNode} node A node which is entering.
6002
+ * Creates new context for BreakStatement.
6003
+ * @param {boolean} breakable The flag to indicate it can break by
6004
+ * an unlabeled BreakStatement.
6005
+ * @param {string|null} label The label of this context.
6006
+ * @returns {Object} The new context.
6007
+ */
6008
+ pushBreakContext(breakable, label) {
6009
+ this.breakContext = {
6010
+ upper: this.breakContext,
6011
+ breakable,
6012
+ label,
6013
+ brokenForkContext: ForkContext.newEmpty(this.forkContext)
6014
+ };
6015
+ return this.breakContext;
6016
+ }
6017
+ /**
6018
+ * Removes the top item of the break context stack.
6019
+ * @returns {Object} The removed context.
6020
+ */
6021
+ popBreakContext() {
6022
+ const context = this.breakContext;
6023
+ const forkContext = this.forkContext;
6024
+ this.breakContext = context.upper;
6025
+ if (!context.breakable) {
6026
+ const brokenForkContext = context.brokenForkContext;
6027
+ if (!brokenForkContext.empty) {
6028
+ brokenForkContext.add(forkContext.head);
6029
+ forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
6030
+ }
6031
+ }
6032
+ return context;
6033
+ }
6034
+ /**
6035
+ * Makes a path for a `break` statement.
6036
+ *
6037
+ * It registers the head segment to a context of `break`.
6038
+ * It makes new unreachable segment, then it set the head with the segment.
6039
+ * @param {string} label A label of the break statement.
6372
6040
  * @returns {void}
6373
6041
  */
6374
- enterNode(node) {
6375
- this.currentNode = node;
6376
- if (node.parent) preprocess(this, node);
6377
- processCodePathToEnter(this, node);
6378
- this.currentNode = null;
6042
+ makeBreak(label) {
6043
+ const forkContext = this.forkContext;
6044
+ if (!forkContext.reachable) return;
6045
+ const context = getBreakContext(this, label);
6046
+ if (context) context.brokenForkContext.add(forkContext.head);
6047
+ /* c8 ignore next */
6048
+ forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
6379
6049
  }
6380
6050
  /**
6381
- * Does the process to leave a given AST node.
6382
- * This updates state of analysis and calls `leaveNode` of the wrapped.
6383
- * @param {ASTNode} node A node which is leaving.
6051
+ * Makes a path for a `continue` statement.
6052
+ *
6053
+ * It makes a looping path.
6054
+ * It makes new unreachable segment, then it set the head with the segment.
6055
+ * @param {string} label A label of the continue statement.
6384
6056
  * @returns {void}
6385
6057
  */
6386
- leaveNode(node) {
6387
- this.currentNode = node;
6388
- processCodePathToExit(this, node);
6389
- postprocess(this, node);
6390
- this.currentNode = null;
6058
+ makeContinue(label) {
6059
+ const forkContext = this.forkContext;
6060
+ if (!forkContext.reachable) return;
6061
+ const context = getContinueContext(this, label);
6062
+ if (context) if (context.continueDestSegments) {
6063
+ makeLooped(this, forkContext.head, context.continueDestSegments);
6064
+ if (context.type === "ForInStatement" || context.type === "ForOfStatement") context.brokenForkContext.add(forkContext.head);
6065
+ } else context.continueForkContext.add(forkContext.head);
6066
+ forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
6391
6067
  }
6392
6068
  /**
6393
- * This is called on a code path looped.
6394
- * Then this raises a looped event.
6395
- * @param {CodePathSegment} fromSegment A segment of prev.
6396
- * @param {CodePathSegment} toSegment A segment of next.
6069
+ * Makes a path for a `return` statement.
6070
+ *
6071
+ * It registers the head segment to a context of `return`.
6072
+ * It makes new unreachable segment, then it set the head with the segment.
6397
6073
  * @returns {void}
6398
6074
  */
6399
- onLooped(fromSegment, toSegment) {
6400
- if (fromSegment.reachable && toSegment.reachable) this.emitter.emit("onCodePathSegmentLoop", fromSegment, toSegment, this.currentNode);
6075
+ makeReturn() {
6076
+ const forkContext = this.forkContext;
6077
+ if (forkContext.reachable) {
6078
+ getReturnContext(this).returnedForkContext.add(forkContext.head);
6079
+ forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
6080
+ }
6401
6081
  }
6402
- };
6403
-
6404
- //#endregion
6405
- //#region src/rules/rules-of-hooks.ts
6406
- /**
6407
- * Copyright (c) Meta Platforms, Inc. and affiliates.
6408
- *
6409
- * This source code is licensed under the MIT license found in the
6410
- * LICENSE file in the root directory of this source tree.
6411
- */
6412
- /**
6413
- * Catch all identifiers that begin with "use" followed by an uppercase Latin
6414
- * character to exclude identifiers like "user".
6415
- */
6416
- function isHookName(s) {
6417
- return s === "use" || /^use[A-Z0-9]/.test(s);
6418
- }
6419
- /**
6420
- * We consider hooks to be a hook name identifier or a member expression
6421
- * containing a hook name.
6422
- */
6423
- function isHook(node) {
6424
- if (node.type === "Identifier") return isHookName(node.name);
6425
- else if (node.type === "MemberExpression" && !node.computed && isHook(node.property)) {
6426
- const obj = node.object;
6427
- return obj.type === "Identifier" && /^[A-Z].*/.test(obj.name);
6428
- } else return false;
6429
- }
6430
- /**
6431
- * Checks if the node is a React component name. React component names must
6432
- * always start with an uppercase letter.
6433
- */
6434
- function isComponentName(node) {
6435
- return node.type === "Identifier" && /^[A-Z]/.test(node.name);
6436
- }
6437
- function isReactFunction(node, functionName) {
6438
- return "name" in node && node.name === functionName || node.type === "MemberExpression" && "name" in node.object && node.object.name === "React" && "name" in node.property && node.property.name === functionName;
6439
- }
6440
- /**
6441
- * Checks if the node is a callback argument of forwardRef. This render function
6442
- * should follow the rules of hooks.
6443
- */
6444
- function isForwardRefCallback(node) {
6445
- return !!(node.parent && "callee" in node.parent && node.parent.callee && isReactFunction(node.parent.callee, "forwardRef"));
6446
- }
6447
- /**
6448
- * Checks if the node is a callback argument of React.memo. This anonymous
6449
- * functional component should follow the rules of hooks.
6450
- */
6451
- function isMemoCallback(node) {
6452
- return !!(node.parent && "callee" in node.parent && node.parent.callee && isReactFunction(node.parent.callee, "memo"));
6453
- }
6454
- function isInsideComponentOrHook(node) {
6455
- while (node) {
6456
- const functionName = getFunctionName(node);
6457
- if (functionName) {
6458
- if (isComponentName(functionName) || isHook(functionName)) return true;
6082
+ /**
6083
+ * Makes a path for a `throw` statement.
6084
+ *
6085
+ * It registers the head segment to a context of `throw`.
6086
+ * It makes new unreachable segment, then it set the head with the segment.
6087
+ * @returns {void}
6088
+ */
6089
+ makeThrow() {
6090
+ const forkContext = this.forkContext;
6091
+ if (forkContext.reachable) {
6092
+ getThrowContext(this).thrownForkContext.add(forkContext.head);
6093
+ forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
6459
6094
  }
6460
- if (isForwardRefCallback(node) || isMemoCallback(node)) return true;
6461
- node = node.parent;
6462
6095
  }
6463
- return false;
6464
- }
6465
- function isInsideDoWhileLoop(node) {
6466
- while (node) {
6467
- if (node.type === "DoWhileStatement") return true;
6468
- node = node.parent;
6096
+ /**
6097
+ * Makes the final path.
6098
+ * @returns {void}
6099
+ */
6100
+ makeFinal() {
6101
+ const segments = this.currentSegments;
6102
+ if (segments.length > 0 && segments[0].reachable) this.returnedForkContext.add(segments);
6103
+ }
6104
+ };
6105
+ /**
6106
+ * A generator for unique ids.
6107
+ */
6108
+ var IdGenerator = class {
6109
+ /**
6110
+ * @param {string} prefix Optional. A prefix of generated ids.
6111
+ */
6112
+ constructor(prefix) {
6113
+ this.prefix = String(prefix);
6114
+ this.n = 0;
6469
6115
  }
6470
- return false;
6471
- }
6472
- function isInsideTryCatch(node) {
6473
- while (node) {
6474
- if (node.type === "TryStatement" || node.type === "CatchClause") return true;
6475
- node = node.parent;
6116
+ /**
6117
+ * Generates id.
6118
+ * @returns {string} A generated id.
6119
+ */
6120
+ next() {
6121
+ this.n = 1 + this.n | 0;
6122
+ /* c8 ignore start */
6123
+ if (this.n < 0) this.n = 1;
6124
+ return this.prefix + this.n;
6476
6125
  }
6477
- return false;
6478
- }
6479
- function getNodeWithoutReactNamespace(node) {
6480
- if (node.type === "MemberExpression" && node.object.type === "Identifier" && node.object.name === "React" && node.property.type === "Identifier" && !node.computed) return node.property;
6481
- return node;
6482
- }
6483
- function isEffectIdentifier(node, additionalHooks) {
6484
- if (node.type === "Identifier" && (node.name === "useEffect" || node.name === "useLayoutEffect" || node.name === "useInsertionEffect")) return true;
6485
- if (additionalHooks && node.type === "Identifier") return additionalHooks.test(node.name);
6486
- return false;
6487
- }
6488
- function isUseEffectEventIdentifier(node) {
6489
- return node.type === "Identifier" && node.name === "useEffectEvent";
6490
- }
6491
- function useEffectEventError(fn, called) {
6492
- if (fn === null) return "React Hook \"useEffectEvent\" can only be called at the top level of your component. It cannot be passed down.";
6493
- return `\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from Effects and Effect Events in the same component.` + (called ? "" : " It cannot be assigned to a variable or passed down.");
6494
- }
6495
- function isUseIdentifier(node) {
6496
- return isReactFunction(node, "use");
6497
- }
6498
- const rule = {
6499
- meta: {
6500
- type: "problem",
6501
- docs: {
6502
- description: "Enforces the Rules of Hooks.",
6503
- recommended: true,
6504
- url: "https://react.dev/reference/rules/rules-of-hooks"
6505
- },
6506
- schema: [{
6507
- type: "object",
6508
- additionalProperties: false,
6509
- properties: { additionalHooks: { type: "string" } }
6510
- }]
6511
- },
6512
- create(context) {
6513
- context.settings;
6514
- const rawOptions = context.options && context.options[0];
6515
- const additionalEffectHooks = rawOptions && rawOptions.additionalHooks ? new RegExp(rawOptions.additionalHooks) : getSettingsFromContext(context).additionalEffectHooks;
6516
- let lastEffect = null;
6517
- const codePathReactHooksMapStack = [];
6518
- const codePathSegmentStack = [];
6519
- const useEffectEventFunctions = /* @__PURE__ */ new WeakSet();
6520
- function recordAllUseEffectEventFunctions(scope) {
6521
- for (const reference of scope.references) {
6522
- const parent = reference.identifier.parent;
6523
- if (parent?.type === "VariableDeclarator" && parent.init && parent.init.type === "CallExpression" && parent.init.callee && isUseEffectEventIdentifier(parent.init.callee)) {
6524
- if (reference.resolved === null) throw new Error("Unexpected null reference.resolved");
6525
- for (const ref of reference.resolved.references) if (ref !== reference) useEffectEventFunctions.add(ref.identifier);
6526
- }
6527
- }
6528
- }
6126
+ };
6127
+ /**
6128
+ * A code path.
6129
+ */
6130
+ var CodePath = class {
6131
+ /**
6132
+ * Creates a new instance.
6133
+ * @param {Object} options Options for the function (see below).
6134
+ * @param {string} options.id An identifier.
6135
+ * @param {string} options.origin The type of code path origin.
6136
+ * @param {CodePath|null} options.upper The code path of the upper function scope.
6137
+ * @param {Function} options.onLooped A callback function to notify looping.
6138
+ */
6139
+ constructor({ id, origin, upper, onLooped }) {
6529
6140
  /**
6530
- * SourceCode that also works down to ESLint 3.0.0
6141
+ * The identifier of this code path.
6142
+ * Rules use it to store additional information of each rule.
6143
+ * @type {string}
6531
6144
  */
6532
- const getSourceCode = typeof context.getSourceCode === "function" ? () => {
6533
- return context.getSourceCode();
6534
- } : () => {
6535
- return context.sourceCode;
6536
- };
6145
+ this.id = id;
6537
6146
  /**
6538
- * SourceCode#getScope that also works down to ESLint 3.0.0
6147
+ * The reason that this code path was started. May be "program",
6148
+ * "function", "class-field-initializer", or "class-static-block".
6149
+ * @type {string}
6539
6150
  */
6540
- const getScope = typeof context.getScope === "function" ? () => {
6541
- return context.getScope();
6542
- } : (node) => {
6543
- return getSourceCode().getScope(node);
6544
- };
6545
- function hasFlowSuppression(node, suppression) {
6546
- const comments = getSourceCode().getAllComments();
6547
- const flowSuppressionRegex = new RegExp("\\$FlowFixMe\\[" + suppression + "\\]");
6548
- return comments.some((commentNode) => flowSuppressionRegex.test(commentNode.value) && commentNode.loc != null && node.loc != null && commentNode.loc.end.line === node.loc.start.line - 1);
6549
- }
6550
- const analyzer = new CodePathAnalyzer({
6551
- onCodePathSegmentStart: (segment) => codePathSegmentStack.push(segment),
6552
- onCodePathSegmentEnd: () => codePathSegmentStack.pop(),
6553
- onCodePathStart: () => codePathReactHooksMapStack.push(/* @__PURE__ */ new Map()),
6554
- onCodePathEnd(codePath, codePathNode) {
6555
- const reactHooksMap = codePathReactHooksMapStack.pop();
6556
- if (reactHooksMap?.size === 0) return;
6557
- else if (typeof reactHooksMap === "undefined") throw new Error("Unexpected undefined reactHooksMap");
6558
- const cyclic = /* @__PURE__ */ new Set();
6559
- /**
6560
- * Count the number of code paths from the start of the function to this
6561
- * segment. For example:
6562
- *
6563
- * ```js
6564
- * function MyComponent() {
6565
- * if (condition) {
6566
- * // Segment 1
6567
- * } else {
6568
- * // Segment 2
6569
- * }
6570
- * // Segment 3
6571
- * }
6572
- * ```
6573
- *
6574
- * Segments 1 and 2 have one path to the beginning of `MyComponent` and
6575
- * segment 3 has two paths to the beginning of `MyComponent` since we
6576
- * could have either taken the path of segment 1 or segment 2.
6577
- *
6578
- * Populates `cyclic` with cyclic segments.
6579
- */
6580
- function countPathsFromStart(segment, pathHistory) {
6581
- const { cache } = countPathsFromStart;
6582
- let paths = cache.get(segment.id);
6583
- const pathList = new Set(pathHistory);
6584
- if (pathList.has(segment.id)) {
6585
- const pathArray = [...pathList];
6586
- const cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
6587
- for (const cyclicSegment of cyclicSegments) cyclic.add(cyclicSegment);
6588
- return BigInt("0");
6589
- }
6590
- pathList.add(segment.id);
6591
- if (paths !== void 0) return paths;
6592
- if (codePath.thrownSegments.includes(segment)) paths = BigInt("0");
6593
- else if (segment.prevSegments.length === 0) paths = BigInt("1");
6594
- else {
6595
- paths = BigInt("0");
6596
- for (const prevSegment of segment.prevSegments) paths += countPathsFromStart(prevSegment, pathList);
6597
- }
6598
- if (segment.reachable && paths === BigInt("0")) cache.delete(segment.id);
6599
- else cache.set(segment.id, paths);
6600
- return paths;
6601
- }
6602
- /**
6603
- * Count the number of code paths from this segment to the end of the
6604
- * function. For example:
6605
- *
6606
- * ```js
6607
- * function MyComponent() {
6608
- * // Segment 1
6609
- * if (condition) {
6610
- * // Segment 2
6611
- * } else {
6612
- * // Segment 3
6613
- * }
6614
- * }
6615
- * ```
6616
- *
6617
- * Segments 2 and 3 have one path to the end of `MyComponent` and
6618
- * segment 1 has two paths to the end of `MyComponent` since we could
6619
- * either take the path of segment 1 or segment 2.
6620
- *
6621
- * Populates `cyclic` with cyclic segments.
6622
- */
6623
- function countPathsToEnd(segment, pathHistory) {
6624
- const { cache } = countPathsToEnd;
6625
- let paths = cache.get(segment.id);
6626
- const pathList = new Set(pathHistory);
6627
- if (pathList.has(segment.id)) {
6628
- const pathArray = Array.from(pathList);
6629
- const cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1);
6630
- for (const cyclicSegment of cyclicSegments) cyclic.add(cyclicSegment);
6631
- return BigInt("0");
6632
- }
6633
- pathList.add(segment.id);
6634
- if (paths !== void 0) return paths;
6635
- if (codePath.thrownSegments.includes(segment)) paths = BigInt("0");
6636
- else if (segment.nextSegments.length === 0) paths = BigInt("1");
6637
- else {
6638
- paths = BigInt("0");
6639
- for (const nextSegment of segment.nextSegments) paths += countPathsToEnd(nextSegment, pathList);
6640
- }
6641
- cache.set(segment.id, paths);
6642
- return paths;
6643
- }
6644
- /**
6645
- * Gets the shortest path length to the start of a code path.
6646
- * For example:
6647
- *
6648
- * ```js
6649
- * function MyComponent() {
6650
- * if (condition) {
6651
- * // Segment 1
6652
- * }
6653
- * // Segment 2
6654
- * }
6655
- * ```
6656
- *
6657
- * There is only one path from segment 1 to the code path start. Its
6658
- * length is one so that is the shortest path.
6659
- *
6660
- * There are two paths from segment 2 to the code path start. One
6661
- * through segment 1 with a length of two and another directly to the
6662
- * start with a length of one. The shortest path has a length of one
6663
- * so we would return that.
6664
- */
6665
- function shortestPathLengthToStart(segment) {
6666
- const { cache } = shortestPathLengthToStart;
6667
- let length = cache.get(segment.id);
6668
- if (length === null) return Infinity;
6669
- if (length !== void 0) return length;
6670
- cache.set(segment.id, null);
6671
- if (segment.prevSegments.length === 0) length = 1;
6672
- else {
6673
- length = Infinity;
6674
- for (const prevSegment of segment.prevSegments) {
6675
- const prevLength = shortestPathLengthToStart(prevSegment);
6676
- if (prevLength < length) length = prevLength;
6677
- }
6678
- length += 1;
6679
- }
6680
- cache.set(segment.id, length);
6681
- return length;
6151
+ this.origin = origin;
6152
+ /**
6153
+ * The code path of the upper function scope.
6154
+ * @type {CodePath|null}
6155
+ */
6156
+ this.upper = upper;
6157
+ /**
6158
+ * The code paths of nested function scopes.
6159
+ * @type {CodePath[]}
6160
+ */
6161
+ this.childCodePaths = [];
6162
+ Object.defineProperty(this, "internal", { value: new CodePathState(new IdGenerator(`${id}_`), onLooped) });
6163
+ if (upper) upper.childCodePaths.push(this);
6164
+ }
6165
+ /**
6166
+ * Gets the state of a given code path.
6167
+ * @param {CodePath} codePath A code path to get.
6168
+ * @returns {CodePathState} The state of the code path.
6169
+ */
6170
+ static getState(codePath) {
6171
+ return codePath.internal;
6172
+ }
6173
+ /**
6174
+ * The initial code path segment.
6175
+ * @type {CodePathSegment}
6176
+ */
6177
+ get initialSegment() {
6178
+ return this.internal.initialSegment;
6179
+ }
6180
+ /**
6181
+ * Final code path segments.
6182
+ * This array is a mix of `returnedSegments` and `thrownSegments`.
6183
+ * @type {CodePathSegment[]}
6184
+ */
6185
+ get finalSegments() {
6186
+ return this.internal.finalSegments;
6187
+ }
6188
+ /**
6189
+ * Final code path segments which is with `return` statements.
6190
+ * This array contains the last path segment if it's reachable.
6191
+ * Since the reachable last path returns `undefined`.
6192
+ * @type {CodePathSegment[]}
6193
+ */
6194
+ get returnedSegments() {
6195
+ return this.internal.returnedForkContext;
6196
+ }
6197
+ /**
6198
+ * Final code path segments which is with `throw` statements.
6199
+ * @type {CodePathSegment[]}
6200
+ */
6201
+ get thrownSegments() {
6202
+ return this.internal.thrownForkContext;
6203
+ }
6204
+ /**
6205
+ * Current code path segments.
6206
+ * @type {CodePathSegment[]}
6207
+ */
6208
+ get currentSegments() {
6209
+ return this.internal.currentSegments;
6210
+ }
6211
+ /**
6212
+ * Traverses all segments in this code path.
6213
+ *
6214
+ * codePath.traverseSegments(function(segment, controller) {
6215
+ * // do something.
6216
+ * });
6217
+ *
6218
+ * This method enumerates segments in order from the head.
6219
+ *
6220
+ * The `controller` object has two methods.
6221
+ *
6222
+ * - `controller.skip()` - Skip the following segments in this branch.
6223
+ * - `controller.break()` - Skip all following segments.
6224
+ * @param {Object} [options] Omittable.
6225
+ * @param {CodePathSegment} [options.first] The first segment to traverse.
6226
+ * @param {CodePathSegment} [options.last] The last segment to traverse.
6227
+ * @param {Function} callback A callback function.
6228
+ * @returns {void}
6229
+ */
6230
+ traverseSegments(options, callback) {
6231
+ let resolvedOptions;
6232
+ let resolvedCallback;
6233
+ if (typeof options === "function") {
6234
+ resolvedCallback = options;
6235
+ resolvedOptions = {};
6236
+ } else {
6237
+ resolvedOptions = options || {};
6238
+ resolvedCallback = callback;
6239
+ }
6240
+ const startSegment = resolvedOptions.first || this.internal.initialSegment;
6241
+ const lastSegment = resolvedOptions.last;
6242
+ let item = null;
6243
+ let index = 0;
6244
+ let end = 0;
6245
+ let segment = null;
6246
+ const visited = Object.create(null);
6247
+ const stack = [[startSegment, 0]];
6248
+ let skippedSegment = null;
6249
+ let broken = false;
6250
+ const controller = {
6251
+ skip() {
6252
+ if (stack.length <= 1) broken = true;
6253
+ else skippedSegment = stack[stack.length - 2][0];
6254
+ },
6255
+ break() {
6256
+ broken = true;
6257
+ }
6258
+ };
6259
+ /**
6260
+ * Checks a given previous segment has been visited.
6261
+ * @param {CodePathSegment} prevSegment A previous segment to check.
6262
+ * @returns {boolean} `true` if the segment has been visited.
6263
+ */
6264
+ function isVisited(prevSegment) {
6265
+ return visited[prevSegment.id] || segment.isLoopedPrevSegment(prevSegment);
6266
+ }
6267
+ while (stack.length > 0) {
6268
+ item = stack[stack.length - 1];
6269
+ segment = item[0];
6270
+ index = item[1];
6271
+ if (index === 0) {
6272
+ if (visited[segment.id]) {
6273
+ stack.pop();
6274
+ continue;
6682
6275
  }
6683
- countPathsFromStart.cache = /* @__PURE__ */ new Map();
6684
- countPathsToEnd.cache = /* @__PURE__ */ new Map();
6685
- shortestPathLengthToStart.cache = /* @__PURE__ */ new Map();
6686
- const allPathsFromStartToEnd = countPathsToEnd(codePath.initialSegment);
6687
- const codePathFunctionName = getFunctionName(codePathNode);
6688
- const isSomewhereInsideComponentOrHook = isInsideComponentOrHook(codePathNode);
6689
- const isDirectlyInsideComponentOrHook = codePathFunctionName ? isComponentName(codePathFunctionName) || isHook(codePathFunctionName) : isForwardRefCallback(codePathNode) || isMemoCallback(codePathNode);
6690
- let shortestFinalPathLength = Infinity;
6691
- for (const finalSegment of codePath.finalSegments) {
6692
- if (!finalSegment.reachable) continue;
6693
- const length = shortestPathLengthToStart(finalSegment);
6694
- if (length < shortestFinalPathLength) shortestFinalPathLength = length;
6276
+ if (segment !== startSegment && segment.prevSegments.length > 0 && !segment.prevSegments.every(isVisited)) {
6277
+ stack.pop();
6278
+ continue;
6695
6279
  }
6696
- for (const [segment, reactHooks] of reactHooksMap) {
6697
- if (!segment.reachable) continue;
6698
- const possiblyHasEarlyReturn = segment.nextSegments.length === 0 ? shortestFinalPathLength <= shortestPathLengthToStart(segment) : shortestFinalPathLength < shortestPathLengthToStart(segment);
6699
- const pathsFromStartToEnd = countPathsFromStart(segment) * countPathsToEnd(segment);
6700
- const cycled = cyclic.has(segment.id);
6701
- for (const hook of reactHooks) {
6702
- if (hasFlowSuppression(hook, "react-rule-hook")) continue;
6703
- if (isUseIdentifier(hook) && isInsideTryCatch(hook)) context.report({
6704
- node: hook,
6705
- message: `React Hook "${getSourceCode().getText(hook)}" cannot be called in a try/catch block.`
6706
- });
6707
- if ((cycled || isInsideDoWhileLoop(hook)) && !isUseIdentifier(hook)) context.report({
6708
- node: hook,
6709
- message: `React Hook "${getSourceCode().getText(hook)}" may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render.`
6710
- });
6711
- if (isDirectlyInsideComponentOrHook) {
6712
- if (codePathNode.async) context.report({
6713
- node: hook,
6714
- message: `React Hook "${getSourceCode().getText(hook)}" cannot be called in an async function.`
6715
- });
6716
- if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd && !isUseIdentifier(hook) && !isInsideDoWhileLoop(hook)) {
6717
- const message = `React Hook "${getSourceCode().getText(hook)}" is called conditionally. React Hooks must be called in the exact same order in every component render.` + (possiblyHasEarlyReturn ? " Did you accidentally call a React Hook after an early return?" : "");
6718
- context.report({
6719
- node: hook,
6720
- message
6721
- });
6722
- }
6723
- } else if (codePathNode.parent != null && (codePathNode.parent.type === "MethodDefinition" || codePathNode.parent.type === "ClassProperty" || codePathNode.parent.type === "PropertyDefinition") && codePathNode.parent.value === codePathNode) {
6724
- const message = `React Hook "${getSourceCode().getText(hook)}" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.`;
6725
- context.report({
6726
- node: hook,
6727
- message
6728
- });
6729
- } else if (codePathFunctionName) {
6730
- const message = `React Hook "${getSourceCode().getText(hook)}" is called in function "${getSourceCode().getText(codePathFunctionName)}" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use".`;
6731
- context.report({
6732
- node: hook,
6733
- message
6734
- });
6735
- } else if (codePathNode.type === "Program") {
6736
- const message = `React Hook "${getSourceCode().getText(hook)}" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function.`;
6737
- context.report({
6738
- node: hook,
6739
- message
6740
- });
6741
- } else if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) {
6742
- const message = `React Hook "${getSourceCode().getText(hook)}" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.`;
6743
- context.report({
6744
- node: hook,
6745
- message
6746
- });
6747
- }
6748
- }
6280
+ if (skippedSegment && segment.prevSegments.includes(skippedSegment)) skippedSegment = null;
6281
+ visited[segment.id] = true;
6282
+ if (!skippedSegment) {
6283
+ resolvedCallback.call(this, segment, controller);
6284
+ if (segment === lastSegment) controller.skip();
6285
+ if (broken) break;
6749
6286
  }
6750
6287
  }
6751
- });
6752
- return {
6753
- "*"(node) {
6754
- analyzer.enterNode(node);
6755
- },
6756
- "*:exit"(node) {
6757
- analyzer.leaveNode(node);
6758
- },
6759
- CallExpression(node) {
6760
- if (isHook(node.callee)) {
6761
- const reactHooksMap = last(codePathReactHooksMapStack);
6762
- const codePathSegment = last(codePathSegmentStack);
6763
- let reactHooks = reactHooksMap.get(codePathSegment);
6764
- if (!reactHooks) {
6765
- reactHooks = [];
6766
- reactHooksMap.set(codePathSegment, reactHooks);
6767
- }
6768
- reactHooks.push(node.callee);
6769
- }
6770
- const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee);
6771
- if ((isEffectIdentifier(nodeWithoutNamespace, additionalEffectHooks) || isUseEffectEventIdentifier(nodeWithoutNamespace)) && node.arguments.length > 0) lastEffect = node;
6772
- if (isUseEffectEventIdentifier(nodeWithoutNamespace) && node.parent?.type !== "VariableDeclarator" && node.parent?.type !== "ExpressionStatement") {
6773
- const message = useEffectEventError(null, false);
6774
- context.report({
6775
- node,
6776
- message
6777
- });
6778
- }
6779
- },
6780
- Identifier(node) {
6781
- if (lastEffect == null && useEffectEventFunctions.has(node)) {
6782
- const message = useEffectEventError(getSourceCode().getText(node), node.parent.type === "CallExpression");
6783
- context.report({
6784
- node,
6785
- message
6786
- });
6787
- }
6788
- },
6789
- "CallExpression:exit"(node) {
6790
- if (node === lastEffect) lastEffect = null;
6791
- },
6792
- FunctionDeclaration(node) {
6793
- if (isInsideComponentOrHook(node)) recordAllUseEffectEventFunctions(getScope(node));
6794
- },
6795
- ArrowFunctionExpression(node) {
6796
- if (isInsideComponentOrHook(node)) recordAllUseEffectEventFunctions(getScope(node));
6797
- },
6798
- ComponentDeclaration(node) {
6799
- recordAllUseEffectEventFunctions(getScope(node));
6800
- },
6801
- HookDeclaration(node) {
6802
- recordAllUseEffectEventFunctions(getScope(node));
6288
+ end = segment.nextSegments.length - 1;
6289
+ if (index < end) {
6290
+ item[1] += 1;
6291
+ stack.push([segment.nextSegments[index], 0]);
6292
+ } else if (index === end) {
6293
+ item[0] = segment.nextSegments[index];
6294
+ item[1] = 0;
6295
+ } else stack.pop();
6296
+ }
6297
+ }
6298
+ };
6299
+ const breakableTypePattern = /^(?:(?:Do)?While|For(?:In|Of)?|Switch)Statement$/u;
6300
+ /**
6301
+ * Checks whether or not a given node is a `case` node (not `default` node).
6302
+ * @param {ASTNode} node A `SwitchCase` node to check.
6303
+ * @returns {boolean} `true` if the node is a `case` node (not `default` node).
6304
+ */
6305
+ function isCaseNode(node) {
6306
+ return Boolean(node.test);
6307
+ }
6308
+ /**
6309
+ * Checks if a given node appears as the value of a PropertyDefinition node.
6310
+ * @param {ASTNode} node THe node to check.
6311
+ * @returns {boolean} `true` if the node is a PropertyDefinition value,
6312
+ * false if not.
6313
+ */
6314
+ function isPropertyDefinitionValue(node) {
6315
+ const parent = node.parent;
6316
+ return parent && parent.type === "PropertyDefinition" && parent.value === node;
6317
+ }
6318
+ /**
6319
+ * Checks whether the given logical operator is taken into account for the code
6320
+ * path analysis.
6321
+ * @param {string} operator The operator found in the LogicalExpression node
6322
+ * @returns {boolean} `true` if the operator is "&&" or "||" or "??"
6323
+ */
6324
+ function isHandledLogicalOperator(operator) {
6325
+ return operator === "&&" || operator === "||" || operator === "??";
6326
+ }
6327
+ /**
6328
+ * Checks whether the given assignment operator is a logical assignment operator.
6329
+ * Logical assignments are taken into account for the code path analysis
6330
+ * because of their short-circuiting semantics.
6331
+ * @param {string} operator The operator found in the AssignmentExpression node
6332
+ * @returns {boolean} `true` if the operator is "&&=" or "||=" or "??="
6333
+ */
6334
+ function isLogicalAssignmentOperator(operator) {
6335
+ return operator === "&&=" || operator === "||=" || operator === "??=";
6336
+ }
6337
+ /**
6338
+ * Gets the label if the parent node of a given node is a LabeledStatement.
6339
+ * @param {ASTNode} node A node to get.
6340
+ * @returns {string|null} The label or `null`.
6341
+ */
6342
+ function getLabel(node) {
6343
+ if (node.parent.type === "LabeledStatement") return node.parent.label.name;
6344
+ return null;
6345
+ }
6346
+ /**
6347
+ * Checks whether or not a given logical expression node goes different path
6348
+ * between the `true` case and the `false` case.
6349
+ * @param {ASTNode} node A node to check.
6350
+ * @returns {boolean} `true` if the node is a test of a choice statement.
6351
+ */
6352
+ function isForkingByTrueOrFalse(node) {
6353
+ const parent = node.parent;
6354
+ switch (parent.type) {
6355
+ case "ConditionalExpression":
6356
+ case "IfStatement":
6357
+ case "WhileStatement":
6358
+ case "DoWhileStatement":
6359
+ case "ForStatement": return parent.test === node;
6360
+ case "LogicalExpression": return isHandledLogicalOperator(parent.operator);
6361
+ case "AssignmentExpression": return isLogicalAssignmentOperator(parent.operator);
6362
+ default: return false;
6363
+ }
6364
+ }
6365
+ /**
6366
+ * Gets the boolean value of a given literal node.
6367
+ *
6368
+ * This is used to detect infinity loops (e.g. `while (true) {}`).
6369
+ * Statements preceded by an infinity loop are unreachable if the loop didn't
6370
+ * have any `break` statement.
6371
+ * @param {ASTNode} node A node to get.
6372
+ * @returns {boolean|undefined} a boolean value if the node is a Literal node,
6373
+ * otherwise `undefined`.
6374
+ */
6375
+ function getBooleanValueIfSimpleConstant(node) {
6376
+ if (node.type === "Literal") return Boolean(node.value);
6377
+ }
6378
+ /**
6379
+ * Checks that a given identifier node is a reference or not.
6380
+ *
6381
+ * This is used to detect the first throwable node in a `try` block.
6382
+ * @param {ASTNode} node An Identifier node to check.
6383
+ * @returns {boolean} `true` if the node is a reference.
6384
+ */
6385
+ function isIdentifierReference(node) {
6386
+ const parent = node.parent;
6387
+ switch (parent.type) {
6388
+ case "LabeledStatement":
6389
+ case "BreakStatement":
6390
+ case "ContinueStatement":
6391
+ case "ArrayPattern":
6392
+ case "RestElement":
6393
+ case "ImportSpecifier":
6394
+ case "ImportDefaultSpecifier":
6395
+ case "ImportNamespaceSpecifier":
6396
+ case "CatchClause": return false;
6397
+ case "FunctionDeclaration":
6398
+ case "ComponentDeclaration":
6399
+ case "HookDeclaration":
6400
+ case "FunctionExpression":
6401
+ case "ArrowFunctionExpression":
6402
+ case "ClassDeclaration":
6403
+ case "ClassExpression":
6404
+ case "VariableDeclarator": return parent.id !== node;
6405
+ case "Property":
6406
+ case "PropertyDefinition":
6407
+ case "MethodDefinition": return parent.key !== node || parent.computed || parent.shorthand;
6408
+ case "AssignmentPattern": return parent.key !== node;
6409
+ default: return true;
6410
+ }
6411
+ }
6412
+ /**
6413
+ * Updates the current segment with the head segment.
6414
+ * This is similar to local branches and tracking branches of git.
6415
+ *
6416
+ * To separate the current and the head is in order to not make useless segments.
6417
+ *
6418
+ * In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd"
6419
+ * events are fired.
6420
+ * @param {CodePathAnalyzer} analyzer The instance.
6421
+ * @param {ASTNode} node The current AST node.
6422
+ * @returns {void}
6423
+ */
6424
+ function forwardCurrentToHead(analyzer, node) {
6425
+ const codePath = analyzer.codePath;
6426
+ const state = CodePath.getState(codePath);
6427
+ const currentSegments = state.currentSegments;
6428
+ const headSegments = state.headSegments;
6429
+ const end = Math.max(currentSegments.length, headSegments.length);
6430
+ let i, currentSegment, headSegment;
6431
+ for (i = 0; i < end; ++i) {
6432
+ currentSegment = currentSegments[i];
6433
+ headSegment = headSegments[i];
6434
+ if (currentSegment !== headSegment && currentSegment) {
6435
+ if (currentSegment.reachable) analyzer.emitter.emit("onCodePathSegmentEnd", currentSegment, node);
6436
+ }
6437
+ }
6438
+ state.currentSegments = headSegments;
6439
+ for (i = 0; i < end; ++i) {
6440
+ currentSegment = currentSegments[i];
6441
+ headSegment = headSegments[i];
6442
+ if (currentSegment !== headSegment && headSegment) {
6443
+ CodePathSegment.markUsed(headSegment);
6444
+ if (headSegment.reachable) analyzer.emitter.emit("onCodePathSegmentStart", headSegment, node);
6445
+ }
6446
+ }
6447
+ }
6448
+ /**
6449
+ * Updates the current segment with empty.
6450
+ * This is called at the last of functions or the program.
6451
+ * @param {CodePathAnalyzer} analyzer The instance.
6452
+ * @param {ASTNode} node The current AST node.
6453
+ * @returns {void}
6454
+ */
6455
+ function leaveFromCurrentSegment(analyzer, node) {
6456
+ const state = CodePath.getState(analyzer.codePath);
6457
+ const currentSegments = state.currentSegments;
6458
+ for (let i = 0; i < currentSegments.length; ++i) {
6459
+ const currentSegment = currentSegments[i];
6460
+ if (currentSegment.reachable) analyzer.emitter.emit("onCodePathSegmentEnd", currentSegment, node);
6461
+ }
6462
+ state.currentSegments = [];
6463
+ }
6464
+ /**
6465
+ * Updates the code path due to the position of a given node in the parent node
6466
+ * thereof.
6467
+ *
6468
+ * For example, if the node is `parent.consequent`, this creates a fork from the
6469
+ * current path.
6470
+ * @param {CodePathAnalyzer} analyzer The instance.
6471
+ * @param {ASTNode} node The current AST node.
6472
+ * @returns {void}
6473
+ */
6474
+ function preprocess(analyzer, node) {
6475
+ const codePath = analyzer.codePath;
6476
+ const state = CodePath.getState(codePath);
6477
+ const parent = node.parent;
6478
+ switch (parent.type) {
6479
+ case "CallExpression":
6480
+ if (parent.optional === true && parent.arguments.length >= 1 && parent.arguments[0] === node) state.makeOptionalRight();
6481
+ break;
6482
+ case "MemberExpression":
6483
+ if (parent.optional === true && parent.property === node) state.makeOptionalRight();
6484
+ break;
6485
+ case "LogicalExpression":
6486
+ if (parent.right === node && isHandledLogicalOperator(parent.operator)) state.makeLogicalRight();
6487
+ break;
6488
+ case "AssignmentExpression":
6489
+ if (parent.right === node && isLogicalAssignmentOperator(parent.operator)) state.makeLogicalRight();
6490
+ break;
6491
+ case "ConditionalExpression":
6492
+ case "IfStatement":
6493
+ if (parent.consequent === node) state.makeIfConsequent();
6494
+ else if (parent.alternate === node) state.makeIfAlternate();
6495
+ break;
6496
+ case "SwitchCase":
6497
+ if (parent.consequent[0] === node) state.makeSwitchCaseBody(false, !parent.test);
6498
+ break;
6499
+ case "TryStatement":
6500
+ if (parent.handler === node) state.makeCatchBlock();
6501
+ else if (parent.finalizer === node) state.makeFinallyBlock();
6502
+ break;
6503
+ case "WhileStatement":
6504
+ if (parent.test === node) state.makeWhileTest(getBooleanValueIfSimpleConstant(node));
6505
+ else {
6506
+ assert(parent.body === node);
6507
+ state.makeWhileBody();
6508
+ }
6509
+ break;
6510
+ case "DoWhileStatement":
6511
+ if (parent.body === node) state.makeDoWhileBody();
6512
+ else {
6513
+ assert(parent.test === node);
6514
+ state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node));
6803
6515
  }
6804
- };
6516
+ break;
6517
+ case "ForStatement":
6518
+ if (parent.test === node) state.makeForTest(getBooleanValueIfSimpleConstant(node));
6519
+ else if (parent.update === node) state.makeForUpdate();
6520
+ else if (parent.body === node) state.makeForBody();
6521
+ break;
6522
+ case "ForInStatement":
6523
+ case "ForOfStatement":
6524
+ if (parent.left === node) state.makeForInOfLeft();
6525
+ else if (parent.right === node) state.makeForInOfRight();
6526
+ else {
6527
+ assert(parent.body === node);
6528
+ state.makeForInOfBody();
6529
+ }
6530
+ break;
6531
+ case "AssignmentPattern":
6532
+ if (parent.right === node) {
6533
+ state.pushForkContext();
6534
+ state.forkBypassPath();
6535
+ state.forkPath();
6536
+ }
6537
+ break;
6538
+ default: break;
6539
+ }
6540
+ }
6541
+ /**
6542
+ * Updates the code path due to the type of a given node in entering.
6543
+ * @param {CodePathAnalyzer} analyzer The instance.
6544
+ * @param {ASTNode} node The current AST node.
6545
+ * @returns {void}
6546
+ */
6547
+ function processCodePathToEnter(analyzer, node) {
6548
+ let codePath = analyzer.codePath;
6549
+ let state = codePath && CodePath.getState(codePath);
6550
+ const parent = node.parent;
6551
+ /**
6552
+ * Creates a new code path and trigger the onCodePathStart event
6553
+ * based on the currently selected node.
6554
+ * @param {string} origin The reason the code path was started.
6555
+ * @returns {void}
6556
+ */
6557
+ function startCodePath(origin) {
6558
+ if (codePath) forwardCurrentToHead(analyzer, node);
6559
+ codePath = analyzer.codePath = new CodePath({
6560
+ id: analyzer.idGenerator.next(),
6561
+ origin,
6562
+ upper: codePath,
6563
+ onLooped: analyzer.onLooped
6564
+ });
6565
+ state = CodePath.getState(codePath);
6566
+ analyzer.emitter.emit("onCodePathStart", codePath, node);
6805
6567
  }
6806
- };
6568
+ if (isPropertyDefinitionValue(node)) startCodePath("class-field-initializer");
6569
+ switch (node.type) {
6570
+ case "Program":
6571
+ startCodePath("program");
6572
+ break;
6573
+ case "FunctionDeclaration":
6574
+ case "ComponentDeclaration":
6575
+ case "HookDeclaration":
6576
+ case "FunctionExpression":
6577
+ case "ArrowFunctionExpression":
6578
+ startCodePath("function");
6579
+ break;
6580
+ case "StaticBlock":
6581
+ startCodePath("class-static-block");
6582
+ break;
6583
+ case "ChainExpression":
6584
+ state.pushChainContext();
6585
+ break;
6586
+ case "CallExpression":
6587
+ if (node.optional === true) state.makeOptionalNode();
6588
+ break;
6589
+ case "MemberExpression":
6590
+ if (node.optional === true) state.makeOptionalNode();
6591
+ break;
6592
+ case "LogicalExpression":
6593
+ if (isHandledLogicalOperator(node.operator)) state.pushChoiceContext(node.operator, isForkingByTrueOrFalse(node));
6594
+ break;
6595
+ case "AssignmentExpression":
6596
+ if (isLogicalAssignmentOperator(node.operator)) state.pushChoiceContext(node.operator.slice(0, -1), isForkingByTrueOrFalse(node));
6597
+ break;
6598
+ case "ConditionalExpression":
6599
+ case "IfStatement":
6600
+ state.pushChoiceContext("test", false);
6601
+ break;
6602
+ case "SwitchStatement":
6603
+ state.pushSwitchContext(node.cases.some(isCaseNode), getLabel(node));
6604
+ break;
6605
+ case "TryStatement":
6606
+ state.pushTryContext(Boolean(node.finalizer));
6607
+ break;
6608
+ case "SwitchCase":
6609
+ if (parent.discriminant !== node && parent.cases[0] !== node) state.forkPath();
6610
+ break;
6611
+ case "WhileStatement":
6612
+ case "DoWhileStatement":
6613
+ case "ForStatement":
6614
+ case "ForInStatement":
6615
+ case "ForOfStatement":
6616
+ state.pushLoopContext(node.type, getLabel(node));
6617
+ break;
6618
+ case "LabeledStatement":
6619
+ if (!breakableTypePattern.test(node.body.type)) state.pushBreakContext(false, node.label.name);
6620
+ break;
6621
+ default: break;
6622
+ }
6623
+ forwardCurrentToHead(analyzer, node);
6624
+ }
6807
6625
  /**
6808
- * Gets the static name of a function AST node. For function declarations it is
6809
- * easy. For anonymous function expressions it is much harder. If you search for
6810
- * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
6811
- * where JS gives anonymous function expressions names. We roughly detect the
6812
- * same AST nodes with some exceptions to better fit our use case.
6626
+ * Updates the code path due to the type of a given node in leaving.
6627
+ * @param {CodePathAnalyzer} analyzer The instance.
6628
+ * @param {ASTNode} node The current AST node.
6629
+ * @returns {void}
6813
6630
  */
6814
- function getFunctionName(node) {
6815
- if (node.type === "ComponentDeclaration" || node.type === "HookDeclaration" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression" && node.id) return node.id;
6816
- else if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") if (node.parent?.type === "VariableDeclarator" && node.parent.init === node) return node.parent.id;
6817
- else if (node.parent?.type === "AssignmentExpression" && node.parent.right === node && node.parent.operator === "=") return node.parent.left;
6818
- else if (node.parent?.type === "Property" && node.parent.value === node && !node.parent.computed) return node.parent.key;
6819
- else if (node.parent?.type === "AssignmentPattern" && node.parent.right === node && !node.parent.computed) return node.parent.left;
6820
- else return;
6821
- else return;
6631
+ function processCodePathToExit(analyzer, node) {
6632
+ const codePath = analyzer.codePath;
6633
+ const state = CodePath.getState(codePath);
6634
+ let dontForward = false;
6635
+ switch (node.type) {
6636
+ case "ChainExpression":
6637
+ state.popChainContext();
6638
+ break;
6639
+ case "IfStatement":
6640
+ case "ConditionalExpression":
6641
+ state.popChoiceContext();
6642
+ break;
6643
+ case "LogicalExpression":
6644
+ if (isHandledLogicalOperator(node.operator)) state.popChoiceContext();
6645
+ break;
6646
+ case "AssignmentExpression":
6647
+ if (isLogicalAssignmentOperator(node.operator)) state.popChoiceContext();
6648
+ break;
6649
+ case "SwitchStatement":
6650
+ state.popSwitchContext();
6651
+ break;
6652
+ case "SwitchCase":
6653
+ if (node.consequent.length === 0) state.makeSwitchCaseBody(true, !node.test);
6654
+ if (state.forkContext.reachable) dontForward = true;
6655
+ break;
6656
+ case "TryStatement":
6657
+ state.popTryContext();
6658
+ break;
6659
+ case "BreakStatement":
6660
+ forwardCurrentToHead(analyzer, node);
6661
+ state.makeBreak(node.label && node.label.name);
6662
+ dontForward = true;
6663
+ break;
6664
+ case "ContinueStatement":
6665
+ forwardCurrentToHead(analyzer, node);
6666
+ state.makeContinue(node.label && node.label.name);
6667
+ dontForward = true;
6668
+ break;
6669
+ case "ReturnStatement":
6670
+ forwardCurrentToHead(analyzer, node);
6671
+ state.makeReturn();
6672
+ dontForward = true;
6673
+ break;
6674
+ case "ThrowStatement":
6675
+ forwardCurrentToHead(analyzer, node);
6676
+ state.makeThrow();
6677
+ dontForward = true;
6678
+ break;
6679
+ case "Identifier":
6680
+ if (isIdentifierReference(node)) {
6681
+ state.makeFirstThrowablePathInTryBlock();
6682
+ dontForward = true;
6683
+ }
6684
+ break;
6685
+ case "CallExpression":
6686
+ case "ImportExpression":
6687
+ case "MemberExpression":
6688
+ case "NewExpression":
6689
+ case "YieldExpression":
6690
+ state.makeFirstThrowablePathInTryBlock();
6691
+ break;
6692
+ case "WhileStatement":
6693
+ case "DoWhileStatement":
6694
+ case "ForStatement":
6695
+ case "ForInStatement":
6696
+ case "ForOfStatement":
6697
+ state.popLoopContext();
6698
+ break;
6699
+ case "AssignmentPattern":
6700
+ state.popForkContext();
6701
+ break;
6702
+ case "LabeledStatement":
6703
+ if (!breakableTypePattern.test(node.body.type)) state.popBreakContext();
6704
+ break;
6705
+ default: break;
6706
+ }
6707
+ if (!dontForward) forwardCurrentToHead(analyzer, node);
6822
6708
  }
6823
6709
  /**
6824
- * Convenience function for peeking the last item in a stack.
6710
+ * Updates the code path to finalize the current code path.
6711
+ * @param {CodePathAnalyzer} analyzer The instance.
6712
+ * @param {ASTNode} node The current AST node.
6713
+ * @returns {void}
6825
6714
  */
6826
- function last(array) {
6827
- return array[array.length - 1];
6715
+ function postprocess(analyzer, node) {
6716
+ /**
6717
+ * Ends the code path for the current node.
6718
+ * @returns {void}
6719
+ */
6720
+ function endCodePath() {
6721
+ let codePath = analyzer.codePath;
6722
+ CodePath.getState(codePath).makeFinal();
6723
+ leaveFromCurrentSegment(analyzer, node);
6724
+ analyzer.emitter.emit("onCodePathEnd", codePath, node);
6725
+ codePath = analyzer.codePath = analyzer.codePath.upper;
6726
+ }
6727
+ switch (node.type) {
6728
+ case "Program":
6729
+ case "FunctionDeclaration":
6730
+ case "ComponentDeclaration":
6731
+ case "HookDeclaration":
6732
+ case "FunctionExpression":
6733
+ case "ArrowFunctionExpression":
6734
+ case "StaticBlock":
6735
+ endCodePath();
6736
+ break;
6737
+ case "CallExpression":
6738
+ if (node.optional === true && node.arguments.length === 0) CodePath.getState(analyzer.codePath).makeOptionalRight();
6739
+ break;
6740
+ default: break;
6741
+ }
6742
+ if (isPropertyDefinitionValue(node)) endCodePath();
6828
6743
  }
6744
+ /**
6745
+ * The class to analyze code paths.
6746
+ * This class implements the EventGenerator interface.
6747
+ */
6748
+ var CodePathAnalyzer = class {
6749
+ /**
6750
+ * @param {EventGenerator} eventGenerator An event generator to wrap.
6751
+ */
6752
+ constructor(emitters) {
6753
+ this.emitter = { emit(event, ...args) {
6754
+ emitters[event]?.(...args);
6755
+ } };
6756
+ this.codePath = null;
6757
+ this.idGenerator = new IdGenerator("s");
6758
+ this.currentNode = null;
6759
+ this.onLooped = this.onLooped.bind(this);
6760
+ }
6761
+ /**
6762
+ * Does the process to enter a given AST node.
6763
+ * This updates state of analysis and calls `enterNode` of the wrapped.
6764
+ * @param {ASTNode} node A node which is entering.
6765
+ * @returns {void}
6766
+ */
6767
+ enterNode(node) {
6768
+ this.currentNode = node;
6769
+ if (node.parent) preprocess(this, node);
6770
+ processCodePathToEnter(this, node);
6771
+ this.currentNode = null;
6772
+ }
6773
+ /**
6774
+ * Does the process to leave a given AST node.
6775
+ * This updates state of analysis and calls `leaveNode` of the wrapped.
6776
+ * @param {ASTNode} node A node which is leaving.
6777
+ * @returns {void}
6778
+ */
6779
+ leaveNode(node) {
6780
+ this.currentNode = node;
6781
+ processCodePathToExit(this, node);
6782
+ postprocess(this, node);
6783
+ this.currentNode = null;
6784
+ }
6785
+ /**
6786
+ * This is called on a code path looped.
6787
+ * Then this raises a looped event.
6788
+ * @param {CodePathSegment} fromSegment A segment of prev.
6789
+ * @param {CodePathSegment} toSegment A segment of next.
6790
+ * @returns {void}
6791
+ */
6792
+ onLooped(fromSegment, toSegment) {
6793
+ if (fromSegment.reachable && toSegment.reachable) this.emitter.emit("onCodePathSegmentLoop", fromSegment, toSegment, this.currentNode);
6794
+ }
6795
+ };
6829
6796
 
6830
6797
  //#endregion
6831
6798
  //#region src/rules/set-state-in-effect.ts