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.
- package/dist/index.js +2217 -2250
- 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.
|
|
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/
|
|
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
|
-
*
|
|
4412
|
-
*
|
|
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
|
|
4416
|
-
return
|
|
4402
|
+
function isHookName(s) {
|
|
4403
|
+
return s === "use" || /^use[A-Z0-9]/.test(s);
|
|
4417
4404
|
}
|
|
4418
4405
|
/**
|
|
4419
|
-
*
|
|
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
|
|
4576
|
-
return
|
|
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
|
-
*
|
|
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
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
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
|
-
*
|
|
4604
|
-
*
|
|
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
|
|
4612
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
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
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
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
|
-
|
|
4781
|
-
return null;
|
|
4456
|
+
return false;
|
|
4782
4457
|
}
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
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
|
-
|
|
4796
|
-
return null;
|
|
4463
|
+
return false;
|
|
4797
4464
|
}
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
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
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
*/
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
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
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
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
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
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
|
-
|
|
5264
|
-
|
|
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
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
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
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
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
|
-
*
|
|
5416
|
-
*
|
|
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
|
-
|
|
5420
|
-
|
|
5421
|
-
this.
|
|
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
|
-
*
|
|
5453
|
-
*
|
|
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
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
5469
|
-
|
|
5470
|
-
const
|
|
5471
|
-
|
|
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
|
-
*
|
|
5124
|
+
* Clears all segments in this context.
|
|
5481
5125
|
* @returns {void}
|
|
5482
5126
|
*/
|
|
5483
|
-
|
|
5484
|
-
|
|
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
|
-
*
|
|
5492
|
-
* @param {
|
|
5493
|
-
* @returns {
|
|
5131
|
+
* Creates the root fork context.
|
|
5132
|
+
* @param {IdGenerator} idGenerator An identifier generator for segments.
|
|
5133
|
+
* @returns {ForkContext} New fork context.
|
|
5494
5134
|
*/
|
|
5495
|
-
|
|
5496
|
-
const context =
|
|
5497
|
-
|
|
5498
|
-
context
|
|
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
|
-
*
|
|
5507
|
-
* @param {
|
|
5508
|
-
* @
|
|
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
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
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
|
-
*
|
|
5522
|
-
*
|
|
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
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
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
|
-
*
|
|
5536
|
-
* @
|
|
5324
|
+
* The head segments.
|
|
5325
|
+
* @type {CodePathSegment[]}
|
|
5537
5326
|
*/
|
|
5538
|
-
|
|
5539
|
-
|
|
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
|
-
*
|
|
5559
|
-
*
|
|
5560
|
-
* @
|
|
5331
|
+
* The parent forking context.
|
|
5332
|
+
* This is used for the root of new forks.
|
|
5333
|
+
* @type {ForkContext}
|
|
5561
5334
|
*/
|
|
5562
|
-
|
|
5563
|
-
const
|
|
5564
|
-
|
|
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
|
-
*
|
|
5572
|
-
*
|
|
5573
|
-
*
|
|
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
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
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
|
-
*
|
|
5586
|
-
*
|
|
5587
|
-
* @returns {void}
|
|
5350
|
+
* Pops and merges the last forking context.
|
|
5351
|
+
* @returns {ForkContext} The last context.
|
|
5588
5352
|
*/
|
|
5589
|
-
|
|
5590
|
-
const
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
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
|
|
5601
|
-
* @
|
|
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
|
-
|
|
5607
|
-
this.
|
|
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
|
-
*
|
|
5617
|
-
*
|
|
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
|
-
|
|
5620
|
-
|
|
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
|
-
*
|
|
5375
|
+
* Creates a context for ConditionalExpression, LogicalExpression, AssignmentExpression (logical assignments only),
|
|
5376
|
+
* IfStatement, WhileStatement, DoWhileStatement, or ForStatement.
|
|
5634
5377
|
*
|
|
5635
|
-
*
|
|
5636
|
-
*
|
|
5637
|
-
*
|
|
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
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
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
|
-
*
|
|
5650
|
-
*
|
|
5651
|
-
*
|
|
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
|
-
|
|
5420
|
+
popChoiceContext() {
|
|
5421
|
+
const context = this.choiceContext;
|
|
5422
|
+
this.choiceContext = context.upper;
|
|
5657
5423
|
const forkContext = this.forkContext;
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
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
|
|
5668
|
-
*
|
|
5669
|
-
*
|
|
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
|
-
|
|
5466
|
+
makeLogicalRight() {
|
|
5467
|
+
const context = this.choiceContext;
|
|
5674
5468
|
const forkContext = this.forkContext;
|
|
5675
|
-
if (
|
|
5676
|
-
|
|
5677
|
-
|
|
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
|
|
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
|
-
|
|
5507
|
+
makeIfConsequent() {
|
|
5508
|
+
const context = this.choiceContext;
|
|
5688
5509
|
const forkContext = this.forkContext;
|
|
5689
|
-
if (
|
|
5690
|
-
|
|
5691
|
-
|
|
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
|
|
5519
|
+
* Makes a code path segment of the `else` block.
|
|
5696
5520
|
* @returns {void}
|
|
5697
5521
|
*/
|
|
5698
|
-
|
|
5699
|
-
const
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
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
|
-
*
|
|
5719
|
-
*
|
|
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
|
-
|
|
5722
|
-
this.
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
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
|
-
*
|
|
5737
|
-
*
|
|
5738
|
-
*
|
|
5739
|
-
* @
|
|
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
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
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
|
-
*
|
|
5771
|
-
*
|
|
5772
|
-
*
|
|
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
|
-
|
|
5775
|
-
|
|
5559
|
+
makeOptionalNode() {
|
|
5560
|
+
if (this.chainContext) {
|
|
5561
|
+
this.chainContext.countChoiceContexts += 1;
|
|
5562
|
+
this.pushChoiceContext("??", false);
|
|
5563
|
+
}
|
|
5776
5564
|
}
|
|
5777
5565
|
/**
|
|
5778
|
-
*
|
|
5779
|
-
*
|
|
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
|
-
|
|
5782
|
-
|
|
5570
|
+
makeOptionalRight() {
|
|
5571
|
+
if (this.chainContext) this.makeLogicalRight();
|
|
5783
5572
|
}
|
|
5784
5573
|
/**
|
|
5785
|
-
*
|
|
5786
|
-
*
|
|
5787
|
-
*
|
|
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
|
-
|
|
5790
|
-
|
|
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
|
-
*
|
|
5794
|
-
*
|
|
5795
|
-
*
|
|
5796
|
-
*
|
|
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
|
-
|
|
5799
|
-
|
|
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
|
-
*
|
|
5803
|
-
* @
|
|
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
|
-
|
|
5806
|
-
|
|
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
|
-
*
|
|
5810
|
-
* @
|
|
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
|
-
|
|
5813
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
if (
|
|
5838
|
-
|
|
5839
|
-
|
|
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
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
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
|
-
*
|
|
6057
|
-
*
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
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
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
*
|
|
6073
|
-
*
|
|
6074
|
-
*
|
|
6075
|
-
*
|
|
6076
|
-
*
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
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
|
-
|
|
6145
|
-
|
|
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
|
-
*
|
|
6160
|
-
*
|
|
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
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
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
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
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
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
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
|
-
*
|
|
5960
|
+
* Makes a code path segment for the left part of a ForInStatement and a
|
|
5961
|
+
* ForOfStatement.
|
|
6325
5962
|
* @returns {void}
|
|
6326
5963
|
*/
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
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
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
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
|
-
*
|
|
5987
|
+
* Makes a code path segment for the body part of a ForInStatement and a
|
|
5988
|
+
* ForOfStatement.
|
|
5989
|
+
* @returns {void}
|
|
6358
5990
|
*/
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
this.
|
|
6366
|
-
|
|
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
|
-
*
|
|
6370
|
-
*
|
|
6371
|
-
*
|
|
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
|
-
|
|
6375
|
-
|
|
6376
|
-
if (
|
|
6377
|
-
|
|
6378
|
-
|
|
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
|
-
*
|
|
6382
|
-
*
|
|
6383
|
-
*
|
|
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
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
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
|
-
*
|
|
6394
|
-
*
|
|
6395
|
-
*
|
|
6396
|
-
*
|
|
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
|
-
|
|
6400
|
-
|
|
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
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
*
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
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
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
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
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
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
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
}
|
|
6488
|
-
function
|
|
6489
|
-
|
|
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
|
-
*
|
|
6141
|
+
* The identifier of this code path.
|
|
6142
|
+
* Rules use it to store additional information of each rule.
|
|
6143
|
+
* @type {string}
|
|
6531
6144
|
*/
|
|
6532
|
-
|
|
6533
|
-
return context.getSourceCode();
|
|
6534
|
-
} : () => {
|
|
6535
|
-
return context.sourceCode;
|
|
6536
|
-
};
|
|
6145
|
+
this.id = id;
|
|
6537
6146
|
/**
|
|
6538
|
-
*
|
|
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
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
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
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
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
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
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
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
}
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
}
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
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
|
-
*
|
|
6809
|
-
*
|
|
6810
|
-
*
|
|
6811
|
-
*
|
|
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
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
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
|
-
*
|
|
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
|
|
6827
|
-
|
|
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
|