oxlint-plugin-react-doctor 0.7.7-dev.30b7eeb → 0.7.7-dev.4190897
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 +2424 -327
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4573,6 +4573,23 @@ const hasPossibleStaticPropertyWrite = (identifier, propertyName, scopes) => {
|
|
|
4573
4573
|
return writtenPropertyName === null || writtenPropertyName === propertyName;
|
|
4574
4574
|
}));
|
|
4575
4575
|
};
|
|
4576
|
+
const canExecuteBefore = (candidateNode, referenceNode, scopes) => {
|
|
4577
|
+
const candidateBoundary = findExecutionBoundary(candidateNode);
|
|
4578
|
+
const referenceBoundary = findExecutionBoundary(referenceNode);
|
|
4579
|
+
if (!candidateBoundary || !referenceBoundary) return true;
|
|
4580
|
+
if (candidateBoundary === referenceBoundary) return candidateNode.range[0] < referenceNode.range[0];
|
|
4581
|
+
if (!isFunctionLike$1(candidateBoundary)) return true;
|
|
4582
|
+
return isFunctionSynchronouslyInvokedBefore(candidateBoundary, referenceNode, scopes);
|
|
4583
|
+
};
|
|
4584
|
+
const hasPossibleStaticPropertyWriteBefore = (identifier, propertyName, referenceNode, scopes) => {
|
|
4585
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
4586
|
+
return getPotentiallyAliasedSymbols(identifier, scopes).some((symbol) => symbol.references.some((reference) => {
|
|
4587
|
+
const writeTarget = getMemberWriteTarget(reference.identifier);
|
|
4588
|
+
if (!writeTarget || !canExecuteBefore(writeTarget, referenceNode, scopes)) return false;
|
|
4589
|
+
const writtenPropertyName = getResolvedStaticPropertyName(writeTarget, scopes);
|
|
4590
|
+
return writtenPropertyName === null || writtenPropertyName === propertyName;
|
|
4591
|
+
}));
|
|
4592
|
+
};
|
|
4576
4593
|
const hasPossibleStaticPropertyMutationOrEscape = (identifier, propertyName, scopes) => {
|
|
4577
4594
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
4578
4595
|
if (hasPossibleStaticPropertyWrite(identifier, propertyName, scopes)) return true;
|
|
@@ -10558,7 +10575,7 @@ const getAssignedName = (node) => {
|
|
|
10558
10575
|
}
|
|
10559
10576
|
return null;
|
|
10560
10577
|
};
|
|
10561
|
-
const isModuleExportsAssignment = (node) => {
|
|
10578
|
+
const isModuleExportsAssignment$1 = (node) => {
|
|
10562
10579
|
const parent = node.parent;
|
|
10563
10580
|
if (!parent || !isNodeOfType(parent, "AssignmentExpression")) return false;
|
|
10564
10581
|
const left = parent.left;
|
|
@@ -10717,7 +10734,7 @@ const displayName = defineRule({
|
|
|
10717
10734
|
}
|
|
10718
10735
|
const assignedName = getAssignedName(node);
|
|
10719
10736
|
if (assignedName && isReactComponentName(assignedName) && ignoreNamed) return;
|
|
10720
|
-
if (isModuleExportsAssignment(node) && !node.id) {
|
|
10737
|
+
if (isModuleExportsAssignment$1(node) && !node.id) {
|
|
10721
10738
|
reportAt(node);
|
|
10722
10739
|
return;
|
|
10723
10740
|
}
|
|
@@ -10739,7 +10756,7 @@ const displayName = defineRule({
|
|
|
10739
10756
|
reportAt(node);
|
|
10740
10757
|
return;
|
|
10741
10758
|
}
|
|
10742
|
-
if (isModuleExportsAssignment(node)) {
|
|
10759
|
+
if (isModuleExportsAssignment$1(node)) {
|
|
10743
10760
|
reportAt(node);
|
|
10744
10761
|
return;
|
|
10745
10762
|
}
|
|
@@ -11673,6 +11690,25 @@ const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(
|
|
|
11673
11690
|
//#region src/plugin/utils/is-event-handler-attribute.ts
|
|
11674
11691
|
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
|
|
11675
11692
|
//#endregion
|
|
11693
|
+
//#region src/plugin/utils/is-ast-descendant.ts
|
|
11694
|
+
/**
|
|
11695
|
+
* True when `inner` is `outer` itself or any descendant in the AST
|
|
11696
|
+
* `parent` chain. Walks `inner.parent` upward and stops at either a
|
|
11697
|
+
* match (`true`) or the chain's root (`false`).
|
|
11698
|
+
*
|
|
11699
|
+
* Was duplicated byte-identical in:
|
|
11700
|
+
* - exhaustive-deps-symbol-stability.ts
|
|
11701
|
+
* - semantic/closure-captures.ts
|
|
11702
|
+
*/
|
|
11703
|
+
const isAstDescendant = (inner, outer) => {
|
|
11704
|
+
let current = inner;
|
|
11705
|
+
while (current) {
|
|
11706
|
+
if (current === outer) return true;
|
|
11707
|
+
current = current.parent ?? null;
|
|
11708
|
+
}
|
|
11709
|
+
return false;
|
|
11710
|
+
};
|
|
11711
|
+
//#endregion
|
|
11676
11712
|
//#region src/plugin/utils/react-ref-origin.ts
|
|
11677
11713
|
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
11678
11714
|
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
@@ -12322,26 +12358,39 @@ const callbackReturnsCleanupForUsage = (callback, usage, context) => {
|
|
|
12322
12358
|
});
|
|
12323
12359
|
return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingCleanupReturns, context);
|
|
12324
12360
|
};
|
|
12361
|
+
const findDirectHandleGuardForRelease = (releaseCall, owner, usage, context) => {
|
|
12362
|
+
if (usage.handleKey === null) return null;
|
|
12363
|
+
const doesTestRequireLiveHandle = (test) => {
|
|
12364
|
+
if (resolveExpressionKey(test, context) === usage.handleKey) return true;
|
|
12365
|
+
const unwrappedTest = stripParenExpression(test);
|
|
12366
|
+
if (!isNodeOfType(unwrappedTest, "BinaryExpression") || unwrappedTest.operator !== "!=" && unwrappedTest.operator !== "!==") return false;
|
|
12367
|
+
const isNullishOperand = (operand) => {
|
|
12368
|
+
const unwrappedOperand = stripParenExpression(operand);
|
|
12369
|
+
return isNodeOfType(unwrappedOperand, "Literal") && unwrappedOperand.value === null || isNodeOfType(unwrappedOperand, "Identifier") && unwrappedOperand.name === "undefined" && context.scopes.isGlobalReference(unwrappedOperand);
|
|
12370
|
+
};
|
|
12371
|
+
return resolveExpressionKey(unwrappedTest.left, context) === usage.handleKey && isNullishOperand(unwrappedTest.right) || resolveExpressionKey(unwrappedTest.right, context) === usage.handleKey && isNullishOperand(unwrappedTest.left);
|
|
12372
|
+
};
|
|
12373
|
+
let ancestor = releaseCall.parent;
|
|
12374
|
+
while (ancestor && ancestor !== owner) {
|
|
12375
|
+
if (isNodeOfType(ancestor, "IfStatement")) {
|
|
12376
|
+
if (ancestor.alternate !== null || !doesTestRequireLiveHandle(ancestor.test) || !doMatchingNodesCoverEveryPathAfterUsage(ancestor.consequent, [releaseCall], context)) return null;
|
|
12377
|
+
return ancestor;
|
|
12378
|
+
}
|
|
12379
|
+
ancestor = ancestor.parent;
|
|
12380
|
+
}
|
|
12381
|
+
return null;
|
|
12382
|
+
};
|
|
12325
12383
|
const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
|
|
12326
12384
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
12327
12385
|
const functionCfg = context.cfg.cfgFor(callback);
|
|
12328
12386
|
const usageBlock = functionCfg?.blockOf(usage.node);
|
|
12329
12387
|
const usageStart = getRangeStart(usage.node);
|
|
12330
12388
|
if (!functionCfg || !usageBlock || usageStart === null) return false;
|
|
12331
|
-
const findHandleGuard = (releaseCall) => {
|
|
12332
|
-
if (usage.handleKey === null) return null;
|
|
12333
|
-
let ancestor = releaseCall.parent;
|
|
12334
|
-
while (ancestor && ancestor !== callback.body) {
|
|
12335
|
-
if (isNodeOfType(ancestor, "IfStatement")) return ancestor.alternate === null && resolveExpressionKey(ancestor.test, context) === usage.handleKey && getRangeStart(ancestor) !== null && (getRangeStart(ancestor) ?? usageStart) < usageStart ? ancestor : null;
|
|
12336
|
-
ancestor = ancestor.parent;
|
|
12337
|
-
}
|
|
12338
|
-
return null;
|
|
12339
|
-
};
|
|
12340
12389
|
const matchingReleaseAnchors = [];
|
|
12341
12390
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
12342
12391
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
12343
12392
|
const releaseStart = getRangeStart(child);
|
|
12344
|
-
const handleGuard =
|
|
12393
|
+
const handleGuard = findDirectHandleGuardForRelease(child, callback, usage, context);
|
|
12345
12394
|
if (releaseStart === null || releaseStart >= usageStart || functionCfg.blockOf(child) !== usageBlock && !handleGuard) return;
|
|
12346
12395
|
if (doesReleaseCallMatchUsage(child, usage, context)) {
|
|
12347
12396
|
matchingReleaseAnchors.push(handleGuard ?? child);
|
|
@@ -12371,12 +12420,12 @@ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
|
|
|
12371
12420
|
return didFindUnmountCleanup;
|
|
12372
12421
|
};
|
|
12373
12422
|
const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasRerunReleaseBeforeUsage(callback, usage, context) && hasStableUnmountCleanupForUsage(callback, usage, context);
|
|
12374
|
-
const collectBlockingBooleanStates = (expression, blockedExpressionValue, context) => {
|
|
12423
|
+
const collectBlockingBooleanStates = (expression, blockedExpressionValue, guardNode, context) => {
|
|
12375
12424
|
const unwrappedExpression = stripParenExpression(expression);
|
|
12376
|
-
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return collectBlockingBooleanStates(unwrappedExpression.argument, !blockedExpressionValue, context);
|
|
12425
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return collectBlockingBooleanStates(unwrappedExpression.argument, !blockedExpressionValue, guardNode, context);
|
|
12377
12426
|
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
|
|
12378
12427
|
if (!(unwrappedExpression.operator === "||" && blockedExpressionValue || unwrappedExpression.operator === "&&" && !blockedExpressionValue)) return [];
|
|
12379
|
-
return [...collectBlockingBooleanStates(unwrappedExpression.left, blockedExpressionValue, context), ...collectBlockingBooleanStates(unwrappedExpression.right, blockedExpressionValue, context)];
|
|
12428
|
+
return [...collectBlockingBooleanStates(unwrappedExpression.left, blockedExpressionValue, guardNode, context), ...collectBlockingBooleanStates(unwrappedExpression.right, blockedExpressionValue, guardNode, context)];
|
|
12380
12429
|
}
|
|
12381
12430
|
if (isNodeOfType(unwrappedExpression, "BinaryExpression") && [
|
|
12382
12431
|
"===",
|
|
@@ -12387,35 +12436,59 @@ const collectBlockingBooleanStates = (expression, blockedExpressionValue, contex
|
|
|
12387
12436
|
const leftValue = readStaticBoolean(unwrappedExpression.left);
|
|
12388
12437
|
const rightValue = readStaticBoolean(unwrappedExpression.right);
|
|
12389
12438
|
const booleanValue = leftValue ?? rightValue;
|
|
12390
|
-
const
|
|
12439
|
+
const comparedExpression = leftValue === null ? unwrappedExpression.left : unwrappedExpression.right;
|
|
12440
|
+
const comparedKey = resolveExpressionKey(comparedExpression, context);
|
|
12391
12441
|
if (booleanValue === null || comparedKey === null) return [];
|
|
12442
|
+
const isEquality = unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==";
|
|
12392
12443
|
return [{
|
|
12444
|
+
bindingIdentifier: isNodeOfType(comparedExpression, "Identifier") ? context.scopes.symbolFor(comparedExpression)?.bindingIdentifier ?? null : null,
|
|
12445
|
+
guardNode,
|
|
12393
12446
|
key: comparedKey,
|
|
12394
|
-
value:
|
|
12447
|
+
value: isEquality === blockedExpressionValue ? booleanValue : !booleanValue
|
|
12395
12448
|
}];
|
|
12396
12449
|
}
|
|
12397
12450
|
const expressionKey = resolveExpressionKey(unwrappedExpression, context);
|
|
12398
12451
|
return expressionKey === null ? [] : [{
|
|
12452
|
+
bindingIdentifier: isNodeOfType(unwrappedExpression, "Identifier") ? context.scopes.symbolFor(unwrappedExpression)?.bindingIdentifier ?? null : null,
|
|
12453
|
+
guardNode,
|
|
12399
12454
|
key: expressionKey,
|
|
12400
12455
|
value: blockedExpressionValue
|
|
12401
12456
|
}];
|
|
12402
12457
|
};
|
|
12403
|
-
const
|
|
12404
|
-
|
|
12405
|
-
|
|
12406
|
-
|
|
12458
|
+
const canNodeReachLaterNodeWithinFunction = (sourceNode, targetNode, owner, context) => {
|
|
12459
|
+
const functionCfg = context.cfg.cfgFor(owner);
|
|
12460
|
+
const sourceBlock = functionCfg?.blockOf(sourceNode);
|
|
12461
|
+
const targetBlock = functionCfg?.blockOf(targetNode);
|
|
12462
|
+
const sourceStart = getRangeStart(sourceNode);
|
|
12463
|
+
const targetStart = getRangeStart(targetNode);
|
|
12464
|
+
if (!functionCfg || !sourceBlock || !targetBlock || sourceStart === null || targetStart === null) return true;
|
|
12465
|
+
if (!isNodeReachableWithinFunction(sourceNode, context)) return false;
|
|
12466
|
+
if (sourceBlock === targetBlock) return sourceStart < targetStart;
|
|
12467
|
+
const visitedBlocks = new Set([sourceBlock]);
|
|
12468
|
+
const pendingBlocks = [sourceBlock];
|
|
12469
|
+
while (pendingBlocks.length > 0) {
|
|
12470
|
+
const currentBlock = pendingBlocks.pop();
|
|
12471
|
+
if (!currentBlock) break;
|
|
12472
|
+
for (const edge of currentBlock.successors) {
|
|
12473
|
+
if (edge.to === targetBlock) return true;
|
|
12474
|
+
if (visitedBlocks.has(edge.to)) continue;
|
|
12475
|
+
visitedBlocks.add(edge.to);
|
|
12476
|
+
pendingBlocks.push(edge.to);
|
|
12477
|
+
}
|
|
12478
|
+
}
|
|
12479
|
+
return false;
|
|
12407
12480
|
};
|
|
12408
12481
|
const collectDeferredUsageGuardStates = (callback, usageNode, context) => {
|
|
12409
12482
|
if (!isFunctionLike$1(callback) || callback.async) return [];
|
|
12410
12483
|
const guardStates = [];
|
|
12411
12484
|
walkAst(callback.body, (child) => {
|
|
12412
12485
|
if (child !== callback.body && isFunctionLike$1(child)) return false;
|
|
12413
|
-
if (isNodeOfType(child, "IfStatement") &&
|
|
12486
|
+
if (isNodeOfType(child, "IfStatement") && !child.alternate && !canNodeReachLaterNodeWithinFunction(child.consequent, usageNode, callback, context) && doMatchingNodesCoverEveryPathBeforeUsage(usageNode, [child], callback, context)) guardStates.push(...collectBlockingBooleanStates(child.test, true, child, context));
|
|
12414
12487
|
});
|
|
12415
12488
|
let descendant = usageNode;
|
|
12416
12489
|
let ancestor = descendant.parent;
|
|
12417
12490
|
while (ancestor && ancestor !== callback) {
|
|
12418
|
-
if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant) guardStates.push(...collectBlockingBooleanStates(ancestor.test, false, context));
|
|
12491
|
+
if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant) guardStates.push(...collectBlockingBooleanStates(ancestor.test, false, ancestor, context));
|
|
12419
12492
|
descendant = ancestor;
|
|
12420
12493
|
ancestor = ancestor.parent;
|
|
12421
12494
|
}
|
|
@@ -12424,7 +12497,7 @@ const collectDeferredUsageGuardStates = (callback, usageNode, context) => {
|
|
|
12424
12497
|
const cleanupReturnInvalidatesGuard = (cleanupReturn, guardState, context) => {
|
|
12425
12498
|
if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return false;
|
|
12426
12499
|
const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
|
|
12427
|
-
if (!cleanupFunction || !isFunctionLike$1(cleanupFunction) || cleanupFunction.async) return false;
|
|
12500
|
+
if (!cleanupFunction || !isFunctionLike$1(cleanupFunction) || cleanupFunction.async || cleanupFunction.generator) return false;
|
|
12428
12501
|
let didInvalidateGuard = false;
|
|
12429
12502
|
walkAst(cleanupFunction.body, (child) => {
|
|
12430
12503
|
if (didInvalidateGuard) return false;
|
|
@@ -12453,11 +12526,110 @@ const deferredUsageWritesGuardBeforeUsage = (callback, usageNode, guardState, co
|
|
|
12453
12526
|
});
|
|
12454
12527
|
return didWriteGuard;
|
|
12455
12528
|
};
|
|
12529
|
+
const canInterruptionReachUsageThroughCatch = (interruptionNode, usageNode, owner, context) => {
|
|
12530
|
+
let descendant = interruptionNode;
|
|
12531
|
+
let ancestor = descendant.parent;
|
|
12532
|
+
while (ancestor && ancestor !== owner) {
|
|
12533
|
+
if (isNodeOfType(ancestor, "TryStatement") && ancestor.block === descendant && ancestor.handler && canNodeReachLaterNodeWithinFunction(ancestor.handler.body, usageNode, owner, context)) return true;
|
|
12534
|
+
descendant = ancestor;
|
|
12535
|
+
ancestor = ancestor.parent;
|
|
12536
|
+
}
|
|
12537
|
+
return false;
|
|
12538
|
+
};
|
|
12539
|
+
const isEffectLocalLifecycleGuard = (callback, guardState, cleanupFunctions, context) => {
|
|
12540
|
+
if (!guardState.bindingIdentifier) return false;
|
|
12541
|
+
const guardSymbol = context.scopes.symbolFor(guardState.bindingIdentifier);
|
|
12542
|
+
if (!guardSymbol || guardSymbol.kind !== "let" && guardSymbol.kind !== "var" || !isNodeOfType(guardSymbol.declarationNode, "VariableDeclarator") || findEnclosingFunction$1(guardSymbol.declarationNode) !== callback) return false;
|
|
12543
|
+
return guardSymbol.references.every((reference) => {
|
|
12544
|
+
if (!isWithinAssignmentTarget(reference.identifier)) return true;
|
|
12545
|
+
const assignmentTarget = findTransparentExpressionRoot(reference.identifier);
|
|
12546
|
+
const assignment = assignmentTarget.parent;
|
|
12547
|
+
return isNodeOfType(assignment, "AssignmentExpression") && assignment.operator === "=" && assignment.left === assignmentTarget && readStaticBoolean(assignment.right) === guardState.value && cleanupFunctions.includes(findEnclosingFunction$1(assignment) ?? assignment);
|
|
12548
|
+
});
|
|
12549
|
+
};
|
|
12550
|
+
const hasPotentialInterruptionAfterGuard = (callback, guardState, usageNode, context) => {
|
|
12551
|
+
if (!isFunctionLike$1(callback)) return true;
|
|
12552
|
+
const guardStart = getRangeStart(guardState.guardNode);
|
|
12553
|
+
const usageStart = getRangeStart(usageNode);
|
|
12554
|
+
if (guardStart === null || usageStart === null) return true;
|
|
12555
|
+
let hasPotentialInterruption = false;
|
|
12556
|
+
walkAst(callback.body, (child) => {
|
|
12557
|
+
if (hasPotentialInterruption) return false;
|
|
12558
|
+
if (child !== callback.body && isFunctionLike$1(child)) return false;
|
|
12559
|
+
const childStart = getRangeStart(child);
|
|
12560
|
+
if (childStart === null || childStart <= guardStart || childStart >= usageStart) return;
|
|
12561
|
+
if (isNodeOfType(child, "CallExpression") || isNodeOfType(child, "AwaitExpression") || isNodeOfType(child, "YieldExpression")) {
|
|
12562
|
+
if (canNodeReachLaterNodeWithinFunction(child, usageNode, callback, context) || canInterruptionReachUsageThroughCatch(child, usageNode, callback, context)) {
|
|
12563
|
+
hasPotentialInterruption = true;
|
|
12564
|
+
return false;
|
|
12565
|
+
}
|
|
12566
|
+
}
|
|
12567
|
+
});
|
|
12568
|
+
return hasPotentialInterruption;
|
|
12569
|
+
};
|
|
12456
12570
|
const hasGuardedDeferredCleanup = (callback, usage, cleanupReturns, context) => {
|
|
12457
12571
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
12458
12572
|
const promiseChainCall = usageFunction ? getPromiseChainCallForCallback(usageFunction) : null;
|
|
12459
|
-
if (usage.handleKey === null || !usageFunction || usageFunction === callback || !promiseChainCall || !collectEffectInvokedFunctions(callback).has(usageFunction) || !doMatchingNodesCoverEveryPathAfterUsage(promiseChainCall, cleanupReturns, context)) return false;
|
|
12460
|
-
|
|
12573
|
+
if (usage.kind !== "timer" || usage.handleKey === null || !usageFunction || !isFunctionLike$1(usageFunction) || usageFunction === callback || usageFunction.async || usageFunction.generator || !isNodeOfType(usage.node, "CallExpression") || !isNodeOfType(usage.node.callee, "Identifier") || !context.scopes.isGlobalReference(usage.node.callee) || !promiseChainCall || !collectEffectInvokedFunctions(callback).has(usageFunction) || !doMatchingNodesCoverEveryPathAfterUsage(promiseChainCall, cleanupReturns, context)) return false;
|
|
12574
|
+
const usageExpression = findTransparentExpressionRoot(usage.node);
|
|
12575
|
+
const usageAssignment = usageExpression.parent;
|
|
12576
|
+
if (!isNodeOfType(usageAssignment, "AssignmentExpression") || usageAssignment.operator !== "=" || usageAssignment.right !== usageExpression || !isNodeOfType(usageAssignment.left, "Identifier")) return false;
|
|
12577
|
+
const handleSymbol = context.scopes.symbolFor(usageAssignment.left);
|
|
12578
|
+
if (!handleSymbol || handleSymbol.kind !== "let" && handleSymbol.kind !== "var" || !isNodeOfType(handleSymbol.declarationNode, "VariableDeclarator") || findEnclosingFunction$1(handleSymbol.declarationNode) !== callback) return false;
|
|
12579
|
+
const cleanupFunctions = cleanupReturns.flatMap((cleanupReturn) => {
|
|
12580
|
+
if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return [];
|
|
12581
|
+
const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
|
|
12582
|
+
return cleanupFunction && isFunctionLike$1(cleanupFunction) ? [cleanupFunction] : [];
|
|
12583
|
+
});
|
|
12584
|
+
if (cleanupFunctions.length !== cleanupReturns.length) return false;
|
|
12585
|
+
const globalReleaseProofsByCleanup = /* @__PURE__ */ new Map();
|
|
12586
|
+
for (const cleanupFunction of cleanupFunctions) {
|
|
12587
|
+
if (!isFunctionLike$1(cleanupFunction)) return false;
|
|
12588
|
+
const globalReleaseProofs = [];
|
|
12589
|
+
walkAst(cleanupFunction.body, (child) => {
|
|
12590
|
+
if (child !== cleanupFunction.body && isFunctionLike$1(child)) return false;
|
|
12591
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && context.scopes.isGlobalReference(child.callee) && doesReleaseCallMatchUsage(child, usage, context)) {
|
|
12592
|
+
const handleGuard = findDirectHandleGuardForRelease(child, cleanupFunction, usage, context);
|
|
12593
|
+
globalReleaseProofs.push({
|
|
12594
|
+
anchor: handleGuard ?? child,
|
|
12595
|
+
call: child,
|
|
12596
|
+
handleGuard
|
|
12597
|
+
});
|
|
12598
|
+
}
|
|
12599
|
+
});
|
|
12600
|
+
if (!doMatchingNodesCoverEveryPathFromFunctionEntry(cleanupFunction, globalReleaseProofs.map((releaseProof) => releaseProof.anchor), context)) return false;
|
|
12601
|
+
globalReleaseProofsByCleanup.set(cleanupFunction, globalReleaseProofs);
|
|
12602
|
+
}
|
|
12603
|
+
const handleAssignments = handleSymbol.references.filter((reference) => isWithinAssignmentTarget(reference.identifier));
|
|
12604
|
+
const hasUsageAssignment = handleAssignments.some((handleAssignment) => findTransparentExpressionRoot(handleAssignment.identifier).parent === usageAssignment);
|
|
12605
|
+
const hasUnsafeHandleAssignment = handleAssignments.some((handleAssignment) => {
|
|
12606
|
+
const assignmentTarget = findTransparentExpressionRoot(handleAssignment.identifier);
|
|
12607
|
+
const assignment = assignmentTarget.parent;
|
|
12608
|
+
if (assignment === usageAssignment) return false;
|
|
12609
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== assignmentTarget) return true;
|
|
12610
|
+
const assignedValue = stripParenExpression(assignment.right);
|
|
12611
|
+
if (!(isNodeOfType(assignedValue, "Literal") && assignedValue.value === null || isNodeOfType(assignedValue, "Identifier") && assignedValue.name === "undefined" && context.scopes.isGlobalReference(assignedValue))) return true;
|
|
12612
|
+
const cleanupFunction = findEnclosingFunction$1(assignment);
|
|
12613
|
+
const globalReleaseProofs = cleanupFunction ? globalReleaseProofsByCleanup.get(cleanupFunction) : void 0;
|
|
12614
|
+
return !(cleanupFunction && globalReleaseProofs && doMatchingNodesCoverEveryPathBeforeUsage(assignment, globalReleaseProofs.map((releaseProof) => releaseProof.handleGuard && isAstDescendant(assignment, releaseProof.handleGuard.consequent) ? releaseProof.call : releaseProof.anchor), cleanupFunction, context));
|
|
12615
|
+
});
|
|
12616
|
+
if (!hasUsageAssignment || hasUnsafeHandleAssignment) return false;
|
|
12617
|
+
let usageAncestor = usage.node.parent;
|
|
12618
|
+
while (usageAncestor && usageAncestor !== usageFunction) {
|
|
12619
|
+
if (isNodeOfType(usageAncestor, "ForStatement") || isNodeOfType(usageAncestor, "ForInStatement") || isNodeOfType(usageAncestor, "ForOfStatement") || isNodeOfType(usageAncestor, "WhileStatement") || isNodeOfType(usageAncestor, "DoWhileStatement")) return false;
|
|
12620
|
+
usageAncestor = usageAncestor.parent;
|
|
12621
|
+
}
|
|
12622
|
+
let hasPotentialInterruption = false;
|
|
12623
|
+
for (const argument of usage.node.arguments ?? []) walkAst(argument, (argumentChild) => {
|
|
12624
|
+
if (hasPotentialInterruption) return false;
|
|
12625
|
+
if (isFunctionLike$1(argumentChild)) return false;
|
|
12626
|
+
if (isNodeOfType(argumentChild, "CallExpression") || isNodeOfType(argumentChild, "AwaitExpression") || isNodeOfType(argumentChild, "YieldExpression")) {
|
|
12627
|
+
hasPotentialInterruption = true;
|
|
12628
|
+
return false;
|
|
12629
|
+
}
|
|
12630
|
+
});
|
|
12631
|
+
if (hasPotentialInterruption) return false;
|
|
12632
|
+
return collectDeferredUsageGuardStates(usageFunction, usage.node, context).some((guardState) => isEffectLocalLifecycleGuard(callback, guardState, cleanupFunctions, context) && !hasPotentialInterruptionAfterGuard(usageFunction, guardState, usage.node, context) && !deferredUsageWritesGuardBeforeUsage(usageFunction, usage.node, guardState, context) && cleanupReturns.every((cleanupReturn) => cleanupReturnInvalidatesGuard(cleanupReturn, guardState, context)));
|
|
12461
12633
|
};
|
|
12462
12634
|
const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
12463
12635
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
@@ -13023,7 +13195,7 @@ const effectNeedsCleanup = defineRule({
|
|
|
13023
13195
|
const hookName = getCalleeName$2(node) ?? "effect";
|
|
13024
13196
|
context.report({
|
|
13025
13197
|
node,
|
|
13026
|
-
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in ${hookName} without
|
|
13198
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in ${hookName} without guaranteed cleanup. Return a cleanup function that owns every allocation so it does not leak after unmount.`
|
|
13027
13199
|
});
|
|
13028
13200
|
},
|
|
13029
13201
|
FunctionDeclaration(node) {
|
|
@@ -14368,6 +14540,22 @@ const isReactHocCallbackArgument = (functionNode) => {
|
|
|
14368
14540
|
return calleeName !== null && REACT_HOC_NAMES.has(calleeName);
|
|
14369
14541
|
};
|
|
14370
14542
|
//#endregion
|
|
14543
|
+
//#region src/plugin/utils/is-outside-all-functions.ts
|
|
14544
|
+
const FUNCTION_SCOPE_KINDS = new Set([
|
|
14545
|
+
"function",
|
|
14546
|
+
"arrow-function",
|
|
14547
|
+
"method"
|
|
14548
|
+
]);
|
|
14549
|
+
const isOutsideAllFunctions = (symbol) => {
|
|
14550
|
+
let scope = symbol.scope;
|
|
14551
|
+
while (scope) {
|
|
14552
|
+
if (FUNCTION_SCOPE_KINDS.has(scope.kind)) return false;
|
|
14553
|
+
if (scope.kind === "module") return true;
|
|
14554
|
+
scope = scope.parent;
|
|
14555
|
+
}
|
|
14556
|
+
return true;
|
|
14557
|
+
};
|
|
14558
|
+
//#endregion
|
|
14371
14559
|
//#region src/plugin/rules/react-builtins/exhaustive-deps-low-level.ts
|
|
14372
14560
|
/**
|
|
14373
14561
|
* Lowest-level helpers consumed by both the main `exhaustive-deps`
|
|
@@ -14420,25 +14608,6 @@ const getHookName = (callee, scopes) => {
|
|
|
14420
14608
|
if (isNodeOfType(strippedCallee, "MemberExpression") && !strippedCallee.computed && isNodeOfType(strippedCallee.property, "Identifier")) return strippedCallee.property.name;
|
|
14421
14609
|
return null;
|
|
14422
14610
|
};
|
|
14423
|
-
const FUNCTION_SCOPE_KINDS = new Set([
|
|
14424
|
-
"function",
|
|
14425
|
-
"arrow-function",
|
|
14426
|
-
"method"
|
|
14427
|
-
]);
|
|
14428
|
-
/**
|
|
14429
|
-
* True for symbols declared at module scope (outside any function
|
|
14430
|
-
* scope). Module-scope bindings don't change between renders so they
|
|
14431
|
-
* don't need to live in dependency arrays.
|
|
14432
|
-
*/
|
|
14433
|
-
const isOutsideAllFunctions = (symbol) => {
|
|
14434
|
-
let scope = symbol.scope;
|
|
14435
|
-
while (scope) {
|
|
14436
|
-
if (FUNCTION_SCOPE_KINDS.has(scope.kind)) return false;
|
|
14437
|
-
if (scope.kind === "module") return true;
|
|
14438
|
-
scope = scope.parent ?? null;
|
|
14439
|
-
}
|
|
14440
|
-
return true;
|
|
14441
|
-
};
|
|
14442
14611
|
//#endregion
|
|
14443
14612
|
//#region src/plugin/rules/react-builtins/exhaustive-deps-messages.ts
|
|
14444
14613
|
const buildMissingDepMessage = (hookName, depName) => `\`${hookName}\` can run with a stale \`${depName}\` & show your users old data.`;
|
|
@@ -14457,7 +14626,7 @@ const buildAsyncEffectMessage = (hookName) => `\`${hookName}\` was given an asyn
|
|
|
14457
14626
|
const buildUnknownCallbackMessage = (hookName) => `\`${hookName}\`'s callback is defined elsewhere, so dependencies can't be checked and stale values can slip through.`;
|
|
14458
14627
|
const buildUnstableDepMessage = (hookName, depName) => `\`${depName}\` is rebuilt every render, so \`${hookName}\` runs every time.`;
|
|
14459
14628
|
const buildForwardedUnstableDepMessage = (depName) => `\`${depName}\` is rebuilt every render and reaches a Hook dependency inside this custom Hook.`;
|
|
14460
|
-
const buildSetStateWithoutDepsMessage = (hookName, setterName) => `\`${hookName}\` calls \`${setterName}\` with no dependency array, so it can
|
|
14629
|
+
const buildSetStateWithoutDepsMessage = (hookName, setterName) => `\`${hookName}\` calls \`${setterName}\` with a value that can change on every render and no dependency array, so it can keep triggering renders.`;
|
|
14461
14630
|
const buildRefCleanupMessage = (depName) => `Your cleanup may read the wrong node since the ref \`${depName}\` can change before it runs.`;
|
|
14462
14631
|
const buildAssignmentMessage = (name) => `Assigning to \`${name}\` inside a hook is thrown away after each render, so the next render reads the old value.`;
|
|
14463
14632
|
//#endregion
|
|
@@ -14476,25 +14645,6 @@ const resolveExhaustiveDepsSettings = (settings) => {
|
|
|
14476
14645
|
};
|
|
14477
14646
|
};
|
|
14478
14647
|
//#endregion
|
|
14479
|
-
//#region src/plugin/utils/is-ast-descendant.ts
|
|
14480
|
-
/**
|
|
14481
|
-
* True when `inner` is `outer` itself or any descendant in the AST
|
|
14482
|
-
* `parent` chain. Walks `inner.parent` upward and stops at either a
|
|
14483
|
-
* match (`true`) or the chain's root (`false`).
|
|
14484
|
-
*
|
|
14485
|
-
* Was duplicated byte-identical in:
|
|
14486
|
-
* - exhaustive-deps-symbol-stability.ts
|
|
14487
|
-
* - semantic/closure-captures.ts
|
|
14488
|
-
*/
|
|
14489
|
-
const isAstDescendant = (inner, outer) => {
|
|
14490
|
-
let current = inner;
|
|
14491
|
-
while (current) {
|
|
14492
|
-
if (current === outer) return true;
|
|
14493
|
-
current = current.parent ?? null;
|
|
14494
|
-
}
|
|
14495
|
-
return false;
|
|
14496
|
-
};
|
|
14497
|
-
//#endregion
|
|
14498
14648
|
//#region src/plugin/rules/react-builtins/exhaustive-deps-sole-writer-guard.ts
|
|
14499
14649
|
const EQUALITY_BINARY_OPERATORS = new Set(["===", "!=="]);
|
|
14500
14650
|
const getUseStateSetterSymbol = (stateSymbol, scopes) => {
|
|
@@ -15236,26 +15386,125 @@ const isExtraDepAllowedForHook = (hookName, node, scopes) => {
|
|
|
15236
15386
|
};
|
|
15237
15387
|
const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
|
|
15238
15388
|
const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
|
|
15239
|
-
const
|
|
15240
|
-
|
|
15241
|
-
|
|
15389
|
+
const FRESH_STATE_CONSTRUCTOR_NAMES = new Set([
|
|
15390
|
+
"Array",
|
|
15391
|
+
"Date",
|
|
15392
|
+
"Error",
|
|
15393
|
+
"Map",
|
|
15394
|
+
"Object",
|
|
15395
|
+
"RegExp",
|
|
15396
|
+
"Set",
|
|
15397
|
+
"WeakMap",
|
|
15398
|
+
"WeakSet"
|
|
15399
|
+
]);
|
|
15400
|
+
const getBindingIdentifier = (node) => {
|
|
15401
|
+
if (!node) return null;
|
|
15402
|
+
const candidate = isNodeOfType(node, "AssignmentPattern") ? node.left : node;
|
|
15403
|
+
return isNodeOfType(candidate, "Identifier") ? candidate : null;
|
|
15242
15404
|
};
|
|
15243
|
-
const
|
|
15244
|
-
const
|
|
15245
|
-
|
|
15246
|
-
const
|
|
15247
|
-
|
|
15248
|
-
|
|
15249
|
-
|
|
15250
|
-
|
|
15251
|
-
|
|
15252
|
-
|
|
15253
|
-
|
|
15405
|
+
const resolveStateSetterSymbol = (identifier, scopes) => {
|
|
15406
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
15407
|
+
let symbol = scopes.symbolFor(identifier);
|
|
15408
|
+
while (symbol?.kind === "const" && isNodeOfType(symbol.declarationNode, "VariableDeclarator") && symbol.declarationNode.id === symbol.bindingIdentifier && symbol.initializer) {
|
|
15409
|
+
if (visitedSymbolIds.has(symbol.id)) return null;
|
|
15410
|
+
visitedSymbolIds.add(symbol.id);
|
|
15411
|
+
const initializer = unwrapExpression$3(symbol.initializer);
|
|
15412
|
+
if (!isNodeOfType(initializer, "Identifier")) return symbol;
|
|
15413
|
+
symbol = scopes.symbolFor(initializer);
|
|
15414
|
+
}
|
|
15415
|
+
return symbol;
|
|
15416
|
+
};
|
|
15417
|
+
const getStateSetterDescriptor = (identifier, scopes) => {
|
|
15418
|
+
const setterSymbol = resolveStateSetterSymbol(identifier, scopes);
|
|
15419
|
+
if (!setterSymbol || !isNodeOfType(setterSymbol.declarationNode, "VariableDeclarator")) return null;
|
|
15420
|
+
const declarator = setterSymbol.declarationNode;
|
|
15421
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return null;
|
|
15422
|
+
const setterElement = declarator.id.elements?.[1];
|
|
15423
|
+
const setterBinding = isAstNode(setterElement) ? getBindingIdentifier(setterElement) : null;
|
|
15424
|
+
if (!setterBinding || setterBinding !== setterSymbol.bindingIdentifier) return null;
|
|
15425
|
+
const initializer = declarator.init ? unwrapExpression$3(declarator.init) : null;
|
|
15426
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression") || !isReactApiCall(initializer, "useState", scopes, {
|
|
15427
|
+
allowGlobalReactNamespace: true,
|
|
15428
|
+
allowUnboundBareCalls: true,
|
|
15429
|
+
resolveNamedAliases: true
|
|
15430
|
+
})) return null;
|
|
15431
|
+
const stateElement = declarator.id.elements?.[0];
|
|
15432
|
+
const stateBinding = isAstNode(stateElement) ? getBindingIdentifier(stateElement) : null;
|
|
15433
|
+
return {
|
|
15434
|
+
setterSymbol,
|
|
15435
|
+
stateSymbol: stateBinding ? scopes.symbolFor(stateBinding) : null
|
|
15436
|
+
};
|
|
15437
|
+
};
|
|
15438
|
+
const expressionReadsSymbol = (node, symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
15439
|
+
if (!symbol) return false;
|
|
15440
|
+
const candidate = unwrapExpression$3(node);
|
|
15441
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
15442
|
+
const reference = scopes.referenceFor(candidate);
|
|
15443
|
+
if (reference?.flag !== "write" && reference?.resolvedSymbol?.id === symbol.id) return true;
|
|
15444
|
+
const candidateSymbol = reference?.resolvedSymbol;
|
|
15445
|
+
if (candidateSymbol?.kind === "const" && candidateSymbol.initializer && !isOutsideAllFunctions(candidateSymbol) && !visitedSymbolIds.has(candidateSymbol.id)) {
|
|
15446
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
15447
|
+
nextVisitedSymbolIds.add(candidateSymbol.id);
|
|
15448
|
+
return expressionReadsSymbol(candidateSymbol.initializer, symbol, scopes, nextVisitedSymbolIds);
|
|
15254
15449
|
}
|
|
15450
|
+
return false;
|
|
15255
15451
|
}
|
|
15256
|
-
const
|
|
15257
|
-
|
|
15258
|
-
|
|
15452
|
+
const record = candidate;
|
|
15453
|
+
for (const key of Object.keys(record)) {
|
|
15454
|
+
if (key === "parent") continue;
|
|
15455
|
+
const child = record[key];
|
|
15456
|
+
if (Array.isArray(child)) {
|
|
15457
|
+
if (child.some((item) => isAstNode(item) && expressionReadsSymbol(item, symbol, scopes))) return true;
|
|
15458
|
+
} else if (isAstNode(child) && expressionReadsSymbol(child, symbol, scopes)) return true;
|
|
15459
|
+
}
|
|
15460
|
+
return false;
|
|
15461
|
+
};
|
|
15462
|
+
const isGuaranteedFreshStateValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
15463
|
+
const candidate = unwrapExpression$3(node);
|
|
15464
|
+
if (isNodeOfType(candidate, "ObjectExpression") || isNodeOfType(candidate, "ArrayExpression") || isNodeOfType(candidate, "JSXElement") || isNodeOfType(candidate, "JSXFragment") || isRegExpLiteral(candidate)) return true;
|
|
15465
|
+
if (isNodeOfType(candidate, "SequenceExpression")) {
|
|
15466
|
+
const finalExpression = candidate.expressions.at(-1);
|
|
15467
|
+
return Boolean(finalExpression && isGuaranteedFreshStateValue(finalExpression, scopes));
|
|
15468
|
+
}
|
|
15469
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) return isGuaranteedFreshStateValue(candidate.consequent, scopes, visitedSymbolIds) && isGuaranteedFreshStateValue(candidate.alternate, scopes, visitedSymbolIds);
|
|
15470
|
+
if (isNodeOfType(candidate, "LogicalExpression")) return isGuaranteedFreshStateValue(candidate.left, scopes, visitedSymbolIds) && isGuaranteedFreshStateValue(candidate.right, scopes, visitedSymbolIds);
|
|
15471
|
+
if (isNodeOfType(candidate, "CallExpression")) {
|
|
15472
|
+
const callee = unwrapExpression$3(candidate.callee);
|
|
15473
|
+
return isNodeOfType(callee, "Identifier") && scopes.isGlobalReference(callee) && (callee.name === "Array" || callee.name === "Object") && (callee.name === "Array" || candidate.arguments.length === 0);
|
|
15474
|
+
}
|
|
15475
|
+
if (isNodeOfType(candidate, "NewExpression")) {
|
|
15476
|
+
const callee = unwrapExpression$3(candidate.callee);
|
|
15477
|
+
return isNodeOfType(callee, "Identifier") && scopes.isGlobalReference(callee) && FRESH_STATE_CONSTRUCTOR_NAMES.has(callee.name) && (callee.name !== "Object" || candidate.arguments.length === 0);
|
|
15478
|
+
}
|
|
15479
|
+
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
15480
|
+
const symbol = scopes.symbolFor(candidate);
|
|
15481
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || isOutsideAllFunctions(symbol) || visitedSymbolIds.has(symbol.id)) return false;
|
|
15482
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
15483
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
15484
|
+
return isGuaranteedFreshStateValue(symbol.initializer, scopes, nextVisitedSymbolIds);
|
|
15485
|
+
};
|
|
15486
|
+
const isNonZeroLiteral = (node) => {
|
|
15487
|
+
const candidate = unwrapExpression$3(node);
|
|
15488
|
+
return isNodeOfType(candidate, "Literal") && (typeof candidate.value === "number" && candidate.value !== 0 || typeof candidate.value === "bigint" && candidate.value !== 0n || typeof candidate.value === "string" && candidate.value.length > 0);
|
|
15489
|
+
};
|
|
15490
|
+
const isProvablyChangingExpression = (node, previousValueSymbol, scopes) => {
|
|
15491
|
+
const candidate = unwrapExpression$3(node);
|
|
15492
|
+
if (isGuaranteedFreshStateValue(candidate, scopes)) return true;
|
|
15493
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) {
|
|
15494
|
+
const isConsequentChanging = isProvablyChangingExpression(candidate.consequent, previousValueSymbol, scopes);
|
|
15495
|
+
const isAlternateChanging = isProvablyChangingExpression(candidate.alternate, previousValueSymbol, scopes);
|
|
15496
|
+
return expressionReadsSymbol(candidate.test, previousValueSymbol, scopes) ? isConsequentChanging && isAlternateChanging : isConsequentChanging || isAlternateChanging;
|
|
15497
|
+
}
|
|
15498
|
+
if (!expressionReadsSymbol(candidate, previousValueSymbol, scopes)) return false;
|
|
15499
|
+
if (isNodeOfType(candidate, "UnaryExpression")) return candidate.operator === "!";
|
|
15500
|
+
if (isNodeOfType(candidate, "UpdateExpression")) return true;
|
|
15501
|
+
if (!isNodeOfType(candidate, "BinaryExpression")) return false;
|
|
15502
|
+
return (candidate.operator === "+" || candidate.operator === "-") && expressionReadsSymbol(candidate.left, previousValueSymbol, scopes) && isNonZeroLiteral(candidate.right);
|
|
15503
|
+
};
|
|
15504
|
+
const isFreshEqualityGuardUpdater = (node, previousValueSymbol, scopes) => {
|
|
15505
|
+
const candidate = unwrapExpression$3(node);
|
|
15506
|
+
if (!isNodeOfType(candidate, "ConditionalExpression")) return false;
|
|
15507
|
+
const test = unwrapExpression$3(candidate.test);
|
|
15259
15508
|
if (!isNodeOfType(test, "BinaryExpression") || ![
|
|
15260
15509
|
"===",
|
|
15261
15510
|
"!==",
|
|
@@ -15263,38 +15512,50 @@ const isConvergingFunctionalUpdater = (node, scopes) => {
|
|
|
15263
15512
|
"!="
|
|
15264
15513
|
].includes(test.operator)) return false;
|
|
15265
15514
|
const isPreviousValue = (expression) => {
|
|
15266
|
-
const
|
|
15267
|
-
return isNodeOfType(
|
|
15515
|
+
const expressionCandidate = unwrapExpression$3(expression);
|
|
15516
|
+
return isNodeOfType(expressionCandidate, "Identifier") && scopes.symbolFor(expressionCandidate)?.id === previousValueSymbol?.id;
|
|
15268
15517
|
};
|
|
15269
15518
|
let comparedValue = null;
|
|
15270
15519
|
if (isPreviousValue(test.left)) comparedValue = test.right;
|
|
15271
|
-
|
|
15272
|
-
if (!comparedValue) return false;
|
|
15273
|
-
|
|
15274
|
-
|
|
15275
|
-
const
|
|
15276
|
-
|
|
15277
|
-
|
|
15278
|
-
if (isNodeOfType(candidate, "Literal") && isNodeOfType(compared, "Literal")) return candidate.value === compared.value;
|
|
15279
|
-
return false;
|
|
15520
|
+
if (isPreviousValue(test.right)) comparedValue = test.left;
|
|
15521
|
+
if (!comparedValue || !isPotentiallyFreshComparedValue(comparedValue, scopes)) return false;
|
|
15522
|
+
const isComparedValue = (expression) => {
|
|
15523
|
+
const expressionCandidate = unwrapExpression$3(expression);
|
|
15524
|
+
const comparedCandidate = unwrapExpression$3(comparedValue);
|
|
15525
|
+
if (isNodeOfType(expressionCandidate, "Identifier")) return isNodeOfType(comparedCandidate, "Identifier") && scopes.symbolFor(expressionCandidate)?.id === scopes.symbolFor(comparedCandidate)?.id;
|
|
15526
|
+
return isNodeOfType(expressionCandidate, "Literal") && isNodeOfType(comparedCandidate, "Literal") && expressionCandidate.value === comparedCandidate.value;
|
|
15280
15527
|
};
|
|
15281
|
-
return test.operator === "===" || test.operator === "==" ? isPreviousValue(
|
|
15282
|
-
};
|
|
15283
|
-
const isGuardedStableSetterCall = (identifier, symbol, scopes) => {
|
|
15284
|
-
const parent = identifier.parent;
|
|
15285
|
-
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier || !isStableSetterLikeSymbol(symbol, scopes)) return false;
|
|
15286
|
-
const writtenValue = parent.arguments?.[0];
|
|
15287
|
-
return Boolean(writtenValue && isConvergingFunctionalUpdater(writtenValue, scopes));
|
|
15528
|
+
return test.operator === "===" || test.operator === "==" ? isPreviousValue(candidate.consequent) && isComparedValue(candidate.alternate) : isComparedValue(candidate.consequent) && isPreviousValue(candidate.alternate);
|
|
15288
15529
|
};
|
|
15289
|
-
const
|
|
15530
|
+
const isProvablyChangingFunctionalUpdater = (node, scopes) => {
|
|
15531
|
+
const updater = unwrapExpression$3(node);
|
|
15532
|
+
if (!isNodeOfType(updater, "ArrowFunctionExpression") && !isNodeOfType(updater, "FunctionExpression")) return false;
|
|
15533
|
+
const previousValueParameter = updater.params?.[0];
|
|
15534
|
+
if (!previousValueParameter || !isNodeOfType(previousValueParameter, "Identifier")) return false;
|
|
15535
|
+
const previousValueSymbol = scopes.symbolFor(previousValueParameter);
|
|
15536
|
+
if (!isNodeOfType(updater.body, "BlockStatement")) return isFreshEqualityGuardUpdater(updater.body, previousValueSymbol, scopes) || isProvablyChangingExpression(updater.body, previousValueSymbol, scopes);
|
|
15537
|
+
if (updater.body.body.length !== 1) return false;
|
|
15538
|
+
const soleStatement = updater.body.body[0];
|
|
15539
|
+
if (!isNodeOfType(soleStatement, "ReturnStatement") || !isAstNode(soleStatement.argument)) return false;
|
|
15540
|
+
return isFreshEqualityGuardUpdater(soleStatement.argument, previousValueSymbol, scopes) || isProvablyChangingExpression(soleStatement.argument, previousValueSymbol, scopes);
|
|
15541
|
+
};
|
|
15542
|
+
const isProvablyRenderChangingSetterCall = (identifier, scopes) => {
|
|
15543
|
+
const setterCall = identifier.parent;
|
|
15544
|
+
if (!setterCall || !isNodeOfType(setterCall, "CallExpression") || setterCall.callee !== identifier) return false;
|
|
15545
|
+
const descriptor = getStateSetterDescriptor(identifier, scopes);
|
|
15546
|
+
const writtenValue = setterCall.arguments?.[0];
|
|
15547
|
+
if (!descriptor || !writtenValue || !isAstNode(writtenValue)) return false;
|
|
15548
|
+
return isProvablyChangingFunctionalUpdater(writtenValue, scopes) ? true : isProvablyChangingExpression(writtenValue, descriptor.stateSymbol, scopes);
|
|
15549
|
+
};
|
|
15550
|
+
const findRenderChangingStateSetterName = (node, scopes) => {
|
|
15290
15551
|
let setterName = null;
|
|
15291
15552
|
const visit = (current) => {
|
|
15292
15553
|
if (setterName) return;
|
|
15293
15554
|
if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
|
|
15294
15555
|
if (isNodeOfType(current, "Identifier")) {
|
|
15295
|
-
const
|
|
15296
|
-
if (
|
|
15297
|
-
setterName =
|
|
15556
|
+
const descriptor = getStateSetterDescriptor(current, scopes);
|
|
15557
|
+
if (descriptor && isProvablyRenderChangingSetterCall(current, scopes)) {
|
|
15558
|
+
setterName = descriptor.setterSymbol.name;
|
|
15298
15559
|
return;
|
|
15299
15560
|
}
|
|
15300
15561
|
}
|
|
@@ -15621,7 +15882,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
15621
15882
|
}
|
|
15622
15883
|
if (!depsArgumentRaw) {
|
|
15623
15884
|
if (callbackToAnalyze && EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) {
|
|
15624
|
-
const setterName =
|
|
15885
|
+
const setterName = findRenderChangingStateSetterName(callbackToAnalyze, context.scopes);
|
|
15625
15886
|
if (setterName) {
|
|
15626
15887
|
context.report({
|
|
15627
15888
|
node: callbackToAnalyze,
|
|
@@ -20014,14 +20275,14 @@ const isIndexOfResultUsedAsMembershipTest = (node) => {
|
|
|
20014
20275
|
if (isNegativeOneLiteral(otherOperand)) return true;
|
|
20015
20276
|
return isZeroLiteral(otherOperand) && (parent.operator === ">=" || parent.operator === "<");
|
|
20016
20277
|
};
|
|
20017
|
-
const getTypeAnnotation = (node) => {
|
|
20278
|
+
const getTypeAnnotation$1 = (node) => {
|
|
20018
20279
|
if (!node || !("typeAnnotation" in node)) return null;
|
|
20019
20280
|
const annotation = node.typeAnnotation;
|
|
20020
20281
|
if (!annotation || !isNodeOfType(annotation, "TSTypeAnnotation")) return null;
|
|
20021
20282
|
return annotation.typeAnnotation;
|
|
20022
20283
|
};
|
|
20023
20284
|
const getDeclaredPropertyType = (members, propertyName) => {
|
|
20024
|
-
for (const member of members) if (isNodeOfType(member, "TSPropertySignature") && isNodeOfType(member.key, "Identifier") && member.key.name === propertyName) return getTypeAnnotation(member);
|
|
20285
|
+
for (const member of members) if (isNodeOfType(member, "TSPropertySignature") && isNodeOfType(member.key, "Identifier") && member.key.name === propertyName) return getTypeAnnotation$1(member);
|
|
20025
20286
|
return null;
|
|
20026
20287
|
};
|
|
20027
20288
|
const getArrayElementType = (typeNode) => {
|
|
@@ -20040,7 +20301,7 @@ const getDestructuredDeclaredType = (identifier) => {
|
|
|
20040
20301
|
const property = binding.bindingIdentifier.parent;
|
|
20041
20302
|
const objectPattern = property?.parent;
|
|
20042
20303
|
if (!isNodeOfType(property, "Property") || !isNodeOfType(property.key, "Identifier") || !isNodeOfType(objectPattern, "ObjectPattern")) return null;
|
|
20043
|
-
const propsType = getTypeAnnotation(objectPattern);
|
|
20304
|
+
const propsType = getTypeAnnotation$1(objectPattern);
|
|
20044
20305
|
if (!propsType) return null;
|
|
20045
20306
|
if (isNodeOfType(propsType, "TSTypeLiteral")) return getDeclaredPropertyType(propsType.members ?? [], property.key.name);
|
|
20046
20307
|
if (!isNodeOfType(propsType, "TSTypeReference") || !isNodeOfType(propsType.typeName, "Identifier")) return null;
|
|
@@ -20057,7 +20318,7 @@ const getIdentifierDeclaredType = (identifier, visitedBindingIdentifiers = /* @_
|
|
|
20057
20318
|
const binding = findVariableInitializer(identifier, identifier.name);
|
|
20058
20319
|
if (!binding || visitedBindingIdentifiers.has(binding.bindingIdentifier)) return null;
|
|
20059
20320
|
visitedBindingIdentifiers.add(binding.bindingIdentifier);
|
|
20060
|
-
const directType = getTypeAnnotation(binding.bindingIdentifier);
|
|
20321
|
+
const directType = getTypeAnnotation$1(binding.bindingIdentifier);
|
|
20061
20322
|
if (directType) return directType;
|
|
20062
20323
|
const initializer = binding.initializer;
|
|
20063
20324
|
if (isNodeOfType(initializer, "TSAsExpression") || isNodeOfType(initializer, "TSTypeAssertion") || isNodeOfType(initializer, "TSSatisfiesExpression")) return initializer.typeAnnotation;
|
|
@@ -27222,6 +27483,211 @@ const computeExternallyDriven = (analysis, declarator) => {
|
|
|
27222
27483
|
return hasDeferredCallSite;
|
|
27223
27484
|
};
|
|
27224
27485
|
//#endregion
|
|
27486
|
+
//#region src/plugin/rules/state-and-effects/utils/effect/is-proven-native-read-method.ts
|
|
27487
|
+
const ARRAY_READ_METHOD_NAMES$1 = new Set([
|
|
27488
|
+
"at",
|
|
27489
|
+
"concat",
|
|
27490
|
+
"entries",
|
|
27491
|
+
"every",
|
|
27492
|
+
"filter",
|
|
27493
|
+
"find",
|
|
27494
|
+
"findIndex",
|
|
27495
|
+
"findLast",
|
|
27496
|
+
"findLastIndex",
|
|
27497
|
+
"flat",
|
|
27498
|
+
"flatMap",
|
|
27499
|
+
"forEach",
|
|
27500
|
+
"includes",
|
|
27501
|
+
"indexOf",
|
|
27502
|
+
"join",
|
|
27503
|
+
"keys",
|
|
27504
|
+
"lastIndexOf",
|
|
27505
|
+
"map",
|
|
27506
|
+
"reduce",
|
|
27507
|
+
"reduceRight",
|
|
27508
|
+
"slice",
|
|
27509
|
+
"some",
|
|
27510
|
+
"toLocaleString",
|
|
27511
|
+
"toReversed",
|
|
27512
|
+
"toSorted",
|
|
27513
|
+
"toSpliced",
|
|
27514
|
+
"toString",
|
|
27515
|
+
"values",
|
|
27516
|
+
"with"
|
|
27517
|
+
]);
|
|
27518
|
+
const MAP_READ_METHOD_NAMES = new Set([
|
|
27519
|
+
"entries",
|
|
27520
|
+
"forEach",
|
|
27521
|
+
"get",
|
|
27522
|
+
"has",
|
|
27523
|
+
"keys",
|
|
27524
|
+
"values"
|
|
27525
|
+
]);
|
|
27526
|
+
const SET_READ_METHOD_NAMES = new Set([
|
|
27527
|
+
"difference",
|
|
27528
|
+
"entries",
|
|
27529
|
+
"forEach",
|
|
27530
|
+
"has",
|
|
27531
|
+
"intersection",
|
|
27532
|
+
"isDisjointFrom",
|
|
27533
|
+
"isSubsetOf",
|
|
27534
|
+
"isSupersetOf",
|
|
27535
|
+
"keys",
|
|
27536
|
+
"symmetricDifference",
|
|
27537
|
+
"union",
|
|
27538
|
+
"values"
|
|
27539
|
+
]);
|
|
27540
|
+
const FUNCTION_READ_METHOD_NAMES = new Set(["bind"]);
|
|
27541
|
+
const PROMISE_READ_METHOD_NAMES = new Set([
|
|
27542
|
+
"catch",
|
|
27543
|
+
"finally",
|
|
27544
|
+
"then"
|
|
27545
|
+
]);
|
|
27546
|
+
const STRING_READ_METHOD_NAMES$1 = new Set([
|
|
27547
|
+
"at",
|
|
27548
|
+
"charAt",
|
|
27549
|
+
"charCodeAt",
|
|
27550
|
+
"codePointAt",
|
|
27551
|
+
"concat",
|
|
27552
|
+
"endsWith",
|
|
27553
|
+
"includes",
|
|
27554
|
+
"indexOf",
|
|
27555
|
+
"isWellFormed",
|
|
27556
|
+
"lastIndexOf",
|
|
27557
|
+
"localeCompare",
|
|
27558
|
+
"match",
|
|
27559
|
+
"matchAll",
|
|
27560
|
+
"normalize",
|
|
27561
|
+
"padEnd",
|
|
27562
|
+
"padStart",
|
|
27563
|
+
"repeat",
|
|
27564
|
+
"replace",
|
|
27565
|
+
"replaceAll",
|
|
27566
|
+
"search",
|
|
27567
|
+
"slice",
|
|
27568
|
+
"split",
|
|
27569
|
+
"startsWith",
|
|
27570
|
+
"substring",
|
|
27571
|
+
"toLocaleLowerCase",
|
|
27572
|
+
"toLocaleUpperCase",
|
|
27573
|
+
"toLowerCase",
|
|
27574
|
+
"toString",
|
|
27575
|
+
"toUpperCase",
|
|
27576
|
+
"toWellFormed",
|
|
27577
|
+
"trim",
|
|
27578
|
+
"trimEnd",
|
|
27579
|
+
"trimStart",
|
|
27580
|
+
"valueOf"
|
|
27581
|
+
]);
|
|
27582
|
+
const NATIVE_TYPE_NAMES = new Set([
|
|
27583
|
+
"Array",
|
|
27584
|
+
"Map",
|
|
27585
|
+
"Promise",
|
|
27586
|
+
"PromiseLike",
|
|
27587
|
+
"ReadonlyArray",
|
|
27588
|
+
"ReadonlyMap",
|
|
27589
|
+
"ReadonlySet",
|
|
27590
|
+
"Set"
|
|
27591
|
+
]);
|
|
27592
|
+
const nativeTypeDeclarationsByProgram = /* @__PURE__ */ new WeakMap();
|
|
27593
|
+
const shadowedNativeTypeNamesByBinding = /* @__PURE__ */ new WeakMap();
|
|
27594
|
+
const isWithinNode = (node, ancestor) => {
|
|
27595
|
+
let current = node;
|
|
27596
|
+
while (current) {
|
|
27597
|
+
if (current === ancestor) return true;
|
|
27598
|
+
current = current.parent;
|
|
27599
|
+
}
|
|
27600
|
+
return false;
|
|
27601
|
+
};
|
|
27602
|
+
const findTypeDeclarationScope = (declaration) => {
|
|
27603
|
+
let current = declaration.parent;
|
|
27604
|
+
while (current) {
|
|
27605
|
+
if (isNodeOfType(current, "Program") || isNodeOfType(current, "BlockStatement") || isNodeOfType(current, "TSModuleBlock") || isNodeOfType(current, "StaticBlock")) return current;
|
|
27606
|
+
current = current.parent;
|
|
27607
|
+
}
|
|
27608
|
+
return null;
|
|
27609
|
+
};
|
|
27610
|
+
const getDeclaredTypeName = (declaration) => {
|
|
27611
|
+
if (!isNodeOfType(declaration, "ClassDeclaration") && !isNodeOfType(declaration, "TSEnumDeclaration") && !isNodeOfType(declaration, "TSImportEqualsDeclaration") && !isNodeOfType(declaration, "TSInterfaceDeclaration") && !isNodeOfType(declaration, "TSModuleDeclaration") && !isNodeOfType(declaration, "TSTypeAliasDeclaration")) return null;
|
|
27612
|
+
return declaration.id && isNodeOfType(declaration.id, "Identifier") ? declaration.id.name : null;
|
|
27613
|
+
};
|
|
27614
|
+
const getNativeTypeDeclarations = (program) => {
|
|
27615
|
+
const cachedDeclarations = nativeTypeDeclarationsByProgram.get(program);
|
|
27616
|
+
if (cachedDeclarations) return cachedDeclarations;
|
|
27617
|
+
const declarations = [];
|
|
27618
|
+
walkAst(program, (candidate) => {
|
|
27619
|
+
if (isNodeOfType(candidate, "ImportDeclaration")) {
|
|
27620
|
+
for (const specifier of candidate.specifiers) if (NATIVE_TYPE_NAMES.has(specifier.local.name)) declarations.push({
|
|
27621
|
+
name: specifier.local.name,
|
|
27622
|
+
scope: program
|
|
27623
|
+
});
|
|
27624
|
+
return;
|
|
27625
|
+
}
|
|
27626
|
+
const declaredTypeName = getDeclaredTypeName(candidate);
|
|
27627
|
+
if (!declaredTypeName || !NATIVE_TYPE_NAMES.has(declaredTypeName)) return;
|
|
27628
|
+
const declarationScope = findTypeDeclarationScope(candidate);
|
|
27629
|
+
if (declarationScope) declarations.push({
|
|
27630
|
+
name: declaredTypeName,
|
|
27631
|
+
scope: declarationScope
|
|
27632
|
+
});
|
|
27633
|
+
});
|
|
27634
|
+
nativeTypeDeclarationsByProgram.set(program, declarations);
|
|
27635
|
+
return declarations;
|
|
27636
|
+
};
|
|
27637
|
+
const getShadowedNativeTypeNames = (binding) => {
|
|
27638
|
+
const cachedNames = shadowedNativeTypeNamesByBinding.get(binding);
|
|
27639
|
+
if (cachedNames) return cachedNames;
|
|
27640
|
+
const shadowedNames = /* @__PURE__ */ new Set();
|
|
27641
|
+
for (const typeName of NATIVE_TYPE_NAMES) if (hasEnclosingTypeParameterNamed(binding, typeName)) shadowedNames.add(typeName);
|
|
27642
|
+
const program = findProgramRoot(binding);
|
|
27643
|
+
if (program) {
|
|
27644
|
+
for (const declaration of getNativeTypeDeclarations(program)) if (isWithinNode(binding, declaration.scope)) shadowedNames.add(declaration.name);
|
|
27645
|
+
}
|
|
27646
|
+
shadowedNativeTypeNamesByBinding.set(binding, shadowedNames);
|
|
27647
|
+
return shadowedNames;
|
|
27648
|
+
};
|
|
27649
|
+
const getParameterBinding = (definitionName) => {
|
|
27650
|
+
if (isNodeOfType(definitionName, "Identifier")) return definitionName;
|
|
27651
|
+
if (!isNodeOfType(definitionName, "AssignmentPattern")) return null;
|
|
27652
|
+
return isNodeOfType(definitionName.left, "Identifier") ? definitionName.left : null;
|
|
27653
|
+
};
|
|
27654
|
+
const getTypeAnnotation = (binding) => {
|
|
27655
|
+
if (!isNodeOfType(binding, "Identifier")) return null;
|
|
27656
|
+
const annotation = binding.typeAnnotation;
|
|
27657
|
+
return annotation && isNodeOfType(annotation, "TSTypeAnnotation") ? annotation.typeAnnotation : null;
|
|
27658
|
+
};
|
|
27659
|
+
const isNullishType = (typeNode) => isNodeOfType(typeNode, "TSNullKeyword") || isNodeOfType(typeNode, "TSUndefinedKeyword");
|
|
27660
|
+
const isNativeReadMethod = (typeNode, methodName, shadowedTypeNames) => {
|
|
27661
|
+
if (isNodeOfType(typeNode, "TSArrayType") || isNodeOfType(typeNode, "TSTupleType")) return ARRAY_READ_METHOD_NAMES$1.has(methodName);
|
|
27662
|
+
if (isNodeOfType(typeNode, "TSStringKeyword")) return STRING_READ_METHOD_NAMES$1.has(methodName);
|
|
27663
|
+
if (isNodeOfType(typeNode, "TSFunctionType")) return FUNCTION_READ_METHOD_NAMES.has(methodName);
|
|
27664
|
+
if (isNodeOfType(typeNode, "TSLiteralType") && isNodeOfType(typeNode.literal, "Literal") && typeof typeNode.literal.value === "string") return STRING_READ_METHOD_NAMES$1.has(methodName);
|
|
27665
|
+
if (isNodeOfType(typeNode, "TSTypeOperator") && typeNode.operator === "readonly" && typeNode.typeAnnotation) return isNativeReadMethod(typeNode.typeAnnotation, methodName, shadowedTypeNames);
|
|
27666
|
+
if (isNodeOfType(typeNode, "TSUnionType")) {
|
|
27667
|
+
let hasNonNullishMember = false;
|
|
27668
|
+
for (const memberType of typeNode.types) {
|
|
27669
|
+
if (isNullishType(memberType)) continue;
|
|
27670
|
+
hasNonNullishMember = true;
|
|
27671
|
+
if (!isNativeReadMethod(memberType, methodName, shadowedTypeNames)) return false;
|
|
27672
|
+
}
|
|
27673
|
+
return hasNonNullishMember;
|
|
27674
|
+
}
|
|
27675
|
+
if (!isNodeOfType(typeNode, "TSTypeReference") || !isNodeOfType(typeNode.typeName, "Identifier")) return false;
|
|
27676
|
+
if (shadowedTypeNames.has(typeNode.typeName.name)) return false;
|
|
27677
|
+
if (typeNode.typeName.name === "Array" || typeNode.typeName.name === "ReadonlyArray") return ARRAY_READ_METHOD_NAMES$1.has(methodName);
|
|
27678
|
+
if (typeNode.typeName.name === "Map" || typeNode.typeName.name === "ReadonlyMap") return MAP_READ_METHOD_NAMES.has(methodName);
|
|
27679
|
+
if (typeNode.typeName.name === "Set" || typeNode.typeName.name === "ReadonlySet") return SET_READ_METHOD_NAMES.has(methodName);
|
|
27680
|
+
if (typeNode.typeName.name === "Promise" || typeNode.typeName.name === "PromiseLike") return PROMISE_READ_METHOD_NAMES.has(methodName);
|
|
27681
|
+
return false;
|
|
27682
|
+
};
|
|
27683
|
+
const isProvenNativeReadMethod = (ref, methodName) => Boolean(ref.resolved?.defs.some((definition) => {
|
|
27684
|
+
if (definition.type !== "Parameter") return false;
|
|
27685
|
+
const parameterBinding = getParameterBinding(definition.name);
|
|
27686
|
+
if (!parameterBinding) return false;
|
|
27687
|
+
const typeAnnotation = getTypeAnnotation(parameterBinding);
|
|
27688
|
+
return Boolean(typeAnnotation && isNativeReadMethod(typeAnnotation, methodName, getShadowedNativeTypeNames(parameterBinding)));
|
|
27689
|
+
}));
|
|
27690
|
+
//#endregion
|
|
27225
27691
|
//#region src/plugin/rules/state-and-effects/utils/effect/react.ts
|
|
27226
27692
|
const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
27227
27693
|
const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
|
|
@@ -27448,7 +27914,132 @@ const isRefCurrent = (ref) => {
|
|
|
27448
27914
|
const isStateSetterCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isStateSetter(analysis, innerRef));
|
|
27449
27915
|
const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(analysis, ref) && isSynchronous(ref.identifier, effectFn) && !resolvesToAsyncFunction(ref);
|
|
27450
27916
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
27451
|
-
const
|
|
27917
|
+
const SYNCHRONOUS_CALLBACK_ARGUMENT_INDEX_BY_METHOD = new Map([
|
|
27918
|
+
["every", 0],
|
|
27919
|
+
["filter", 0],
|
|
27920
|
+
["find", 0],
|
|
27921
|
+
["findIndex", 0],
|
|
27922
|
+
["findLast", 0],
|
|
27923
|
+
["findLastIndex", 0],
|
|
27924
|
+
["flatMap", 0],
|
|
27925
|
+
["forEach", 0],
|
|
27926
|
+
["map", 0],
|
|
27927
|
+
["reduce", 0],
|
|
27928
|
+
["reduceRight", 0],
|
|
27929
|
+
["replace", 1],
|
|
27930
|
+
["replaceAll", 1],
|
|
27931
|
+
["some", 0]
|
|
27932
|
+
]);
|
|
27933
|
+
const addSimpleBindingNames = (pattern, names) => {
|
|
27934
|
+
if (isNodeOfType(pattern, "Identifier")) {
|
|
27935
|
+
if (names.has(pattern.name)) return false;
|
|
27936
|
+
names.add(pattern.name);
|
|
27937
|
+
return true;
|
|
27938
|
+
}
|
|
27939
|
+
if (isNodeOfType(pattern, "AssignmentPattern")) return addSimpleBindingNames(pattern.left, names);
|
|
27940
|
+
if (isNodeOfType(pattern, "RestElement")) return addSimpleBindingNames(pattern.argument, names);
|
|
27941
|
+
if (isNodeOfType(pattern, "ObjectPattern")) {
|
|
27942
|
+
let didAddName = false;
|
|
27943
|
+
for (const property of pattern.properties) didAddName = addSimpleBindingNames(isNodeOfType(property, "Property") ? property.value : property, names) || didAddName;
|
|
27944
|
+
return didAddName;
|
|
27945
|
+
}
|
|
27946
|
+
if (isNodeOfType(pattern, "ArrayPattern")) {
|
|
27947
|
+
let didAddName = false;
|
|
27948
|
+
for (const element of pattern.elements) if (element) didAddName = addSimpleBindingNames(element, names) || didAddName;
|
|
27949
|
+
return didAddName;
|
|
27950
|
+
}
|
|
27951
|
+
return false;
|
|
27952
|
+
};
|
|
27953
|
+
const getSimpleParameterNames = (callback) => {
|
|
27954
|
+
if (!isFunctionLike$1(callback)) return /* @__PURE__ */ new Set();
|
|
27955
|
+
const names = /* @__PURE__ */ new Set();
|
|
27956
|
+
for (const parameter of callback.params ?? []) addSimpleBindingNames(parameter, names);
|
|
27957
|
+
return names;
|
|
27958
|
+
};
|
|
27959
|
+
const inlineCallbackInvokesParameter = (callback) => {
|
|
27960
|
+
const parameterNames = getSimpleParameterNames(callback);
|
|
27961
|
+
if (parameterNames.size === 0 || !isFunctionLike$1(callback)) return false;
|
|
27962
|
+
let didAddAlias = true;
|
|
27963
|
+
while (didAddAlias) {
|
|
27964
|
+
didAddAlias = false;
|
|
27965
|
+
walkAst(callback.body, (candidate) => {
|
|
27966
|
+
if (candidate !== callback.body && isFunctionLike$1(candidate)) return false;
|
|
27967
|
+
if (isNodeOfType(candidate, "AssignmentExpression") && candidate.operator === "=" && isNodeOfType(candidate.left, "Identifier")) {
|
|
27968
|
+
const assignedValueRoot = getRootIdentifier$1(candidate.right);
|
|
27969
|
+
if (isNodeOfType(assignedValueRoot, "Identifier") && parameterNames.has(assignedValueRoot.name) && !parameterNames.has(candidate.left.name)) {
|
|
27970
|
+
parameterNames.add(candidate.left.name);
|
|
27971
|
+
didAddAlias = true;
|
|
27972
|
+
}
|
|
27973
|
+
return;
|
|
27974
|
+
}
|
|
27975
|
+
if (!isNodeOfType(candidate, "VariableDeclarator") || !candidate.init) return;
|
|
27976
|
+
const initializerRoot = getRootIdentifier$1(candidate.init);
|
|
27977
|
+
if (!isNodeOfType(initializerRoot, "Identifier") || !parameterNames.has(initializerRoot.name)) return;
|
|
27978
|
+
if (addSimpleBindingNames(candidate.id, parameterNames)) didAddAlias = true;
|
|
27979
|
+
});
|
|
27980
|
+
}
|
|
27981
|
+
let didInvokeParameter = false;
|
|
27982
|
+
walkAst(callback.body, (candidate) => {
|
|
27983
|
+
if (didInvokeParameter) return false;
|
|
27984
|
+
if (candidate !== callback.body && isFunctionLike$1(candidate)) return false;
|
|
27985
|
+
if (!isNodeOfType(candidate, "CallExpression")) return;
|
|
27986
|
+
const callee = stripParenExpression(candidate.callee);
|
|
27987
|
+
if (isNodeOfType(callee, "Identifier") && parameterNames.has(callee.name)) {
|
|
27988
|
+
didInvokeParameter = true;
|
|
27989
|
+
return false;
|
|
27990
|
+
}
|
|
27991
|
+
if (isNodeOfType(callee, "MemberExpression")) {
|
|
27992
|
+
const receiverRoot = getRootIdentifier$1(callee.object);
|
|
27993
|
+
if (receiverRoot && parameterNames.has(receiverRoot.name)) {
|
|
27994
|
+
didInvokeParameter = true;
|
|
27995
|
+
return false;
|
|
27996
|
+
}
|
|
27997
|
+
}
|
|
27998
|
+
});
|
|
27999
|
+
return didInvokeParameter;
|
|
28000
|
+
};
|
|
28001
|
+
const callbackInvokesPropCallback = (analysis, callback) => {
|
|
28002
|
+
if (!isFunctionLike$1(callback)) return false;
|
|
28003
|
+
let didInvokePropCallback = false;
|
|
28004
|
+
walkAst(callback.body, (candidate) => {
|
|
28005
|
+
if (didInvokePropCallback) return false;
|
|
28006
|
+
if (candidate !== callback.body && isFunctionLike$1(candidate)) return false;
|
|
28007
|
+
if (!isNodeOfType(candidate, "CallExpression")) return;
|
|
28008
|
+
if (getDownstreamRefs(analysis, stripParenExpression(candidate.callee)).some((reference) => isPropCallbackInvocationRef(analysis, reference))) {
|
|
28009
|
+
didInvokePropCallback = true;
|
|
28010
|
+
return false;
|
|
28011
|
+
}
|
|
28012
|
+
});
|
|
28013
|
+
return didInvokePropCallback;
|
|
28014
|
+
};
|
|
28015
|
+
const isNativeMethodSuppressionSafe = (analysis, callExpression, methodName, scopes) => {
|
|
28016
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return false;
|
|
28017
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
28018
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
28019
|
+
const receiver = stripParenExpression(callee.object);
|
|
28020
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
28021
|
+
const receiverSymbol = scopes.symbolFor(receiver);
|
|
28022
|
+
if (!receiverSymbol || hasSymbolWriteBefore(receiverSymbol, callExpression, scopes) || hasPossibleStaticPropertyWriteBefore(receiver, methodName, callExpression, scopes)) return false;
|
|
28023
|
+
let callResult = callExpression;
|
|
28024
|
+
let callResultConsumer = callResult.parent;
|
|
28025
|
+
while (callResultConsumer && (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(callResultConsumer.type) && "expression" in callResultConsumer && callResultConsumer.expression === callResult || isNodeOfType(callResultConsumer, "MemberExpression") && callResultConsumer.object === callResult)) {
|
|
28026
|
+
callResult = callResultConsumer;
|
|
28027
|
+
callResultConsumer = callResult.parent;
|
|
28028
|
+
}
|
|
28029
|
+
if (isNodeOfType(callResultConsumer, "CallExpression") && callResultConsumer.callee === callResult) return false;
|
|
28030
|
+
const callbackArgumentIndex = SYNCHRONOUS_CALLBACK_ARGUMENT_INDEX_BY_METHOD.get(methodName);
|
|
28031
|
+
if (callbackArgumentIndex === void 0) return true;
|
|
28032
|
+
const callbackArgument = callExpression.arguments?.[callbackArgumentIndex];
|
|
28033
|
+
if (!callbackArgument) return true;
|
|
28034
|
+
const callbackValue = stripParenExpression(callbackArgument);
|
|
28035
|
+
if (isFunctionLike$1(callbackValue)) return !inlineCallbackInvokesParameter(callbackValue) && !callbackInvokesPropCallback(analysis, callbackValue);
|
|
28036
|
+
if (!isNodeOfType(callbackValue, "Identifier")) return false;
|
|
28037
|
+
const callbackRef = getRef(analysis, callbackValue);
|
|
28038
|
+
if (!callbackRef || isPropAlias(analysis, callbackRef)) return false;
|
|
28039
|
+
const callbackFunction = resolveToFunction(callbackRef);
|
|
28040
|
+
return Boolean(callbackFunction && isFunctionLike$1(callbackFunction) && !inlineCallbackInvokesParameter(callbackFunction) && !callbackInvokesPropCallback(analysis, callbackFunction));
|
|
28041
|
+
};
|
|
28042
|
+
const isPropCallbackInvocationRef = (analysis, ref, options = {}) => {
|
|
27452
28043
|
if (!isPropAlias(analysis, ref)) return false;
|
|
27453
28044
|
let effectiveNode = ref.identifier;
|
|
27454
28045
|
let parent = effectiveNode.parent;
|
|
@@ -27461,7 +28052,9 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
|
27461
28052
|
if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
|
|
27462
28053
|
const memberParent = parent.parent;
|
|
27463
28054
|
if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
|
|
27464
|
-
|
|
28055
|
+
const propertyName = getStaticPropertyName(parent);
|
|
28056
|
+
if (propertyName && HANDLER_NAMED_METHOD_PATTERN.test(propertyName)) return true;
|
|
28057
|
+
if (options.nativeMethodScopes && isCustomHookParameter(ref) && propertyName && isProvenNativeReadMethod(ref, propertyName) && isNativeMethodSuppressionSafe(analysis, memberParent, propertyName, options.nativeMethodScopes)) return false;
|
|
27465
28058
|
return isWholePropsObjectReference(analysis, ref);
|
|
27466
28059
|
}
|
|
27467
28060
|
}
|
|
@@ -29496,7 +30089,7 @@ const isNumericForLoopCounter = (attributeNode, indexName) => {
|
|
|
29496
30089
|
if (forStatement.test && forLoopTestReadsDataLength(forStatement.test)) return false;
|
|
29497
30090
|
return true;
|
|
29498
30091
|
};
|
|
29499
|
-
const EMPTY_NAME_SET = /* @__PURE__ */ new Set();
|
|
30092
|
+
const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
|
|
29500
30093
|
const findIteratorItemNamesOfBinding = (binding) => {
|
|
29501
30094
|
const names = /* @__PURE__ */ new Set();
|
|
29502
30095
|
if (!binding.bindingFunction || !isFunctionLike$1(binding.bindingFunction)) return names;
|
|
@@ -29610,9 +30203,9 @@ const noArrayIndexAsKey = defineRule({
|
|
|
29610
30203
|
const jsxElement = openingElement.parent;
|
|
29611
30204
|
if (jsxElement && isNodeOfType(jsxElement, "JSXElement")) {
|
|
29612
30205
|
const isInlineTextRun = INLINE_TEXT_LEAF_TAGS.has(elementName.name);
|
|
29613
|
-
const primitiveItemNames = keyTemplate ? findBareItemNamesReferencedByTemplate(keyTemplate, itemNames) : EMPTY_NAME_SET;
|
|
30206
|
+
const primitiveItemNames = keyTemplate ? findBareItemNamesReferencedByTemplate(keyTemplate, itemNames) : EMPTY_NAME_SET$1;
|
|
29614
30207
|
if (!containsStatefulDescendant(jsxElement, {
|
|
29615
|
-
memberRootNames: isInlineTextRun ? itemNames : EMPTY_NAME_SET,
|
|
30208
|
+
memberRootNames: isInlineTextRun ? itemNames : EMPTY_NAME_SET$1,
|
|
29616
30209
|
bareIdentifierNames: primitiveItemNames.size > 0 ? new Set([...derivedNames, ...primitiveItemNames]) : derivedNames
|
|
29617
30210
|
})) return;
|
|
29618
30211
|
}
|
|
@@ -30113,6 +30706,9 @@ const findNearestPackageDirectory = (filename) => {
|
|
|
30113
30706
|
const readNearestPackageManifest = (filename) => {
|
|
30114
30707
|
const packageDirectory = findNearestPackageDirectory(filename);
|
|
30115
30708
|
if (!packageDirectory) return null;
|
|
30709
|
+
return readPackageManifest(packageDirectory);
|
|
30710
|
+
};
|
|
30711
|
+
const readPackageManifest = (packageDirectory) => {
|
|
30116
30712
|
const packageJsonPath = path.join(packageDirectory, "package.json");
|
|
30117
30713
|
recordContentProbe(packageJsonPath);
|
|
30118
30714
|
const cached = cachedManifestByPackageDirectory.get(packageDirectory);
|
|
@@ -30491,7 +31087,16 @@ const isRenderPreservingCallArgumentFunction = (node, scopes) => {
|
|
|
30491
31087
|
return parent.arguments.some((argumentNode) => argumentNode === node) && isProvenArrayMapCall(parent, scopes);
|
|
30492
31088
|
};
|
|
30493
31089
|
const isNestedRenderEvidenceBoundary = (node, scopes) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isRenderPreservingCallArgumentFunction(node, scopes);
|
|
30494
|
-
const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
|
|
31090
|
+
const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS) || isReactDomCreatePortalCall(node, scopes);
|
|
31091
|
+
const isReactDomCreatePortalCall = (node, scopes) => {
|
|
31092
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
31093
|
+
const callee = stripParenExpression(node.callee);
|
|
31094
|
+
if (isNodeOfType(callee, "Identifier")) return scopes.symbolFor(callee)?.kind === "import" && getImportedNameFromModule(callee, callee.name, "react-dom") === "createPortal";
|
|
31095
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "createPortal") return false;
|
|
31096
|
+
const symbol = scopes.symbolFor(callee.object);
|
|
31097
|
+
if (!symbol || symbol.kind !== "import") return false;
|
|
31098
|
+
return isDefaultImportFromModule(callee.object, callee.object.name, "react-dom") || isNamespaceImportFromModule(callee.object, callee.object.name, "react-dom");
|
|
31099
|
+
};
|
|
30495
31100
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
30496
31101
|
let hasRenderOutput = false;
|
|
30497
31102
|
walkAst(rootNode, (node) => {
|
|
@@ -30643,6 +31248,514 @@ const noCascadingSetState = defineRetiredRule({
|
|
|
30643
31248
|
recommendation: "Retired: React batches synchronous state updates from one effect into the same follow-up commit, so setter count does not prove repeated redraws."
|
|
30644
31249
|
});
|
|
30645
31250
|
//#endregion
|
|
31251
|
+
//#region src/plugin/rules/state-and-effects/utils/create-state-trigger-reachability.ts
|
|
31252
|
+
const getRefCurrentSymbol = (node, context) => {
|
|
31253
|
+
const expression = stripParenExpression(node);
|
|
31254
|
+
if (!isNodeOfType(expression, "MemberExpression") || getStaticMemberPropertyName(expression) !== "current" || !isNodeOfType(expression.object, "Identifier")) return null;
|
|
31255
|
+
return context.scopes.symbolFor(expression.object);
|
|
31256
|
+
};
|
|
31257
|
+
const expressionReadsState = (analysis, expression) => getDownstreamRefs(analysis, expression).some((reference) => getUpstreamRefs(analysis, reference).some((upstreamReference) => isState(analysis, upstreamReference)));
|
|
31258
|
+
const expressionReadsProp = (analysis, expression) => getDownstreamRefs(analysis, expression).some((reference) => getUpstreamRefs(analysis, reference).some((upstreamReference) => isProp(analysis, upstreamReference)));
|
|
31259
|
+
const hasNonInitializerWrite = (analysis, identifier) => {
|
|
31260
|
+
const reference = getRef(analysis, identifier);
|
|
31261
|
+
return Boolean(reference?.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
31262
|
+
};
|
|
31263
|
+
const isStableParameterDefault = (expression) => {
|
|
31264
|
+
const candidate = stripParenExpression(expression);
|
|
31265
|
+
if (isNodeOfType(candidate, "Literal")) return candidate.value === null || [
|
|
31266
|
+
"bigint",
|
|
31267
|
+
"boolean",
|
|
31268
|
+
"number",
|
|
31269
|
+
"string"
|
|
31270
|
+
].includes(typeof candidate.value);
|
|
31271
|
+
return Boolean(isNodeOfType(candidate, "TemplateLiteral") && (candidate.expressions ?? []).length === 0);
|
|
31272
|
+
};
|
|
31273
|
+
const isDirectComponentPropBinding = (symbol) => {
|
|
31274
|
+
const parameter = symbol.declarationNode;
|
|
31275
|
+
if (parameter === symbol.bindingIdentifier) return true;
|
|
31276
|
+
if (isNodeOfType(parameter, "AssignmentPattern")) return parameter.left === symbol.bindingIdentifier && isStableParameterDefault(parameter.right);
|
|
31277
|
+
if (!isNodeOfType(parameter, "ObjectPattern")) return false;
|
|
31278
|
+
return parameter.properties.some((property) => {
|
|
31279
|
+
if (!isNodeOfType(property, "Property")) return false;
|
|
31280
|
+
const propertyValue = property.value;
|
|
31281
|
+
if (propertyValue === symbol.bindingIdentifier) return true;
|
|
31282
|
+
return Boolean(isNodeOfType(propertyValue, "AssignmentPattern") && propertyValue.left === symbol.bindingIdentifier && isStableParameterDefault(propertyValue.right));
|
|
31283
|
+
});
|
|
31284
|
+
};
|
|
31285
|
+
const REFLEXIVE_TYPE_NODE_TYPES = new Set([
|
|
31286
|
+
"TSBigIntKeyword",
|
|
31287
|
+
"TSBooleanKeyword",
|
|
31288
|
+
"TSNeverKeyword",
|
|
31289
|
+
"TSNullKeyword",
|
|
31290
|
+
"TSObjectKeyword",
|
|
31291
|
+
"TSStringKeyword",
|
|
31292
|
+
"TSSymbolKeyword",
|
|
31293
|
+
"TSUndefinedKeyword"
|
|
31294
|
+
]);
|
|
31295
|
+
const PRIMITIVE_TYPE_NODE_TYPES = new Set([
|
|
31296
|
+
"TSBigIntKeyword",
|
|
31297
|
+
"TSBooleanKeyword",
|
|
31298
|
+
"TSNeverKeyword",
|
|
31299
|
+
"TSNullKeyword",
|
|
31300
|
+
"TSNumberKeyword",
|
|
31301
|
+
"TSStringKeyword",
|
|
31302
|
+
"TSSymbolKeyword",
|
|
31303
|
+
"TSUndefinedKeyword"
|
|
31304
|
+
]);
|
|
31305
|
+
const isDefinitelyReflexiveType = (typeNode) => {
|
|
31306
|
+
if (REFLEXIVE_TYPE_NODE_TYPES.has(typeNode.type)) return true;
|
|
31307
|
+
if (isNodeOfType(typeNode, "TSLiteralType")) return Boolean(isNodeOfType(typeNode.literal, "Literal") && (typeof typeNode.literal.value !== "number" || !Number.isNaN(typeNode.literal.value)));
|
|
31308
|
+
if (isNodeOfType(typeNode, "TSUnionType")) return (typeNode.types ?? []).every((member) => isDefinitelyReflexiveType(member));
|
|
31309
|
+
return false;
|
|
31310
|
+
};
|
|
31311
|
+
const isDefinitelyPrimitiveType = (typeNode) => {
|
|
31312
|
+
if (PRIMITIVE_TYPE_NODE_TYPES.has(typeNode.type)) return true;
|
|
31313
|
+
if (isNodeOfType(typeNode, "TSLiteralType")) return true;
|
|
31314
|
+
if (isNodeOfType(typeNode, "TSUnionType")) return (typeNode.types ?? []).every((member) => isDefinitelyPrimitiveType(member));
|
|
31315
|
+
return false;
|
|
31316
|
+
};
|
|
31317
|
+
const isDefinitelyPrimitiveExpression = (expression, context) => {
|
|
31318
|
+
const candidate = stripParenExpression(expression);
|
|
31319
|
+
if (isNodeOfType(candidate, "Literal")) return true;
|
|
31320
|
+
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
31321
|
+
const symbol = context.scopes.symbolFor(candidate);
|
|
31322
|
+
const typeAnnotation = symbol ? getSymbolTypeAnnotation(symbol) : null;
|
|
31323
|
+
return Boolean(typeAnnotation && isDefinitelyPrimitiveType(typeAnnotation));
|
|
31324
|
+
};
|
|
31325
|
+
const isDefinitelyReflexiveExpression = (expression, analysis, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
31326
|
+
const candidate = stripParenExpression(expression);
|
|
31327
|
+
if (isNodeOfType(candidate, "Literal")) return typeof candidate.value !== "number" || !Number.isNaN(candidate.value);
|
|
31328
|
+
if (isNodeOfType(candidate, "TemplateLiteral") || isNodeOfType(candidate, "ArrayExpression") || isNodeOfType(candidate, "ObjectExpression") || isFunctionLike$1(candidate)) return true;
|
|
31329
|
+
if (isNodeOfType(candidate, "UnaryExpression")) return [
|
|
31330
|
+
"!",
|
|
31331
|
+
"typeof",
|
|
31332
|
+
"void",
|
|
31333
|
+
"~"
|
|
31334
|
+
].includes(candidate.operator);
|
|
31335
|
+
if (isNodeOfType(candidate, "BinaryExpression")) return [
|
|
31336
|
+
"&",
|
|
31337
|
+
"|",
|
|
31338
|
+
"^",
|
|
31339
|
+
"<<",
|
|
31340
|
+
">>",
|
|
31341
|
+
">>>"
|
|
31342
|
+
].includes(candidate.operator);
|
|
31343
|
+
if (isNodeOfType(candidate, "CallExpression")) {
|
|
31344
|
+
const callee = stripParenExpression(candidate.callee);
|
|
31345
|
+
return Boolean(isNodeOfType(callee, "Identifier") && ["Boolean", "String"].includes(callee.name) && context.scopes.isGlobalReference(callee));
|
|
31346
|
+
}
|
|
31347
|
+
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
31348
|
+
if (candidate.name === "undefined" && context.scopes.isGlobalReference(candidate)) return true;
|
|
31349
|
+
const symbol = context.scopes.symbolFor(candidate);
|
|
31350
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
31351
|
+
const typeAnnotation = getSymbolTypeAnnotation(symbol);
|
|
31352
|
+
if (typeAnnotation && isDefinitelyReflexiveType(typeAnnotation)) return true;
|
|
31353
|
+
if (!symbol.initializer || hasNonInitializerWrite(analysis, candidate)) return false;
|
|
31354
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
31355
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
31356
|
+
return isDefinitelyReflexiveExpression(symbol.initializer, analysis, context, nextVisitedSymbolIds);
|
|
31357
|
+
};
|
|
31358
|
+
const isSupportedPropProjection = (analysis, expression, context, visitedBindings = /* @__PURE__ */ new Set()) => {
|
|
31359
|
+
const candidate = stripParenExpression(expression);
|
|
31360
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
31361
|
+
const reference = getRef(analysis, candidate);
|
|
31362
|
+
if (!reference) return false;
|
|
31363
|
+
if (isProp(analysis, reference)) {
|
|
31364
|
+
if (isCustomHookParameter(reference)) return false;
|
|
31365
|
+
const symbol = context.scopes.symbolFor(candidate);
|
|
31366
|
+
return Boolean(symbol && isDirectComponentPropBinding(symbol));
|
|
31367
|
+
}
|
|
31368
|
+
if (!reference.resolved || visitedBindings.has(reference.resolved)) return false;
|
|
31369
|
+
if (hasNonInitializerWrite(analysis, candidate)) return false;
|
|
31370
|
+
const declarator = reference.resolved.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
31371
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) return false;
|
|
31372
|
+
const nextVisitedBindings = new Set(visitedBindings);
|
|
31373
|
+
nextVisitedBindings.add(reference.resolved);
|
|
31374
|
+
return isSupportedPropProjection(analysis, declarator.init, context, nextVisitedBindings);
|
|
31375
|
+
}
|
|
31376
|
+
if (isNodeOfType(candidate, "MemberExpression")) return false;
|
|
31377
|
+
if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator === "~") return Boolean(isDefinitelyPrimitiveExpression(candidate.argument, context) && isSupportedPropProjection(analysis, candidate.argument, context, visitedBindings));
|
|
31378
|
+
if (isNodeOfType(candidate, "BinaryExpression") && [
|
|
31379
|
+
"&",
|
|
31380
|
+
"|",
|
|
31381
|
+
"^",
|
|
31382
|
+
"<<",
|
|
31383
|
+
">>",
|
|
31384
|
+
">>>"
|
|
31385
|
+
].includes(candidate.operator)) return [candidate.left, candidate.right].every((operand) => {
|
|
31386
|
+
const projectionOperand = stripParenExpression(operand);
|
|
31387
|
+
return isNodeOfType(projectionOperand, "Literal") || isDefinitelyPrimitiveExpression(projectionOperand, context) && isSupportedPropProjection(analysis, projectionOperand, context, visitedBindings);
|
|
31388
|
+
});
|
|
31389
|
+
if (isNodeOfType(candidate, "CallExpression")) {
|
|
31390
|
+
const callee = stripParenExpression(candidate.callee);
|
|
31391
|
+
if (isNodeOfType(callee, "Identifier") && ["Boolean", "String"].includes(callee.name) && context.scopes.isGlobalReference(callee) && (candidate.arguments ?? []).length === 1) {
|
|
31392
|
+
const argument = candidate.arguments?.[0];
|
|
31393
|
+
return Boolean(argument && isDefinitelyPrimitiveExpression(argument, context) && isSupportedPropProjection(analysis, argument, context, visitedBindings));
|
|
31394
|
+
}
|
|
31395
|
+
return false;
|
|
31396
|
+
}
|
|
31397
|
+
return false;
|
|
31398
|
+
};
|
|
31399
|
+
const getStableCurrentValueKey = (analysis, expression, context) => {
|
|
31400
|
+
if (expressionReadsState(analysis, expression)) return null;
|
|
31401
|
+
if (!expressionReadsProp(analysis, expression)) return null;
|
|
31402
|
+
if (!isSupportedPropProjection(analysis, expression, context)) return null;
|
|
31403
|
+
return resolveExpressionKey$1(expression, context);
|
|
31404
|
+
};
|
|
31405
|
+
const getSnapshotShape = (analysis, expression, context) => {
|
|
31406
|
+
const candidate = stripParenExpression(expression);
|
|
31407
|
+
if (isNodeOfType(candidate, "ArrayExpression")) {
|
|
31408
|
+
const elementKeys = [];
|
|
31409
|
+
for (const element of candidate.elements ?? []) {
|
|
31410
|
+
if (!element || isNodeOfType(element, "SpreadElement")) return null;
|
|
31411
|
+
const elementKey = getStableCurrentValueKey(analysis, element, context);
|
|
31412
|
+
if (!elementKey) return null;
|
|
31413
|
+
elementKeys.push(elementKey);
|
|
31414
|
+
}
|
|
31415
|
+
return {
|
|
31416
|
+
scalarKey: null,
|
|
31417
|
+
elementKeys
|
|
31418
|
+
};
|
|
31419
|
+
}
|
|
31420
|
+
const scalarKey = getStableCurrentValueKey(analysis, candidate, context);
|
|
31421
|
+
return scalarKey ? {
|
|
31422
|
+
scalarKey,
|
|
31423
|
+
elementKeys: null
|
|
31424
|
+
} : null;
|
|
31425
|
+
};
|
|
31426
|
+
const snapshotShapesMatch = (left, right) => {
|
|
31427
|
+
if (left.scalarKey !== right.scalarKey) return false;
|
|
31428
|
+
if (left.elementKeys === null || right.elementKeys === null) return left.elementKeys === right.elementKeys;
|
|
31429
|
+
return left.elementKeys.length === right.elementKeys.length && left.elementKeys.every((elementKey, index) => elementKey === right.elementKeys?.[index]);
|
|
31430
|
+
};
|
|
31431
|
+
const collectSnapshotEnvironment = (analysis, effectFunction, context) => {
|
|
31432
|
+
const assignmentsByRefSymbolId = /* @__PURE__ */ new Map();
|
|
31433
|
+
const refSymbolsById = /* @__PURE__ */ new Map();
|
|
31434
|
+
walkAst(effectFunction, (child) => {
|
|
31435
|
+
if (child !== effectFunction && isFunctionLike$1(child)) return false;
|
|
31436
|
+
if (!isNodeOfType(child, "AssignmentExpression") || child.operator !== "=") return;
|
|
31437
|
+
const refSymbol = getRefCurrentSymbol(child.left, context);
|
|
31438
|
+
if (!refSymbol) return;
|
|
31439
|
+
const assignments = assignmentsByRefSymbolId.get(refSymbol.id) ?? [];
|
|
31440
|
+
assignments.push(child);
|
|
31441
|
+
assignmentsByRefSymbolId.set(refSymbol.id, assignments);
|
|
31442
|
+
refSymbolsById.set(refSymbol.id, refSymbol);
|
|
31443
|
+
});
|
|
31444
|
+
const refShapes = /* @__PURE__ */ new Map();
|
|
31445
|
+
for (const [refSymbolId, assignments] of assignmentsByRefSymbolId) {
|
|
31446
|
+
if (assignments.length !== 1) continue;
|
|
31447
|
+
const assignment = assignments[0];
|
|
31448
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression")) continue;
|
|
31449
|
+
if (context.cfg.enclosingFunction(assignment) !== effectFunction) continue;
|
|
31450
|
+
if (!context.cfg.isUnconditionalFromEntry(assignment)) continue;
|
|
31451
|
+
const declarator = refSymbolsById.get(refSymbolId)?.declarationNode ?? null;
|
|
31452
|
+
if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useRef") || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
31453
|
+
const refIdentifier = isNodeOfType(declarator.id, "Identifier") ? declarator.id : null;
|
|
31454
|
+
const declarationSymbol = refIdentifier ? context.scopes.symbolFor(refIdentifier) : null;
|
|
31455
|
+
if (!declarationSymbol || declarationSymbol.id !== refSymbolId) continue;
|
|
31456
|
+
if (declarationSymbol.references.some((reference) => {
|
|
31457
|
+
const member = reference.identifier.parent;
|
|
31458
|
+
if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== reference.identifier || getStaticMemberPropertyName(member) !== "current") return true;
|
|
31459
|
+
const parent = member.parent;
|
|
31460
|
+
if (!parent) return false;
|
|
31461
|
+
if (isNodeOfType(parent, "UpdateExpression")) return true;
|
|
31462
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.left === member) return parent !== assignment;
|
|
31463
|
+
return false;
|
|
31464
|
+
})) continue;
|
|
31465
|
+
const initializer = declarator.init.arguments?.[0];
|
|
31466
|
+
if (!initializer) continue;
|
|
31467
|
+
const initialShape = getSnapshotShape(analysis, initializer, context);
|
|
31468
|
+
const assignedShape = getSnapshotShape(analysis, assignment.right, context);
|
|
31469
|
+
if (!initialShape || !assignedShape || !snapshotShapesMatch(initialShape, assignedShape)) continue;
|
|
31470
|
+
refShapes.set(refSymbolId, assignedShape);
|
|
31471
|
+
}
|
|
31472
|
+
const previousValueKeys = /* @__PURE__ */ new Map();
|
|
31473
|
+
walkAst(effectFunction, (child) => {
|
|
31474
|
+
if (child !== effectFunction && isFunctionLike$1(child)) return false;
|
|
31475
|
+
if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
|
|
31476
|
+
const refSymbol = getRefCurrentSymbol(child.init, context);
|
|
31477
|
+
if (!refSymbol) return;
|
|
31478
|
+
const snapshotShape = refShapes.get(refSymbol.id);
|
|
31479
|
+
if (!snapshotShape) return;
|
|
31480
|
+
if (isNodeOfType(child.id, "Identifier") && snapshotShape.scalarKey) {
|
|
31481
|
+
const previousSymbol = context.scopes.symbolFor(child.id);
|
|
31482
|
+
if (previousSymbol && previousSymbol.kind === "const") previousValueKeys.set(previousSymbol.id, snapshotShape.scalarKey);
|
|
31483
|
+
return;
|
|
31484
|
+
}
|
|
31485
|
+
if (!isNodeOfType(child.id, "ArrayPattern") || !snapshotShape.elementKeys) return;
|
|
31486
|
+
for (let elementIndex = 0; elementIndex < child.id.elements.length; elementIndex += 1) {
|
|
31487
|
+
const element = child.id.elements[elementIndex];
|
|
31488
|
+
const elementKey = snapshotShape.elementKeys[elementIndex];
|
|
31489
|
+
if (!element || !elementKey || !isNodeOfType(element, "Identifier")) continue;
|
|
31490
|
+
const previousSymbol = context.scopes.symbolFor(element);
|
|
31491
|
+
if (previousSymbol && previousSymbol.kind === "const") previousValueKeys.set(previousSymbol.id, elementKey);
|
|
31492
|
+
}
|
|
31493
|
+
});
|
|
31494
|
+
return {
|
|
31495
|
+
refShapes,
|
|
31496
|
+
previousValueKeys
|
|
31497
|
+
};
|
|
31498
|
+
};
|
|
31499
|
+
const getSnapshotValueKey = (expression, environment) => {
|
|
31500
|
+
const candidate = stripParenExpression(expression);
|
|
31501
|
+
const refSymbol = getRefCurrentSymbol(candidate, environment.context);
|
|
31502
|
+
if (refSymbol) return environment.snapshotEnvironment.refShapes.get(refSymbol.id)?.scalarKey ?? null;
|
|
31503
|
+
if (!isNodeOfType(candidate, "Identifier")) return null;
|
|
31504
|
+
const symbol = environment.context.scopes.symbolFor(candidate);
|
|
31505
|
+
if (!symbol) return null;
|
|
31506
|
+
const substitutedExpression = environment.substitutions.get(symbol.id);
|
|
31507
|
+
if (substitutedExpression) return getSnapshotValueKey(substitutedExpression, environment);
|
|
31508
|
+
const directKey = environment.snapshotEnvironment.previousValueKeys.get(symbol.id);
|
|
31509
|
+
if (directKey) return directKey;
|
|
31510
|
+
if (symbol.kind !== "const" || !symbol.initializer || environment.visitedSymbolIds.has(symbol.id)) return null;
|
|
31511
|
+
const nextVisitedSymbolIds = new Set(environment.visitedSymbolIds);
|
|
31512
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
31513
|
+
return getSnapshotValueKey(symbol.initializer, {
|
|
31514
|
+
...environment,
|
|
31515
|
+
visitedSymbolIds: nextVisitedSymbolIds
|
|
31516
|
+
});
|
|
31517
|
+
};
|
|
31518
|
+
const evaluateComparison = (expression, environment) => {
|
|
31519
|
+
if (!isNodeOfType(expression, "BinaryExpression")) return null;
|
|
31520
|
+
if (![
|
|
31521
|
+
"===",
|
|
31522
|
+
"!==",
|
|
31523
|
+
"==",
|
|
31524
|
+
"!="
|
|
31525
|
+
].includes(expression.operator)) return null;
|
|
31526
|
+
const resolveSubstitutedExpression = (operand) => {
|
|
31527
|
+
const candidate = stripParenExpression(operand);
|
|
31528
|
+
if (!isNodeOfType(candidate, "Identifier")) return candidate;
|
|
31529
|
+
const symbol = environment.context.scopes.symbolFor(candidate);
|
|
31530
|
+
return (symbol && environment.substitutions.get(symbol.id)) ?? candidate;
|
|
31531
|
+
};
|
|
31532
|
+
const leftExpression = resolveSubstitutedExpression(expression.left);
|
|
31533
|
+
const rightExpression = resolveSubstitutedExpression(expression.right);
|
|
31534
|
+
const leftSnapshotKey = getSnapshotValueKey(leftExpression, environment);
|
|
31535
|
+
const rightSnapshotKey = getSnapshotValueKey(rightExpression, environment);
|
|
31536
|
+
const leftCurrentKey = getStableCurrentValueKey(environment.analysis, leftExpression, environment.context);
|
|
31537
|
+
const rightCurrentKey = getStableCurrentValueKey(environment.analysis, rightExpression, environment.context);
|
|
31538
|
+
const currentExpression = leftSnapshotKey !== null && leftSnapshotKey === rightCurrentKey ? rightExpression : rightSnapshotKey !== null && rightSnapshotKey === leftCurrentKey ? leftExpression : null;
|
|
31539
|
+
if (!currentExpression || !isDefinitelyReflexiveExpression(currentExpression, environment.analysis, environment.context)) return null;
|
|
31540
|
+
return expression.operator === "===" || expression.operator === "==";
|
|
31541
|
+
};
|
|
31542
|
+
const hasDirectGlobalObjectIsWrite = (objectIdentifier, context) => {
|
|
31543
|
+
const program = findProgramRoot(objectIdentifier);
|
|
31544
|
+
if (!program) return true;
|
|
31545
|
+
let didFindWrite = false;
|
|
31546
|
+
walkAst(program, (candidate) => {
|
|
31547
|
+
if (didFindWrite) return false;
|
|
31548
|
+
let writeTarget = null;
|
|
31549
|
+
if (isNodeOfType(candidate, "AssignmentExpression")) writeTarget = candidate.left;
|
|
31550
|
+
else if (isNodeOfType(candidate, "UpdateExpression")) writeTarget = candidate.argument;
|
|
31551
|
+
else if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator === "delete") writeTarget = candidate.argument;
|
|
31552
|
+
if (!writeTarget) return;
|
|
31553
|
+
const target = stripParenExpression(writeTarget);
|
|
31554
|
+
if (isNodeOfType(target, "Identifier")) {
|
|
31555
|
+
didFindWrite = target.name === "Object" && context.scopes.isGlobalReference(target);
|
|
31556
|
+
return;
|
|
31557
|
+
}
|
|
31558
|
+
if (isNodeOfType(target, "MemberExpression") && isNodeOfType(target.object, "Identifier") && target.object.name === "Object" && context.scopes.isGlobalReference(target.object)) {
|
|
31559
|
+
const propertyName = getStaticMemberPropertyName(target);
|
|
31560
|
+
didFindWrite = propertyName === null || propertyName === "is";
|
|
31561
|
+
}
|
|
31562
|
+
});
|
|
31563
|
+
return didFindWrite;
|
|
31564
|
+
};
|
|
31565
|
+
const evaluateObjectIs = (expression, environment) => {
|
|
31566
|
+
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
31567
|
+
const callee = stripParenExpression(expression.callee);
|
|
31568
|
+
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Object" || !environment.context.scopes.isGlobalReference(callee.object) || getStaticMemberPropertyName(callee) !== "is") return null;
|
|
31569
|
+
if (hasPossibleStaticPropertyWrite(callee.object, "is", environment.context.scopes) || hasDirectGlobalObjectIsWrite(callee.object, environment.context)) return null;
|
|
31570
|
+
const argumentsToCompare = expression.arguments ?? [];
|
|
31571
|
+
if (argumentsToCompare.length !== 2) return null;
|
|
31572
|
+
const resolveSubstitutedExpression = (operand) => {
|
|
31573
|
+
const candidate = stripParenExpression(operand);
|
|
31574
|
+
if (!isNodeOfType(candidate, "Identifier")) return candidate;
|
|
31575
|
+
const symbol = environment.context.scopes.symbolFor(candidate);
|
|
31576
|
+
return (symbol && environment.substitutions.get(symbol.id)) ?? candidate;
|
|
31577
|
+
};
|
|
31578
|
+
const leftExpression = resolveSubstitutedExpression(argumentsToCompare[0]);
|
|
31579
|
+
const rightExpression = resolveSubstitutedExpression(argumentsToCompare[1]);
|
|
31580
|
+
const leftSnapshotKey = getSnapshotValueKey(leftExpression, environment);
|
|
31581
|
+
const rightSnapshotKey = getSnapshotValueKey(rightExpression, environment);
|
|
31582
|
+
const leftCurrentKey = getStableCurrentValueKey(environment.analysis, leftExpression, environment.context);
|
|
31583
|
+
const rightCurrentKey = getStableCurrentValueKey(environment.analysis, rightExpression, environment.context);
|
|
31584
|
+
return leftSnapshotKey !== null && leftSnapshotKey === rightCurrentKey ? true : rightSnapshotKey !== null && rightSnapshotKey === leftCurrentKey ? true : null;
|
|
31585
|
+
};
|
|
31586
|
+
const getHelperReturnExpression = (functionNode) => {
|
|
31587
|
+
if (!isFunctionLike$1(functionNode)) return null;
|
|
31588
|
+
if (Boolean(functionNode.async) || Boolean(functionNode.generator)) return null;
|
|
31589
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) return functionNode.body;
|
|
31590
|
+
const statements = functionNode.body.body ?? [];
|
|
31591
|
+
if (statements.length !== 1 || !isNodeOfType(statements[0], "ReturnStatement")) return null;
|
|
31592
|
+
return statements[0].argument ? statements[0].argument : null;
|
|
31593
|
+
};
|
|
31594
|
+
const evaluateBoolean = (expression, environment) => {
|
|
31595
|
+
const candidate = stripParenExpression(expression);
|
|
31596
|
+
if (isNodeOfType(candidate, "Literal") && typeof candidate.value === "boolean") return candidate.value;
|
|
31597
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
31598
|
+
const symbol = environment.context.scopes.symbolFor(candidate);
|
|
31599
|
+
if (!symbol) return null;
|
|
31600
|
+
const substitutedExpression = environment.substitutions.get(symbol.id);
|
|
31601
|
+
if (substitutedExpression) return evaluateBoolean(substitutedExpression, environment);
|
|
31602
|
+
if (symbol.kind !== "const" || !symbol.initializer || environment.visitedSymbolIds.has(symbol.id)) return null;
|
|
31603
|
+
const nextVisitedSymbolIds = new Set(environment.visitedSymbolIds);
|
|
31604
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
31605
|
+
return evaluateBoolean(symbol.initializer, {
|
|
31606
|
+
...environment,
|
|
31607
|
+
visitedSymbolIds: nextVisitedSymbolIds
|
|
31608
|
+
});
|
|
31609
|
+
}
|
|
31610
|
+
if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator === "!") {
|
|
31611
|
+
const argumentValue = evaluateBoolean(candidate.argument, environment);
|
|
31612
|
+
return argumentValue === null ? null : !argumentValue;
|
|
31613
|
+
}
|
|
31614
|
+
if (isNodeOfType(candidate, "LogicalExpression")) {
|
|
31615
|
+
const leftValue = evaluateBoolean(candidate.left, environment);
|
|
31616
|
+
if (candidate.operator === "&&") {
|
|
31617
|
+
if (leftValue === false) return false;
|
|
31618
|
+
const rightValue = evaluateBoolean(candidate.right, environment);
|
|
31619
|
+
if (leftValue === true) return rightValue;
|
|
31620
|
+
return rightValue === false ? false : null;
|
|
31621
|
+
}
|
|
31622
|
+
if (candidate.operator === "||") {
|
|
31623
|
+
if (leftValue === true) return true;
|
|
31624
|
+
const rightValue = evaluateBoolean(candidate.right, environment);
|
|
31625
|
+
if (leftValue === false) return rightValue;
|
|
31626
|
+
return rightValue === true ? true : null;
|
|
31627
|
+
}
|
|
31628
|
+
return null;
|
|
31629
|
+
}
|
|
31630
|
+
const objectIsValue = evaluateObjectIs(candidate, environment);
|
|
31631
|
+
if (objectIsValue !== null) return objectIsValue;
|
|
31632
|
+
const comparisonValue = evaluateComparison(candidate, environment);
|
|
31633
|
+
if (comparisonValue !== null) return comparisonValue;
|
|
31634
|
+
if (!environment.allowHelperCall || !isNodeOfType(candidate, "CallExpression")) return null;
|
|
31635
|
+
const callee = stripParenExpression(candidate.callee);
|
|
31636
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
31637
|
+
const calleeReference = getRef(environment.analysis, callee);
|
|
31638
|
+
if (calleeReference?.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
|
|
31639
|
+
const helperFunction = calleeReference ? resolveToFunction(calleeReference) : null;
|
|
31640
|
+
const returnExpression = helperFunction ? getHelperReturnExpression(helperFunction) : null;
|
|
31641
|
+
if (!helperFunction || !returnExpression || !isFunctionLike$1(helperFunction)) return null;
|
|
31642
|
+
const parameters = helperFunction.params ?? [];
|
|
31643
|
+
const argumentsForHelper = candidate.arguments ?? [];
|
|
31644
|
+
if (parameters.length !== argumentsForHelper.length) return null;
|
|
31645
|
+
const substitutions = new Map(environment.substitutions);
|
|
31646
|
+
for (let parameterIndex = 0; parameterIndex < parameters.length; parameterIndex += 1) {
|
|
31647
|
+
const parameter = parameters[parameterIndex];
|
|
31648
|
+
const argument = argumentsForHelper[parameterIndex];
|
|
31649
|
+
if (!parameter || !argument || !isNodeOfType(parameter, "Identifier")) return null;
|
|
31650
|
+
const parameterSymbol = environment.context.scopes.symbolFor(parameter);
|
|
31651
|
+
if (!parameterSymbol) return null;
|
|
31652
|
+
substitutions.set(parameterSymbol.id, argument);
|
|
31653
|
+
}
|
|
31654
|
+
return evaluateBoolean(returnExpression, {
|
|
31655
|
+
...environment,
|
|
31656
|
+
substitutions,
|
|
31657
|
+
visitedSymbolIds: /* @__PURE__ */ new Set(),
|
|
31658
|
+
allowHelperCall: false
|
|
31659
|
+
});
|
|
31660
|
+
};
|
|
31661
|
+
const statementCanCompleteNormally = (statement, environment) => {
|
|
31662
|
+
if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ContinueStatement")) return false;
|
|
31663
|
+
if (isNodeOfType(statement, "BlockStatement")) return (statement.body ?? []).every((childStatement) => statementCanCompleteNormally(childStatement, environment));
|
|
31664
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
31665
|
+
const testValue = evaluateBoolean(statement.test, environment);
|
|
31666
|
+
if (testValue === true) return statementCanCompleteNormally(statement.consequent, environment);
|
|
31667
|
+
if (testValue === false) return statement.alternate ? statementCanCompleteNormally(statement.alternate, environment) : true;
|
|
31668
|
+
const consequentCanComplete = statementCanCompleteNormally(statement.consequent, environment);
|
|
31669
|
+
const alternateCanComplete = statement.alternate ? statementCanCompleteNormally(statement.alternate, environment) : true;
|
|
31670
|
+
return consequentCanComplete || alternateCanComplete;
|
|
31671
|
+
}
|
|
31672
|
+
if (isNodeOfType(statement, "LabeledStatement")) {
|
|
31673
|
+
const labelName = isNodeOfType(statement.label, "Identifier") ? statement.label.name : null;
|
|
31674
|
+
if (labelName && statementCanCompleteWithBreakToLabel(statement.body, labelName, environment)) return true;
|
|
31675
|
+
return statementCanCompleteNormallyForLabel(statement.body, environment);
|
|
31676
|
+
}
|
|
31677
|
+
return true;
|
|
31678
|
+
};
|
|
31679
|
+
const statementCanCompleteNormallyForLabel = (statement, environment) => {
|
|
31680
|
+
if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ContinueStatement")) return false;
|
|
31681
|
+
if (isNodeOfType(statement, "BlockStatement")) return (statement.body ?? []).every((childStatement) => statementCanCompleteNormallyForLabel(childStatement, environment));
|
|
31682
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
31683
|
+
const testValue = evaluateBoolean(statement.test, environment);
|
|
31684
|
+
if (testValue === true) return statementCanCompleteNormallyForLabel(statement.consequent, environment);
|
|
31685
|
+
if (testValue === false) return statement.alternate ? statementCanCompleteNormallyForLabel(statement.alternate, environment) : true;
|
|
31686
|
+
const consequentCanComplete = statementCanCompleteNormallyForLabel(statement.consequent, environment);
|
|
31687
|
+
const alternateCanComplete = statement.alternate ? statementCanCompleteNormallyForLabel(statement.alternate, environment) : true;
|
|
31688
|
+
return consequentCanComplete || alternateCanComplete;
|
|
31689
|
+
}
|
|
31690
|
+
if (isNodeOfType(statement, "LabeledStatement")) return statementCanCompleteNormally(statement, environment);
|
|
31691
|
+
return isNodeOfType(statement, "ExpressionStatement") || isNodeOfType(statement, "VariableDeclaration") || isNodeOfType(statement, "EmptyStatement") || isNodeOfType(statement, "DebuggerStatement");
|
|
31692
|
+
};
|
|
31693
|
+
const statementCanCompleteWithBreakToLabel = (statement, labelName, environment) => {
|
|
31694
|
+
if (isNodeOfType(statement, "BreakStatement")) return Boolean(isNodeOfType(statement.label, "Identifier") && statement.label.name === labelName);
|
|
31695
|
+
if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement")) return false;
|
|
31696
|
+
if (isNodeOfType(statement, "BlockStatement")) {
|
|
31697
|
+
for (const childStatement of statement.body ?? []) {
|
|
31698
|
+
if (statementCanCompleteWithBreakToLabel(childStatement, labelName, environment)) return true;
|
|
31699
|
+
if (!statementCanCompleteNormallyForLabel(childStatement, environment)) return false;
|
|
31700
|
+
}
|
|
31701
|
+
return false;
|
|
31702
|
+
}
|
|
31703
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
31704
|
+
const testValue = evaluateBoolean(statement.test, environment);
|
|
31705
|
+
if (testValue === true) return statementCanCompleteWithBreakToLabel(statement.consequent, labelName, environment);
|
|
31706
|
+
if (testValue === false) return Boolean(statement.alternate && statementCanCompleteWithBreakToLabel(statement.alternate, labelName, environment));
|
|
31707
|
+
return statementCanCompleteWithBreakToLabel(statement.consequent, labelName, environment) || Boolean(statement.alternate && statementCanCompleteWithBreakToLabel(statement.alternate, labelName, environment));
|
|
31708
|
+
}
|
|
31709
|
+
if (isNodeOfType(statement, "LabeledStatement")) return statementCanCompleteWithBreakToLabel(statement.body, labelName, environment);
|
|
31710
|
+
return false;
|
|
31711
|
+
};
|
|
31712
|
+
const isReachableUnderSnapshotEnvironment = (target, effectFunction, environment) => {
|
|
31713
|
+
if (environment.context.cfg.enclosingFunction(target) !== effectFunction) return true;
|
|
31714
|
+
let current = target;
|
|
31715
|
+
while (current !== effectFunction) {
|
|
31716
|
+
const parent = current.parent;
|
|
31717
|
+
if (!parent) return true;
|
|
31718
|
+
if (isFunctionLike$1(parent) && parent !== effectFunction) return true;
|
|
31719
|
+
if (isNodeOfType(parent, "IfStatement")) {
|
|
31720
|
+
const testValue = evaluateBoolean(parent.test, environment);
|
|
31721
|
+
if (isAstDescendant(current, parent.consequent) && testValue === false) return false;
|
|
31722
|
+
if (parent.alternate && isAstDescendant(current, parent.alternate) && testValue === true) return false;
|
|
31723
|
+
}
|
|
31724
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === current) {
|
|
31725
|
+
const leftValue = evaluateBoolean(parent.left, environment);
|
|
31726
|
+
if (parent.operator === "&&" && leftValue === false) return false;
|
|
31727
|
+
if (parent.operator === "||" && leftValue === true) return false;
|
|
31728
|
+
}
|
|
31729
|
+
if (isNodeOfType(parent, "ConditionalExpression")) {
|
|
31730
|
+
const testValue = evaluateBoolean(parent.test, environment);
|
|
31731
|
+
if (parent.consequent === current && testValue === false) return false;
|
|
31732
|
+
if (parent.alternate === current && testValue === true) return false;
|
|
31733
|
+
}
|
|
31734
|
+
if (isNodeOfType(parent, "BlockStatement")) {
|
|
31735
|
+
const containingStatementIndex = (parent.body ?? []).findIndex((statement) => isAstDescendant(current, statement));
|
|
31736
|
+
if (containingStatementIndex >= 0) {
|
|
31737
|
+
if ((parent.body ?? []).slice(0, containingStatementIndex).some((statement) => !statementCanCompleteNormally(statement, environment))) return false;
|
|
31738
|
+
}
|
|
31739
|
+
}
|
|
31740
|
+
current = parent;
|
|
31741
|
+
}
|
|
31742
|
+
return true;
|
|
31743
|
+
};
|
|
31744
|
+
const createStateTriggerReachability = ({ analysis, context, effectFunction }) => {
|
|
31745
|
+
const snapshotEnvironment = collectSnapshotEnvironment(analysis, effectFunction, context);
|
|
31746
|
+
if (snapshotEnvironment.refShapes.size === 0) return () => true;
|
|
31747
|
+
const environment = {
|
|
31748
|
+
analysis,
|
|
31749
|
+
context,
|
|
31750
|
+
effectFunction,
|
|
31751
|
+
snapshotEnvironment,
|
|
31752
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
31753
|
+
visitedSymbolIds: /* @__PURE__ */ new Set(),
|
|
31754
|
+
allowHelperCall: true
|
|
31755
|
+
};
|
|
31756
|
+
return (target) => isReachableUnderSnapshotEnvironment(target, effectFunction, environment);
|
|
31757
|
+
};
|
|
31758
|
+
//#endregion
|
|
30646
31759
|
//#region src/plugin/rules/state-and-effects/no-chain-state-updates.ts
|
|
30647
31760
|
const getUseStateDeclarator = (ref) => (ref.resolved?.defs ?? []).map((def) => def.node).find((node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "ArrayPattern")) ?? null;
|
|
30648
31761
|
const isDeclaredWithin = (node, container) => {
|
|
@@ -30708,11 +31821,17 @@ const noChainStateUpdates = defineRule({
|
|
|
30708
31821
|
if (stateDeps.length === 0) return;
|
|
30709
31822
|
if (stateDeps.every((ref) => isExternallyDrivenState(analysis, ref))) return;
|
|
30710
31823
|
const stateDepDeclarators = new Set(stateDeps.map((ref) => getUseStateDeclarator(ref)).filter((declarator) => declarator !== null));
|
|
31824
|
+
const isReachableFromStateTrigger = createStateTriggerReachability({
|
|
31825
|
+
analysis,
|
|
31826
|
+
context,
|
|
31827
|
+
effectFunction: effectFn
|
|
31828
|
+
});
|
|
30711
31829
|
const domSyncedStateDeclarators = /* @__PURE__ */ new Set();
|
|
30712
31830
|
for (const ref of effectFnRefs) {
|
|
30713
31831
|
if (!isSyncStateSetterCall(analysis, ref, effectFn)) continue;
|
|
30714
31832
|
const callExpr = getCallExpr(ref);
|
|
30715
31833
|
if (!callExpr) continue;
|
|
31834
|
+
if (!isReachableFromStateTrigger(callExpr)) continue;
|
|
30716
31835
|
if (!readsPostMountValueThroughLocals(callExpr, effectFn, { ignoreBareRefCurrent: true })) continue;
|
|
30717
31836
|
const declarator = getUseStateDeclarator(ref);
|
|
30718
31837
|
if (declarator) domSyncedStateDeclarators.add(declarator);
|
|
@@ -30721,6 +31840,7 @@ const noChainStateUpdates = defineRule({
|
|
|
30721
31840
|
if (!isSyncStateSetterCall(analysis, ref, effectFn)) continue;
|
|
30722
31841
|
const callExpr = getCallExpr(ref);
|
|
30723
31842
|
if (!callExpr) continue;
|
|
31843
|
+
if (!isReachableFromStateTrigger(callExpr)) continue;
|
|
30724
31844
|
if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isState(analysis, argRef))) continue;
|
|
30725
31845
|
const setterDeclarator = getUseStateDeclarator(ref);
|
|
30726
31846
|
if (setterDeclarator && domSyncedStateDeclarators.has(setterDeclarator)) continue;
|
|
@@ -40920,7 +42040,7 @@ const noPropCallbackInRender = defineRule({
|
|
|
40920
42040
|
if (!analysis) return;
|
|
40921
42041
|
const callee = stripParenExpression(node.callee);
|
|
40922
42042
|
if (isFunctionLike$1(callee)) return;
|
|
40923
|
-
if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return;
|
|
42043
|
+
if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference, { nativeMethodScopes: context.scopes }))) return;
|
|
40924
42044
|
context.report({
|
|
40925
42045
|
node,
|
|
40926
42046
|
message: "This prop callback runs during render. React can replay or discard render work, so the callback can fire more than once or for UI that never commits."
|
|
@@ -41883,18 +43003,107 @@ const getNodeText$1 = (node) => {
|
|
|
41883
43003
|
return value;
|
|
41884
43004
|
});
|
|
41885
43005
|
};
|
|
41886
|
-
const
|
|
41887
|
-
|
|
41888
|
-
|
|
41889
|
-
|
|
41890
|
-
|
|
41891
|
-
|
|
41892
|
-
|
|
41893
|
-
|
|
41894
|
-
|
|
41895
|
-
|
|
43006
|
+
const normalizeAsBoolean = (identity) => ({
|
|
43007
|
+
...identity,
|
|
43008
|
+
booleanNormalization: identity.booleanNormalization === "identity" ? "normalized" : identity.booleanNormalization
|
|
43009
|
+
});
|
|
43010
|
+
const negateBoolean = (identity) => ({
|
|
43011
|
+
...identity,
|
|
43012
|
+
booleanNormalization: identity.booleanNormalization === "negated" ? "normalized" : "negated"
|
|
43013
|
+
});
|
|
43014
|
+
const getLivePropExpressionIdentity = (analysis, context, node, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
43015
|
+
const expression = stripParenExpression(node);
|
|
43016
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
43017
|
+
const reference = getRef(analysis, expression);
|
|
43018
|
+
const symbol = context.scopes.symbolFor(expression);
|
|
43019
|
+
if (!reference || !symbol) return null;
|
|
43020
|
+
if (isProp(analysis, reference)) return {
|
|
43021
|
+
propSymbolId: symbol.id,
|
|
43022
|
+
memberPath: [],
|
|
43023
|
+
booleanNormalization: "identity"
|
|
43024
|
+
};
|
|
43025
|
+
if (visitedSymbolIds.has(symbol.id) || symbol.kind !== "const") return null;
|
|
43026
|
+
visitedSymbolIds.add(symbol.id);
|
|
43027
|
+
const initializer = getDirectConstInitializer(symbol);
|
|
43028
|
+
if (initializer) return getLivePropExpressionIdentity(analysis, context, initializer, visitedSymbolIds);
|
|
43029
|
+
return getUpstreamRefs(analysis, reference).some((upstreamReference) => isProp(analysis, upstreamReference)) ? {
|
|
43030
|
+
propSymbolId: symbol.id,
|
|
43031
|
+
memberPath: [],
|
|
43032
|
+
booleanNormalization: "identity"
|
|
43033
|
+
} : null;
|
|
43034
|
+
}
|
|
43035
|
+
if (isNodeOfType(expression, "MemberExpression")) {
|
|
43036
|
+
const propertyName = getStaticMemberPropertyName(expression);
|
|
43037
|
+
if (!propertyName) return null;
|
|
43038
|
+
const objectIdentity = getLivePropExpressionIdentity(analysis, context, expression.object, visitedSymbolIds);
|
|
43039
|
+
if (!objectIdentity) return null;
|
|
43040
|
+
return {
|
|
43041
|
+
...objectIdentity,
|
|
43042
|
+
memberPath: [...objectIdentity.memberPath, propertyName]
|
|
43043
|
+
};
|
|
43044
|
+
}
|
|
43045
|
+
if (isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "Boolean" && context.scopes.isGlobalReference(expression.callee) && expression.arguments?.length === 1) {
|
|
43046
|
+
const argument = expression.arguments[0];
|
|
43047
|
+
const argumentIdentity = getLivePropExpressionIdentity(analysis, context, argument, visitedSymbolIds);
|
|
43048
|
+
return argumentIdentity ? normalizeAsBoolean(argumentIdentity) : null;
|
|
43049
|
+
}
|
|
43050
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "!") {
|
|
43051
|
+
const argumentIdentity = getLivePropExpressionIdentity(analysis, context, expression.argument, visitedSymbolIds);
|
|
43052
|
+
return argumentIdentity ? negateBoolean(argumentIdentity) : null;
|
|
43053
|
+
}
|
|
43054
|
+
return null;
|
|
43055
|
+
};
|
|
43056
|
+
const isMountSnapshotInitializer = (context, node) => {
|
|
43057
|
+
if (!isReactApiCall(node, "useMemo", context.scopes, { resolveNamedAliases: true }) || !isNodeOfType(node, "CallExpression")) return false;
|
|
43058
|
+
const dependencies = node.arguments?.[1];
|
|
43059
|
+
return isNodeOfType(dependencies, "ArrayExpression") && dependencies.elements.length === 0;
|
|
43060
|
+
};
|
|
43061
|
+
const isMountSnapshotBinding = (context, identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
43062
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
43063
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
43064
|
+
visitedSymbolIds.add(symbol.id);
|
|
43065
|
+
const initializer = getDirectConstInitializer(symbol);
|
|
43066
|
+
if (!initializer) return false;
|
|
43067
|
+
if (isMountSnapshotInitializer(context, initializer)) return true;
|
|
43068
|
+
const expression = stripParenExpression(initializer);
|
|
43069
|
+
return isNodeOfType(expression, "Identifier") && isMountSnapshotBinding(context, expression, visitedSymbolIds);
|
|
41896
43070
|
};
|
|
41897
|
-
const
|
|
43071
|
+
const isConstantBooleanExpression = (context, node, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
43072
|
+
const expression = stripParenExpression(node);
|
|
43073
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
43074
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
43075
|
+
const symbol = context.scopes.symbolFor(expression);
|
|
43076
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
43077
|
+
visitedSymbolIds.add(symbol.id);
|
|
43078
|
+
const initializer = getDirectConstInitializer(symbol);
|
|
43079
|
+
return Boolean(initializer && isConstantBooleanExpression(context, initializer, visitedSymbolIds));
|
|
43080
|
+
}
|
|
43081
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "!") return isConstantBooleanExpression(context, expression.argument, visitedSymbolIds);
|
|
43082
|
+
if (isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "Boolean" && context.scopes.isGlobalReference(expression.callee) && expression.arguments?.length === 1) return isConstantBooleanExpression(context, expression.arguments[0], visitedSymbolIds);
|
|
43083
|
+
return false;
|
|
43084
|
+
};
|
|
43085
|
+
const isProvenLiveBinding = (analysis, context, identifier) => {
|
|
43086
|
+
const reference = getRef(analysis, identifier);
|
|
43087
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
43088
|
+
if (!reference || !symbol || symbol.kind !== "const" || isOutsideAllFunctions(symbol)) return false;
|
|
43089
|
+
const initializer = symbol.initializer;
|
|
43090
|
+
if (!initializer || isMountSnapshotBinding(context, identifier)) return false;
|
|
43091
|
+
if (getDownstreamRefs(analysis, initializer).some((initializerReference) => isRefCurrent(initializerReference))) return false;
|
|
43092
|
+
return !isConstantBooleanExpression(context, initializer);
|
|
43093
|
+
};
|
|
43094
|
+
const areSameProvenLiveBinding = (analysis, context, left, right) => {
|
|
43095
|
+
const leftExpression = stripParenExpression(left);
|
|
43096
|
+
const rightExpression = stripParenExpression(right);
|
|
43097
|
+
if (!isNodeOfType(leftExpression, "Identifier") || !isNodeOfType(rightExpression, "Identifier")) return false;
|
|
43098
|
+
const leftSymbol = context.scopes.symbolFor(leftExpression);
|
|
43099
|
+
const rightSymbol = context.scopes.symbolFor(rightExpression);
|
|
43100
|
+
return Boolean(leftSymbol && leftSymbol === rightSymbol && isProvenLiveBinding(analysis, context, leftExpression));
|
|
43101
|
+
};
|
|
43102
|
+
const haveSameLivePropExpressionIdentity = (left, right) => {
|
|
43103
|
+
if (left.propSymbolId !== right.propSymbolId || left.booleanNormalization !== right.booleanNormalization || left.memberPath.length !== right.memberPath.length) return false;
|
|
43104
|
+
return left.memberPath.every((propertyName, index) => propertyName === right.memberPath[index]);
|
|
43105
|
+
};
|
|
43106
|
+
const isSetStateToInitialValue = (analysis, context, setterRef) => {
|
|
41898
43107
|
const callExpr = getCallExpr(setterRef);
|
|
41899
43108
|
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) return false;
|
|
41900
43109
|
const setStateToValue = callExpr.arguments?.[0];
|
|
@@ -41905,7 +43114,12 @@ const isSetStateToInitialValue = (analysis, setterRef) => {
|
|
|
41905
43114
|
if (isUndefinedNode(setStateToValue) && isUndefinedNode(stateInitialValue)) return true;
|
|
41906
43115
|
if (setStateToValue == null && stateInitialValue == null) return true;
|
|
41907
43116
|
if (setStateToValue && !stateInitialValue || !setStateToValue && stateInitialValue) return false;
|
|
41908
|
-
if (stateInitialValue &&
|
|
43117
|
+
if (stateInitialValue && setStateToValue) {
|
|
43118
|
+
const initialLivePropIdentity = getLivePropExpressionIdentity(analysis, context, stateInitialValue);
|
|
43119
|
+
const nextLivePropIdentity = getLivePropExpressionIdentity(analysis, context, setStateToValue);
|
|
43120
|
+
if (initialLivePropIdentity && nextLivePropIdentity && haveSameLivePropExpressionIdentity(initialLivePropIdentity, nextLivePropIdentity)) return false;
|
|
43121
|
+
if (areSameProvenLiveBinding(analysis, context, stateInitialValue, setStateToValue)) return false;
|
|
43122
|
+
}
|
|
41909
43123
|
return getNodeText$1(setStateToValue) === getNodeText$1(stateInitialValue);
|
|
41910
43124
|
};
|
|
41911
43125
|
const countUseStates = (analysis, componentNode) => {
|
|
@@ -41914,10 +43128,10 @@ const countUseStates = (analysis, componentNode) => {
|
|
|
41914
43128
|
for (const ref of getDownstreamRefs(analysis, componentNode)) if (isState(analysis, ref)) stateVariables.add(ref.resolved);
|
|
41915
43129
|
return stateVariables.size;
|
|
41916
43130
|
};
|
|
41917
|
-
const findPropUsedToResetAllState = (analysis, effectFnRefs, depsRefs, useEffectNode, effectFn) => {
|
|
43131
|
+
const findPropUsedToResetAllState = (analysis, context, effectFnRefs, depsRefs, useEffectNode, effectFn) => {
|
|
41918
43132
|
const stateSetterRefs = effectFnRefs.filter((ref) => isSyncStateSetterCall(analysis, ref, effectFn));
|
|
41919
43133
|
if (stateSetterRefs.length === 0) return null;
|
|
41920
|
-
if (!stateSetterRefs.every((ref) => isSetStateToInitialValue(analysis, ref))) return null;
|
|
43134
|
+
if (!stateSetterRefs.every((ref) => isSetStateToInitialValue(analysis, context, ref))) return null;
|
|
41921
43135
|
if (stateSetterRefs.every((setterRef) => effectFnRefs.some((otherRef) => otherRef !== setterRef && otherRef.resolved === setterRef.resolved && Boolean(getCallExpr(otherRef)) && !isSyncStateSetterCall(analysis, otherRef, effectFn)))) return null;
|
|
41922
43136
|
const containing = findContainingNode(analysis, useEffectNode);
|
|
41923
43137
|
if (new Set(stateSetterRefs.map((setterRef) => setterRef.resolved)).size !== countUseStates(analysis, containing)) return null;
|
|
@@ -41941,7 +43155,7 @@ const noResetAllStateOnPropChange = defineRule({
|
|
|
41941
43155
|
if (containing && isCustomHook(containing)) return;
|
|
41942
43156
|
const effectFn = getEffectFn(analysis, node);
|
|
41943
43157
|
if (!effectFn) return;
|
|
41944
|
-
if (!findPropUsedToResetAllState(analysis, effectFnRefs, depsRefs, node, effectFn)) return;
|
|
43158
|
+
if (!findPropUsedToResetAllState(analysis, context, effectFnRefs, depsRefs, node, effectFn)) return;
|
|
41945
43159
|
context.report({
|
|
41946
43160
|
node,
|
|
41947
43161
|
message: `Your users briefly see stale state when a prop changes because this useEffect clears all state.`
|
|
@@ -45744,19 +46958,726 @@ const nosqlInjectionRisk = defineRule({
|
|
|
45744
46958
|
})
|
|
45745
46959
|
});
|
|
45746
46960
|
//#endregion
|
|
46961
|
+
//#region src/plugin/utils/export-all-adds-runtime-values.ts
|
|
46962
|
+
const getExportedName = (node) => {
|
|
46963
|
+
if (!node) return null;
|
|
46964
|
+
if (isNodeOfType(node, "Identifier")) return node.name;
|
|
46965
|
+
if (isNodeOfType(node, "Literal") && typeof node.value === "string") return node.value;
|
|
46966
|
+
return null;
|
|
46967
|
+
};
|
|
46968
|
+
const programHasRuntimeNamedExports = (filePath, program, visitedFilePaths) => {
|
|
46969
|
+
if (!isNodeOfType(program, "Program")) return true;
|
|
46970
|
+
for (const statement of program.body) {
|
|
46971
|
+
if (isNodeOfType(statement, "ExportDefaultDeclaration")) continue;
|
|
46972
|
+
if (isNodeOfType(statement, "ExportAllDeclaration")) {
|
|
46973
|
+
if (statement.exportKind === "type") continue;
|
|
46974
|
+
if (statement.exported) return true;
|
|
46975
|
+
if (typeof statement.source.value !== "string") return true;
|
|
46976
|
+
if (exportAllAddsRuntimeValues(filePath, statement.source.value, visitedFilePaths)) return true;
|
|
46977
|
+
continue;
|
|
46978
|
+
}
|
|
46979
|
+
if (!isNodeOfType(statement, "ExportNamedDeclaration")) continue;
|
|
46980
|
+
if (statement.exportKind === "type") continue;
|
|
46981
|
+
if (statement.declaration) {
|
|
46982
|
+
if (statement.declaration.type !== "TSInterfaceDeclaration" && statement.declaration.type !== "TSTypeAliasDeclaration" && statement.declaration.type !== "TSDeclareFunction") return true;
|
|
46983
|
+
continue;
|
|
46984
|
+
}
|
|
46985
|
+
if (isEverySpecifierInlineType(statement.specifiers, "ExportSpecifier", "exportKind")) continue;
|
|
46986
|
+
for (const specifier of statement.specifiers) {
|
|
46987
|
+
if (!isNodeOfType(specifier, "ExportSpecifier") || specifier.exportKind === "type") continue;
|
|
46988
|
+
if (getExportedName(specifier.exported) !== "default") return true;
|
|
46989
|
+
}
|
|
46990
|
+
}
|
|
46991
|
+
return false;
|
|
46992
|
+
};
|
|
46993
|
+
const exportAllAddsRuntimeValues = (importerFilePath, source, visitedFilePaths = /* @__PURE__ */ new Set()) => {
|
|
46994
|
+
const resolvedFilePath = resolveRelativeImportPath(importerFilePath, source);
|
|
46995
|
+
if (!resolvedFilePath) return true;
|
|
46996
|
+
if (path.extname(resolvedFilePath) === ".cjs" || resolvedFilePath.endsWith(".cts")) return true;
|
|
46997
|
+
if (visitedFilePaths.has(resolvedFilePath)) return true;
|
|
46998
|
+
visitedFilePaths.add(resolvedFilePath);
|
|
46999
|
+
const program = parseSourceFile(resolvedFilePath);
|
|
47000
|
+
if (!program) return true;
|
|
47001
|
+
return programHasRuntimeNamedExports(resolvedFilePath, program, visitedFilePaths);
|
|
47002
|
+
};
|
|
47003
|
+
//#endregion
|
|
45747
47004
|
//#region src/plugin/utils/is-framework-route-or-special-filename.ts
|
|
45748
|
-
const
|
|
45749
|
-
const
|
|
45750
|
-
|
|
45751
|
-
|
|
45752
|
-
|
|
45753
|
-
|
|
45754
|
-
|
|
45755
|
-
const
|
|
47005
|
+
const NEXT_APP_ROUTE_FILE_PATTERN = new RegExp(`^(page|layout|loading|error|not-found|template|default|global-error|route)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
47006
|
+
const NEXT_PAGES_ROUTE_FILE_PATTERN = new RegExp(`^(_app|_document|_error|_meta)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
47007
|
+
const EXPO_ROUTE_FILE_PATTERN = new RegExp(`^(_layout|\\+html|\\+not-found|\\+native-intent)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
47008
|
+
const TANSTACK_ROUTE_FILE_PATTERN = new RegExp(`(?:^__root|\\.lazy)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
47009
|
+
const REACT_ROUTER_FILE_PATTERN = new RegExp(`^(root|entry\\.client|entry\\.server)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
47010
|
+
const isInNextDirectory = (context, directoryPath) => {
|
|
47011
|
+
if (isInProjectDirectory(context, directoryPath)) return true;
|
|
47012
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
47013
|
+
if (path.isAbsolute(filename)) return false;
|
|
47014
|
+
return filename.startsWith(`${directoryPath}/`) || filename.includes(`/${directoryPath}/`);
|
|
47015
|
+
};
|
|
47016
|
+
const isInExpoRouteDirectory = (context) => {
|
|
47017
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
47018
|
+
const configuredRootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
47019
|
+
const packageDirectory = path.isAbsolute(filename) ? findNearestPackageDirectory(filename) : null;
|
|
47020
|
+
const projectRelativeFilename = getProjectRelativeFilename(filename, configuredRootDirectory ?? packageDirectory ?? void 0);
|
|
47021
|
+
const relativeFilename = projectRelativeFilename === filename && path.isAbsolute(filename) ? filename.split("/").slice(2).join("/") : projectRelativeFilename;
|
|
47022
|
+
return relativeFilename.startsWith("app/") || relativeFilename.startsWith("src/app/");
|
|
47023
|
+
};
|
|
47024
|
+
const isFrameworkRouteOrSpecialFilename = (context, runtime) => {
|
|
47025
|
+
const rawFilename = context.filename;
|
|
45756
47026
|
if (!rawFilename) return false;
|
|
45757
|
-
if (isNextjsMetadataImageRouteFilename(rawFilename)) return true;
|
|
45758
47027
|
const basename = path.basename(rawFilename);
|
|
45759
|
-
return
|
|
47028
|
+
if (runtime === "next") return isInNextDirectory(context, "app") && (isNextjsMetadataImageRouteFilename(rawFilename) || NEXT_APP_ROUTE_FILE_PATTERN.test(basename)) || isInNextDirectory(context, "pages") && NEXT_PAGES_ROUTE_FILE_PATTERN.test(basename);
|
|
47029
|
+
if (runtime === "expo") return isInExpoRouteDirectory(context) && EXPO_ROUTE_FILE_PATTERN.test(basename);
|
|
47030
|
+
if (runtime === "tanstack") return TANSTACK_ROUTE_FILE_PATTERN.test(basename);
|
|
47031
|
+
if (runtime === "react-router" || runtime === "remix") return REACT_ROUTER_FILE_PATTERN.test(basename);
|
|
47032
|
+
return false;
|
|
47033
|
+
};
|
|
47034
|
+
//#endregion
|
|
47035
|
+
//#region src/plugin/utils/function-has-react-element-return-type.ts
|
|
47036
|
+
const isReactNamespaceBinding = (node) => {
|
|
47037
|
+
if (!isNodeOfType(node, "Identifier")) return false;
|
|
47038
|
+
const binding = getImportBindingForName(node, node.name);
|
|
47039
|
+
return Boolean(binding && binding.source === "react" && (binding.isNamespace || binding.exportedName === "default"));
|
|
47040
|
+
};
|
|
47041
|
+
const hasLocalJsxNamespace = (node) => {
|
|
47042
|
+
const program = findProgramRoot(node);
|
|
47043
|
+
if (!program || !isNodeOfType(program, "Program")) return false;
|
|
47044
|
+
return program.body.some((statement) => isNodeOfType(statement, "TSModuleDeclaration") && isNodeOfType(statement.id, "Identifier") && statement.id.name === "JSX");
|
|
47045
|
+
};
|
|
47046
|
+
const isReactElementTypeReference = (node) => {
|
|
47047
|
+
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
47048
|
+
const typeName = node.typeName;
|
|
47049
|
+
if (isNodeOfType(typeName, "Identifier")) return getImportedNameFromModule(typeName, typeName.name, "react") === "ReactElement";
|
|
47050
|
+
if (!isNodeOfType(typeName, "TSQualifiedName")) return false;
|
|
47051
|
+
if (!isNodeOfType(typeName.right, "Identifier")) return false;
|
|
47052
|
+
if (typeName.right.name === "ReactElement" && isReactNamespaceBinding(typeName.left)) return true;
|
|
47053
|
+
if (typeName.right.name !== "Element") return false;
|
|
47054
|
+
if (isNodeOfType(typeName.left, "Identifier")) {
|
|
47055
|
+
if (typeName.left.name !== "JSX") return false;
|
|
47056
|
+
const binding = getImportBindingForName(typeName.left, typeName.left.name);
|
|
47057
|
+
if (!binding) return !hasLocalJsxNamespace(typeName.left);
|
|
47058
|
+
return binding.source === "react" && binding.exportedName === "JSX";
|
|
47059
|
+
}
|
|
47060
|
+
return isNodeOfType(typeName.left, "TSQualifiedName") && isNodeOfType(typeName.left.right, "Identifier") && typeName.left.right.name === "JSX" && isReactNamespaceBinding(typeName.left.left);
|
|
47061
|
+
};
|
|
47062
|
+
const containsReactElementType = (node) => {
|
|
47063
|
+
if (isNodeOfType(node, "TSUnionType")) return node.types.some((member) => containsReactElementType(member));
|
|
47064
|
+
return isReactElementTypeReference(node);
|
|
47065
|
+
};
|
|
47066
|
+
const functionHasReactElementReturnType = (functionNode) => {
|
|
47067
|
+
const returnType = Reflect.get(functionNode, "returnType");
|
|
47068
|
+
if (!isNodeOfType(returnType, "TSTypeAnnotation")) return false;
|
|
47069
|
+
return containsReactElementType(returnType.typeAnnotation);
|
|
47070
|
+
};
|
|
47071
|
+
//#endregion
|
|
47072
|
+
//#region src/plugin/constants/fast-refresh.ts
|
|
47073
|
+
const FAST_REFRESH_CONFIG_FILENAMES = [
|
|
47074
|
+
"vite.config.ts",
|
|
47075
|
+
"vite.config.mts",
|
|
47076
|
+
"vite.config.cts",
|
|
47077
|
+
"vite.config.js",
|
|
47078
|
+
"vite.config.mjs",
|
|
47079
|
+
"vite.config.cjs",
|
|
47080
|
+
"webpack.config.ts",
|
|
47081
|
+
"webpack.config.js",
|
|
47082
|
+
"webpack.config.mjs",
|
|
47083
|
+
"webpack.config.cjs",
|
|
47084
|
+
"rsbuild.config.ts",
|
|
47085
|
+
"rsbuild.config.js",
|
|
47086
|
+
"rspack.config.ts",
|
|
47087
|
+
"rspack.config.js"
|
|
47088
|
+
];
|
|
47089
|
+
const MINIMUM_FAST_REFRESH_VERSIONS = {
|
|
47090
|
+
dumi: {
|
|
47091
|
+
major: 2,
|
|
47092
|
+
minor: 0
|
|
47093
|
+
},
|
|
47094
|
+
expo: {
|
|
47095
|
+
major: 36,
|
|
47096
|
+
minor: 0
|
|
47097
|
+
},
|
|
47098
|
+
gatsby: {
|
|
47099
|
+
major: 2,
|
|
47100
|
+
minor: 31
|
|
47101
|
+
},
|
|
47102
|
+
next: {
|
|
47103
|
+
major: 9,
|
|
47104
|
+
minor: 4
|
|
47105
|
+
},
|
|
47106
|
+
parcel: {
|
|
47107
|
+
major: 2,
|
|
47108
|
+
minor: 0
|
|
47109
|
+
},
|
|
47110
|
+
reactForGatsbyTwo: {
|
|
47111
|
+
major: 17,
|
|
47112
|
+
minor: 0
|
|
47113
|
+
},
|
|
47114
|
+
reactNative: {
|
|
47115
|
+
major: 0,
|
|
47116
|
+
minor: 61
|
|
47117
|
+
},
|
|
47118
|
+
reactScripts: {
|
|
47119
|
+
major: 4,
|
|
47120
|
+
minor: 0
|
|
47121
|
+
},
|
|
47122
|
+
storybookReact: {
|
|
47123
|
+
major: 6,
|
|
47124
|
+
minor: 1
|
|
47125
|
+
}
|
|
47126
|
+
};
|
|
47127
|
+
//#endregion
|
|
47128
|
+
//#region src/plugin/utils/get-fast-refresh-file-status.ts
|
|
47129
|
+
const INTEGRATION_IMPORTS = new Map([
|
|
47130
|
+
["@vitejs/plugin-react", {
|
|
47131
|
+
importedNames: null,
|
|
47132
|
+
requiresViteDevelopmentServer: true,
|
|
47133
|
+
runtime: "generic"
|
|
47134
|
+
}],
|
|
47135
|
+
["@vitejs/plugin-react-swc", {
|
|
47136
|
+
importedNames: null,
|
|
47137
|
+
requiresViteDevelopmentServer: true,
|
|
47138
|
+
runtime: "generic"
|
|
47139
|
+
}],
|
|
47140
|
+
["@pmmmwh/react-refresh-webpack-plugin", {
|
|
47141
|
+
importedNames: null,
|
|
47142
|
+
runtime: "generic"
|
|
47143
|
+
}],
|
|
47144
|
+
["@react-router/dev/vite", {
|
|
47145
|
+
importedNames: new Set(["reactRouter"]),
|
|
47146
|
+
requiresViteDevelopmentServer: true,
|
|
47147
|
+
runtime: "react-router"
|
|
47148
|
+
}],
|
|
47149
|
+
["@remix-run/dev", {
|
|
47150
|
+
importedNames: new Set(["vitePlugin"]),
|
|
47151
|
+
requiresViteDevelopmentServer: true,
|
|
47152
|
+
runtime: "remix"
|
|
47153
|
+
}],
|
|
47154
|
+
["@rsbuild/plugin-react", {
|
|
47155
|
+
importedNames: new Set(["pluginReact"]),
|
|
47156
|
+
runtime: "generic"
|
|
47157
|
+
}],
|
|
47158
|
+
["@rspack/plugin-react-refresh", {
|
|
47159
|
+
importedNames: null,
|
|
47160
|
+
runtime: "generic"
|
|
47161
|
+
}],
|
|
47162
|
+
["@rozenite/vite-plugin", {
|
|
47163
|
+
importedNames: new Set(["rozenitePlugin"]),
|
|
47164
|
+
requiresViteDevelopmentServer: true,
|
|
47165
|
+
runtime: "generic"
|
|
47166
|
+
}],
|
|
47167
|
+
["@tanstack/react-start/plugin/vite", {
|
|
47168
|
+
importedNames: new Set(["tanstackStart"]),
|
|
47169
|
+
requiresViteDevelopmentServer: true,
|
|
47170
|
+
runtime: "tanstack"
|
|
47171
|
+
}]
|
|
47172
|
+
]);
|
|
47173
|
+
const REGISTERED_INTEGRATION_RUNTIME_PRECEDENCE = [
|
|
47174
|
+
"tanstack",
|
|
47175
|
+
"remix",
|
|
47176
|
+
"react-router",
|
|
47177
|
+
"generic"
|
|
47178
|
+
];
|
|
47179
|
+
const cachedLocalStatusByManifest = /* @__PURE__ */ new WeakMap();
|
|
47180
|
+
const cachedWorkspaceIndexByManifest = /* @__PURE__ */ new WeakMap();
|
|
47181
|
+
const INACTIVE_STATUS = {
|
|
47182
|
+
isActive: false,
|
|
47183
|
+
runtime: "generic"
|
|
47184
|
+
};
|
|
47185
|
+
const WORKSPACE_IGNORED_DIRECTORY_NAMES = new Set([
|
|
47186
|
+
".git",
|
|
47187
|
+
".next",
|
|
47188
|
+
"build",
|
|
47189
|
+
"coverage",
|
|
47190
|
+
"dist",
|
|
47191
|
+
"node_modules",
|
|
47192
|
+
"out"
|
|
47193
|
+
]);
|
|
47194
|
+
const parseVersion = (version) => {
|
|
47195
|
+
if (typeof version !== "string") return null;
|
|
47196
|
+
const match = version.match(/(?:^|[^\d])(\d+)(?:\.(\d+))?/);
|
|
47197
|
+
if (!match) return null;
|
|
47198
|
+
return {
|
|
47199
|
+
major: Number(match[1]),
|
|
47200
|
+
minor: Number(match[2] ?? 0)
|
|
47201
|
+
};
|
|
47202
|
+
};
|
|
47203
|
+
const isVersionAtLeast = (version, minimum) => {
|
|
47204
|
+
const parsed = parseVersion(version);
|
|
47205
|
+
if (!parsed) return false;
|
|
47206
|
+
return parsed.major > minimum.major || parsed.major === minimum.major && parsed.minor >= minimum.minor;
|
|
47207
|
+
};
|
|
47208
|
+
const getDependencyVersion = (manifest, dependencyName) => manifest.dependencies?.[dependencyName] ?? manifest.devDependencies?.[dependencyName] ?? manifest.peerDependencies?.[dependencyName] ?? manifest.optionalDependencies?.[dependencyName];
|
|
47209
|
+
const getRuntimeDependencyVersion = (manifest, dependencyName) => manifest.dependencies?.[dependencyName] ?? manifest.optionalDependencies?.[dependencyName];
|
|
47210
|
+
const getOwnedDependencyVersion = (manifest, dependencyName, developmentCommandPattern) => {
|
|
47211
|
+
const runtimeVersion = getRuntimeDependencyVersion(manifest, dependencyName);
|
|
47212
|
+
if (runtimeVersion !== void 0) return runtimeVersion;
|
|
47213
|
+
return Object.values(manifest.scripts ?? {}).some((script) => typeof script === "string" && developmentCommandPattern.test(script)) ? manifest.devDependencies?.[dependencyName] : void 0;
|
|
47214
|
+
};
|
|
47215
|
+
const hasParcelBrowserEntry = (manifest) => {
|
|
47216
|
+
if (typeof manifest.source === "string" && manifest.source.endsWith(".html")) return true;
|
|
47217
|
+
return Object.values(manifest.scripts ?? {}).some((script) => typeof script === "string" && /(?:^|\s)parcel(?:\s+serve)?\s+[^\n]*\.html(?:\s|$)/.test(script));
|
|
47218
|
+
};
|
|
47219
|
+
const hasParcelDevelopmentCommand = (manifest) => Object.values(manifest.scripts ?? {}).some((script) => typeof script === "string" && /(?:^|[\s;&|'\\"])parcel(?=\s|$)(?!\s+(?:build|help|watch|--help|--version|-h|-V)(?:\s|$))/.test(script) && !/(?:^|\s)--no-hmr(?:\s|$)/.test(script));
|
|
47220
|
+
const hasOwnedDevelopmentCommand = (manifest, commandPattern) => Object.values(manifest.scripts ?? {}).some((script) => typeof script === "string" && commandPattern.test(script));
|
|
47221
|
+
const hasViteDevelopmentCommand = (manifest) => Object.values(manifest.scripts ?? {}).some((script) => {
|
|
47222
|
+
if (typeof script !== "string") return false;
|
|
47223
|
+
return [...script.matchAll(/(?:^|[\s;&|'\\"])vite(?:\s+(build|preview|test)\b)?/g)].some((match) => match[1] === void 0);
|
|
47224
|
+
});
|
|
47225
|
+
const hasViteBrowserEntry = (manifest, packageDirectory) => {
|
|
47226
|
+
if (typeof manifest.source === "string" && manifest.source.endsWith(".html")) return true;
|
|
47227
|
+
const browserEntryPath = path.join(packageDirectory, "index.html");
|
|
47228
|
+
recordExistenceProbe(browserEntryPath);
|
|
47229
|
+
return fs.existsSync(browserEntryPath);
|
|
47230
|
+
};
|
|
47231
|
+
const getBuiltInStatus = (manifest) => {
|
|
47232
|
+
if (isVersionAtLeast(getOwnedDependencyVersion(manifest, "next", /(?:^|\s)next\s+dev(?:\s|$)/), MINIMUM_FAST_REFRESH_VERSIONS.next)) return {
|
|
47233
|
+
isActive: true,
|
|
47234
|
+
runtime: "next"
|
|
47235
|
+
};
|
|
47236
|
+
const gatsbyDependency = getOwnedDependencyVersion(manifest, "gatsby", /(?:^|\s)gatsby\s+develop(?:\s|$)/);
|
|
47237
|
+
const gatsbyVersion = parseVersion(gatsbyDependency);
|
|
47238
|
+
const hasGatsbyFastRefresh = gatsbyVersion !== null && (gatsbyVersion.major >= 3 || isVersionAtLeast(gatsbyDependency, MINIMUM_FAST_REFRESH_VERSIONS.gatsby) && isVersionAtLeast(getDependencyVersion(manifest, "react"), MINIMUM_FAST_REFRESH_VERSIONS.reactForGatsbyTwo));
|
|
47239
|
+
if (isVersionAtLeast(getOwnedDependencyVersion(manifest, "react-scripts", /(?:^|\s)react-scripts\s+start(?:\s|$)/), MINIMUM_FAST_REFRESH_VERSIONS.reactScripts) || hasGatsbyFastRefresh || isVersionAtLeast(getDependencyVersion(manifest, "parcel"), MINIMUM_FAST_REFRESH_VERSIONS.parcel) && declaresDependency(manifest, "react") && hasParcelDevelopmentCommand(manifest) && hasParcelBrowserEntry(manifest)) return {
|
|
47240
|
+
isActive: true,
|
|
47241
|
+
runtime: "generic"
|
|
47242
|
+
};
|
|
47243
|
+
if (isVersionAtLeast(getOwnedDependencyVersion(manifest, "expo", /(?:^|\s)expo\s+start(?:\s|$)/), MINIMUM_FAST_REFRESH_VERSIONS.expo) || isVersionAtLeast(getOwnedDependencyVersion(manifest, "react-native", /(?:^|\s)react-native\s+start(?:\s|$)/), MINIMUM_FAST_REFRESH_VERSIONS.reactNative)) return {
|
|
47244
|
+
isActive: true,
|
|
47245
|
+
runtime: "expo"
|
|
47246
|
+
};
|
|
47247
|
+
if (isVersionAtLeast(getOwnedDependencyVersion(manifest, "dumi", /(?:^|\s)dumi\s+dev(?:\s|$)/), MINIMUM_FAST_REFRESH_VERSIONS.dumi)) return {
|
|
47248
|
+
isActive: true,
|
|
47249
|
+
runtime: "generic"
|
|
47250
|
+
};
|
|
47251
|
+
return null;
|
|
47252
|
+
};
|
|
47253
|
+
const getIntegrationLocalNames = (program, scopes) => {
|
|
47254
|
+
const localBindings = /* @__PURE__ */ new Map();
|
|
47255
|
+
const registerLocalName = (source, importedName, localIdentifier) => {
|
|
47256
|
+
const integration = INTEGRATION_IMPORTS.get(source);
|
|
47257
|
+
if (!integration) return;
|
|
47258
|
+
if (integration.importedNames === null ? importedName === null || importedName === "default" : importedName !== null && integration.importedNames.has(importedName)) {
|
|
47259
|
+
const symbol = scopes.symbolFor(localIdentifier);
|
|
47260
|
+
if (symbol) localBindings.set(symbol, integration);
|
|
47261
|
+
}
|
|
47262
|
+
};
|
|
47263
|
+
walkAst(program, (node) => {
|
|
47264
|
+
if (isNodeOfType(node, "ImportDeclaration")) {
|
|
47265
|
+
const source = node.source.value;
|
|
47266
|
+
if (typeof source !== "string") return;
|
|
47267
|
+
for (const specifier of node.specifiers) {
|
|
47268
|
+
if (isNodeOfType(specifier, "ImportDefaultSpecifier") || isNodeOfType(specifier, "ImportNamespaceSpecifier")) {
|
|
47269
|
+
registerLocalName(source, null, specifier.local);
|
|
47270
|
+
continue;
|
|
47271
|
+
}
|
|
47272
|
+
registerLocalName(source, getImportedName(specifier) ?? null, specifier.local);
|
|
47273
|
+
}
|
|
47274
|
+
return;
|
|
47275
|
+
}
|
|
47276
|
+
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return;
|
|
47277
|
+
const initializer = node.init;
|
|
47278
|
+
if (!isNodeOfType(initializer, "CallExpression") || !isNodeOfType(initializer.callee, "Identifier") || initializer.callee.name !== "require") return;
|
|
47279
|
+
const sourceArgument = initializer.arguments[0];
|
|
47280
|
+
if (!sourceArgument || !isNodeOfType(sourceArgument, "Literal")) return;
|
|
47281
|
+
const source = sourceArgument.value;
|
|
47282
|
+
if (typeof source !== "string") return;
|
|
47283
|
+
if (isNodeOfType(node.id, "Identifier")) {
|
|
47284
|
+
registerLocalName(source, null, node.id);
|
|
47285
|
+
return;
|
|
47286
|
+
}
|
|
47287
|
+
if (!isNodeOfType(node.id, "ObjectPattern")) return;
|
|
47288
|
+
for (const property of node.id.properties) {
|
|
47289
|
+
if (!isNodeOfType(property, "Property") || !isNodeOfType(property.value, "Identifier")) continue;
|
|
47290
|
+
registerLocalName(source, isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" ? property.key.value : null, property.value);
|
|
47291
|
+
}
|
|
47292
|
+
});
|
|
47293
|
+
return localBindings;
|
|
47294
|
+
};
|
|
47295
|
+
const isModuleExportsAssignment = (node) => isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && isNodeOfType(node.left.object, "Identifier") && node.left.object.name === "module" && isNodeOfType(node.left.property, "Identifier") && node.left.property.name === "exports";
|
|
47296
|
+
const getExportedBindings = (program, scopes) => {
|
|
47297
|
+
const exportedBindings = /* @__PURE__ */ new Set();
|
|
47298
|
+
walkAst(program, (node) => {
|
|
47299
|
+
let exportRoot = null;
|
|
47300
|
+
if (isNodeOfType(node, "ExportDefaultDeclaration")) exportRoot = node.declaration;
|
|
47301
|
+
else if (isNodeOfType(node, "AssignmentExpression") && isModuleExportsAssignment(node)) exportRoot = node.right;
|
|
47302
|
+
if (!exportRoot) return;
|
|
47303
|
+
if (isFunctionLike$1(exportRoot) || isNodeOfType(exportRoot, "ObjectExpression")) return;
|
|
47304
|
+
walkAst(exportRoot, (exportNode) => {
|
|
47305
|
+
if (isFunctionLike$1(exportNode) || isNodeOfType(exportNode, "ObjectExpression")) return false;
|
|
47306
|
+
if (!isNodeOfType(exportNode, "Identifier")) return;
|
|
47307
|
+
const symbol = scopes.symbolFor(exportNode);
|
|
47308
|
+
if (symbol) exportedBindings.add(symbol);
|
|
47309
|
+
});
|
|
47310
|
+
});
|
|
47311
|
+
return exportedBindings;
|
|
47312
|
+
};
|
|
47313
|
+
const isExportedConfigProperty = (property, exportedBindings, scopes) => {
|
|
47314
|
+
let ancestor = property.parent;
|
|
47315
|
+
let didFindContainingObject = false;
|
|
47316
|
+
let didCrossNestedProperty = false;
|
|
47317
|
+
let didCrossFunctionBoundary = false;
|
|
47318
|
+
let containingFunction = null;
|
|
47319
|
+
let containingReturn = null;
|
|
47320
|
+
while (ancestor) {
|
|
47321
|
+
if (isNodeOfType(ancestor, "ObjectExpression") && !didFindContainingObject) didFindContainingObject = true;
|
|
47322
|
+
else if (didFindContainingObject && isNodeOfType(ancestor, "Property")) didCrossNestedProperty = true;
|
|
47323
|
+
if (!containingReturn && isNodeOfType(ancestor, "ReturnStatement")) containingReturn = ancestor;
|
|
47324
|
+
if (isFunctionLike$1(ancestor)) if (containingFunction) didCrossFunctionBoundary = true;
|
|
47325
|
+
else containingFunction = ancestor;
|
|
47326
|
+
if (isNodeOfType(ancestor, "ExportDefaultDeclaration") || isModuleExportsAssignment(ancestor)) return !didCrossNestedProperty && !didCrossFunctionBoundary && (!containingFunction || Boolean(containingReturn) || !isNodeOfType(containingFunction.body, "BlockStatement"));
|
|
47327
|
+
if (isNodeOfType(ancestor, "VariableDeclarator") && isNodeOfType(ancestor.id, "Identifier")) {
|
|
47328
|
+
const binding = scopes.symbolFor(ancestor.id);
|
|
47329
|
+
if (binding && exportedBindings.has(binding)) return !didCrossNestedProperty && !containingFunction;
|
|
47330
|
+
}
|
|
47331
|
+
ancestor = ancestor.parent;
|
|
47332
|
+
}
|
|
47333
|
+
return false;
|
|
47334
|
+
};
|
|
47335
|
+
const getExplicitConfigPaths = (packageDirectory, manifest) => {
|
|
47336
|
+
const configPaths = new Set(FAST_REFRESH_CONFIG_FILENAMES.map((configFilename) => path.join(packageDirectory, configFilename)));
|
|
47337
|
+
for (const script of Object.values(manifest.scripts ?? {})) {
|
|
47338
|
+
if (typeof script !== "string") continue;
|
|
47339
|
+
for (const match of script.matchAll(/(?:^|\s)--config(?:=|\s+)(?:"([^"]+)"|'([^']+)'|([^\s;&|]+))/g)) {
|
|
47340
|
+
const relativeConfigPath = match[1] ?? match[2] ?? match[3];
|
|
47341
|
+
if (!relativeConfigPath) continue;
|
|
47342
|
+
const configPath = path.resolve(packageDirectory, relativeConfigPath);
|
|
47343
|
+
const relativePath = path.relative(packageDirectory, configPath);
|
|
47344
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) continue;
|
|
47345
|
+
configPaths.add(configPath);
|
|
47346
|
+
}
|
|
47347
|
+
}
|
|
47348
|
+
return [...configPaths];
|
|
47349
|
+
};
|
|
47350
|
+
const getRegisteredIntegration = (packageDirectory, manifest) => {
|
|
47351
|
+
for (const configPath of getExplicitConfigPaths(packageDirectory, manifest)) {
|
|
47352
|
+
const program = parseSourceFile(configPath);
|
|
47353
|
+
if (!program) continue;
|
|
47354
|
+
const scopes = analyzeScopes(program);
|
|
47355
|
+
const integrationLocalBindings = getIntegrationLocalNames(program, scopes);
|
|
47356
|
+
if (integrationLocalBindings.size === 0) continue;
|
|
47357
|
+
const exportedBindings = getExportedBindings(program, scopes);
|
|
47358
|
+
const registeredIntegrations = [];
|
|
47359
|
+
walkAst(program, (node) => {
|
|
47360
|
+
if (!isNodeOfType(node, "Property")) return;
|
|
47361
|
+
if (!(isNodeOfType(node.key, "Identifier") && node.key.name === "plugins" || isNodeOfType(node.key, "Literal") && node.key.value === "plugins")) return;
|
|
47362
|
+
if (!isExportedConfigProperty(node, exportedBindings, scopes)) return;
|
|
47363
|
+
const inspectedBindings = /* @__PURE__ */ new Set();
|
|
47364
|
+
const inspectValue = (value) => {
|
|
47365
|
+
walkAst(value, (valueNode) => {
|
|
47366
|
+
if (isNodeOfType(valueNode, "Identifier")) {
|
|
47367
|
+
const symbol = scopes.symbolFor(valueNode);
|
|
47368
|
+
const initializer = symbol ? getDirectUnreassignedInitializer(symbol) : null;
|
|
47369
|
+
if (symbol && initializer && !inspectedBindings.has(symbol.id)) {
|
|
47370
|
+
inspectedBindings.add(symbol.id);
|
|
47371
|
+
inspectValue(initializer);
|
|
47372
|
+
}
|
|
47373
|
+
}
|
|
47374
|
+
if (!isNodeOfType(valueNode, "CallExpression") && !isNodeOfType(valueNode, "NewExpression")) return;
|
|
47375
|
+
if (!isNodeOfType(valueNode.callee, "Identifier")) return;
|
|
47376
|
+
const symbol = scopes.symbolFor(valueNode.callee);
|
|
47377
|
+
const integration = symbol ? integrationLocalBindings.get(symbol) : null;
|
|
47378
|
+
if (integration) registeredIntegrations.push(integration);
|
|
47379
|
+
});
|
|
47380
|
+
};
|
|
47381
|
+
inspectValue(node.value);
|
|
47382
|
+
return false;
|
|
47383
|
+
});
|
|
47384
|
+
const hasViteDevelopmentRuntime = hasViteDevelopmentCommand(manifest) || hasViteBrowserEntry(manifest, packageDirectory);
|
|
47385
|
+
const registeredIntegration = REGISTERED_INTEGRATION_RUNTIME_PRECEDENCE.flatMap((runtime) => registeredIntegrations.filter((integration) => integration.runtime === runtime && (!integration.requiresViteDevelopmentServer || hasViteDevelopmentRuntime)))[0];
|
|
47386
|
+
if (registeredIntegration) return {
|
|
47387
|
+
isActive: true,
|
|
47388
|
+
runtime: registeredIntegration.runtime
|
|
47389
|
+
};
|
|
47390
|
+
}
|
|
47391
|
+
return null;
|
|
47392
|
+
};
|
|
47393
|
+
const hasStorybookReactViteConfig = (packageDirectory) => {
|
|
47394
|
+
for (const configFilename of [
|
|
47395
|
+
"main.ts",
|
|
47396
|
+
"main.js",
|
|
47397
|
+
"main.mjs",
|
|
47398
|
+
"main.cjs"
|
|
47399
|
+
]) {
|
|
47400
|
+
const program = parseSourceFile(path.join(packageDirectory, ".storybook", configFilename));
|
|
47401
|
+
if (!program) continue;
|
|
47402
|
+
const scopes = analyzeScopes(program);
|
|
47403
|
+
const exportedBindings = getExportedBindings(program, scopes);
|
|
47404
|
+
let hasReactViteFramework = false;
|
|
47405
|
+
walkAst(program, (node) => {
|
|
47406
|
+
if (!isNodeOfType(node, "Property")) return;
|
|
47407
|
+
if (!(isNodeOfType(node.key, "Identifier") && node.key.name === "framework" || isNodeOfType(node.key, "Literal") && node.key.value === "framework")) return;
|
|
47408
|
+
if (!isExportedConfigProperty(node, exportedBindings, scopes)) return;
|
|
47409
|
+
walkAst(node.value, (valueNode) => {
|
|
47410
|
+
if (isNodeOfType(valueNode, "Literal") && valueNode.value === "@storybook/react-vite") {
|
|
47411
|
+
hasReactViteFramework = true;
|
|
47412
|
+
return false;
|
|
47413
|
+
}
|
|
47414
|
+
});
|
|
47415
|
+
if (hasReactViteFramework) return false;
|
|
47416
|
+
});
|
|
47417
|
+
if (hasReactViteFramework) return true;
|
|
47418
|
+
}
|
|
47419
|
+
return false;
|
|
47420
|
+
};
|
|
47421
|
+
const resolveDirectUnreassignedValue = (value, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
47422
|
+
const unwrappedValue = stripParenExpression(value);
|
|
47423
|
+
if (!isNodeOfType(unwrappedValue, "Identifier")) return unwrappedValue;
|
|
47424
|
+
const symbol = scopes.symbolFor(unwrappedValue);
|
|
47425
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return unwrappedValue;
|
|
47426
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
47427
|
+
if (!initializer) return unwrappedValue;
|
|
47428
|
+
visitedSymbolIds.add(symbol.id);
|
|
47429
|
+
return resolveDirectUnreassignedValue(initializer, scopes, visitedSymbolIds);
|
|
47430
|
+
};
|
|
47431
|
+
const isStaticPropertyNamed = (node, propertyName) => isNodeOfType(node, "Property") && (!node.computed && isNodeOfType(node.key, "Identifier") && node.key.name === propertyName || isNodeOfType(node.key, "Literal") && node.key.value === propertyName);
|
|
47432
|
+
const readLastStaticBooleanProperty = (value, propertyName, scopes) => {
|
|
47433
|
+
const resolvedValue = resolveDirectUnreassignedValue(value, scopes);
|
|
47434
|
+
if (!isNodeOfType(resolvedValue, "ObjectExpression")) return null;
|
|
47435
|
+
let propertyValue = null;
|
|
47436
|
+
for (const property of resolvedValue.properties) {
|
|
47437
|
+
if (isNodeOfType(property, "SpreadElement")) {
|
|
47438
|
+
propertyValue = null;
|
|
47439
|
+
continue;
|
|
47440
|
+
}
|
|
47441
|
+
if (!isStaticPropertyNamed(property, propertyName) || !isNodeOfType(property, "Property")) continue;
|
|
47442
|
+
propertyValue = readStaticBoolean(resolveDirectUnreassignedValue(property.value, scopes));
|
|
47443
|
+
}
|
|
47444
|
+
return propertyValue;
|
|
47445
|
+
};
|
|
47446
|
+
const isLastStaticPropertyInObject = (property, propertyName) => {
|
|
47447
|
+
if (!isNodeOfType(property.parent, "ObjectExpression")) return false;
|
|
47448
|
+
const propertyIndex = property.parent.properties.findIndex((objectProperty) => objectProperty === property);
|
|
47449
|
+
return property.parent.properties.slice(propertyIndex + 1).every((laterProperty) => {
|
|
47450
|
+
if (isNodeOfType(laterProperty, "SpreadElement")) return false;
|
|
47451
|
+
return !isStaticPropertyNamed(laterProperty, propertyName);
|
|
47452
|
+
});
|
|
47453
|
+
};
|
|
47454
|
+
const hasStorybookReactWebpackFastRefreshConfig = (packageDirectory) => {
|
|
47455
|
+
for (const configFilename of [
|
|
47456
|
+
"main.ts",
|
|
47457
|
+
"main.js",
|
|
47458
|
+
"main.mjs",
|
|
47459
|
+
"main.cjs"
|
|
47460
|
+
]) {
|
|
47461
|
+
const program = parseSourceFile(path.join(packageDirectory, ".storybook", configFilename));
|
|
47462
|
+
if (!program) continue;
|
|
47463
|
+
const scopes = analyzeScopes(program);
|
|
47464
|
+
const exportedBindings = getExportedBindings(program, scopes);
|
|
47465
|
+
let hasFastRefresh = false;
|
|
47466
|
+
walkAst(program, (node) => {
|
|
47467
|
+
if (!isStaticPropertyNamed(node, "reactOptions") || !isNodeOfType(node, "Property")) return;
|
|
47468
|
+
if (!isExportedConfigProperty(node, exportedBindings, scopes)) return;
|
|
47469
|
+
if (!isLastStaticPropertyInObject(node, "reactOptions")) return;
|
|
47470
|
+
if (readLastStaticBooleanProperty(node.value, "fastRefresh", scopes) === true) {
|
|
47471
|
+
hasFastRefresh = true;
|
|
47472
|
+
return false;
|
|
47473
|
+
}
|
|
47474
|
+
});
|
|
47475
|
+
if (hasFastRefresh) return true;
|
|
47476
|
+
}
|
|
47477
|
+
return false;
|
|
47478
|
+
};
|
|
47479
|
+
const hasStorybookReactViteIntegration = (packageDirectory, manifest) => hasOwnedDevelopmentCommand(manifest, /(?:^|\s)(?:storybook\s+dev|start-storybook)(?:\s|$)/) && hasStorybookReactViteConfig(packageDirectory);
|
|
47480
|
+
const hasStorybookReactWebpackFastRefreshIntegration = (packageDirectory, manifest) => {
|
|
47481
|
+
const developmentCommandPattern = /(?:^|\s)(?:storybook\s+dev|start-storybook)(?:\s|$)/;
|
|
47482
|
+
if (!hasOwnedDevelopmentCommand(manifest, developmentCommandPattern)) return false;
|
|
47483
|
+
return isVersionAtLeast(getOwnedDependencyVersion(manifest, "@storybook/react", developmentCommandPattern) ?? getOwnedDependencyVersion(manifest, "@storybook/react-webpack5", developmentCommandPattern), MINIMUM_FAST_REFRESH_VERSIONS.storybookReact) && hasStorybookReactWebpackFastRefreshConfig(packageDirectory);
|
|
47484
|
+
};
|
|
47485
|
+
const hasNxStorybookReactViteIntegration = (packageDirectory) => {
|
|
47486
|
+
if (!hasStorybookReactViteConfig(packageDirectory)) return false;
|
|
47487
|
+
try {
|
|
47488
|
+
const project = JSON.parse(fs.readFileSync(path.join(packageDirectory, "project.json"), "utf8"));
|
|
47489
|
+
if (typeof project !== "object" || project === null) return false;
|
|
47490
|
+
const targets = Reflect.get(project, "targets");
|
|
47491
|
+
if (typeof targets !== "object" || targets === null) return false;
|
|
47492
|
+
return Object.keys(targets).some((targetName) => targetName === "storybook:serve:dev");
|
|
47493
|
+
} catch {
|
|
47494
|
+
return false;
|
|
47495
|
+
}
|
|
47496
|
+
};
|
|
47497
|
+
const getLocalFastRefreshStatus = (packageDirectory, manifest) => {
|
|
47498
|
+
const cachedStatus = cachedLocalStatusByManifest.get(manifest);
|
|
47499
|
+
if (cachedStatus) return cachedStatus;
|
|
47500
|
+
const status = getBuiltInStatus(manifest) ?? (hasStorybookReactViteIntegration(packageDirectory, manifest) || hasNxStorybookReactViteIntegration(packageDirectory) || hasStorybookReactWebpackFastRefreshIntegration(packageDirectory, manifest) ? {
|
|
47501
|
+
isActive: true,
|
|
47502
|
+
runtime: "generic"
|
|
47503
|
+
} : null) ?? getRegisteredIntegration(packageDirectory, manifest) ?? INACTIVE_STATUS;
|
|
47504
|
+
cachedLocalStatusByManifest.set(manifest, status);
|
|
47505
|
+
return status;
|
|
47506
|
+
};
|
|
47507
|
+
const isWorkspaceRoot = (directory, manifest) => {
|
|
47508
|
+
if (manifest?.workspaces !== void 0) return true;
|
|
47509
|
+
return [
|
|
47510
|
+
"pnpm-workspace.yaml",
|
|
47511
|
+
"pnpm-workspace.yml",
|
|
47512
|
+
"nx.json"
|
|
47513
|
+
].some((filename) => fs.existsSync(path.join(directory, filename)));
|
|
47514
|
+
};
|
|
47515
|
+
const findWorkspaceRoot = (packageDirectory) => {
|
|
47516
|
+
let currentDirectory = packageDirectory;
|
|
47517
|
+
let workspaceRoot = null;
|
|
47518
|
+
while (true) {
|
|
47519
|
+
const manifest = readPackageManifest(currentDirectory);
|
|
47520
|
+
if (isWorkspaceRoot(currentDirectory, manifest)) workspaceRoot = currentDirectory;
|
|
47521
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
47522
|
+
if (parentDirectory === currentDirectory) return workspaceRoot;
|
|
47523
|
+
currentDirectory = parentDirectory;
|
|
47524
|
+
}
|
|
47525
|
+
};
|
|
47526
|
+
const collectWorkspacePackages = (workspaceRoot) => {
|
|
47527
|
+
const packages = [];
|
|
47528
|
+
const pendingDirectories = [workspaceRoot];
|
|
47529
|
+
while (pendingDirectories.length > 0) {
|
|
47530
|
+
const directory = pendingDirectories.pop();
|
|
47531
|
+
if (!directory) continue;
|
|
47532
|
+
const manifest = readPackageManifest(directory);
|
|
47533
|
+
if (manifest) packages.push({
|
|
47534
|
+
directory,
|
|
47535
|
+
manifest,
|
|
47536
|
+
status: getLocalFastRefreshStatus(directory, manifest)
|
|
47537
|
+
});
|
|
47538
|
+
let entries;
|
|
47539
|
+
try {
|
|
47540
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
47541
|
+
} catch {
|
|
47542
|
+
continue;
|
|
47543
|
+
}
|
|
47544
|
+
for (const entry of entries) {
|
|
47545
|
+
if (!entry.isDirectory()) continue;
|
|
47546
|
+
if (entry.name.startsWith(".") || WORKSPACE_IGNORED_DIRECTORY_NAMES.has(entry.name)) continue;
|
|
47547
|
+
pendingDirectories.push(path.join(directory, entry.name));
|
|
47548
|
+
}
|
|
47549
|
+
}
|
|
47550
|
+
return packages;
|
|
47551
|
+
};
|
|
47552
|
+
const isPropertyNamed = (node, name) => isNodeOfType(node, "Property") && (isNodeOfType(node.key, "Identifier") && node.key.name === name || isNodeOfType(node.key, "Literal") && node.key.value === name);
|
|
47553
|
+
const normalizeAliasRoot = (configDirectory, aliasPath) => {
|
|
47554
|
+
if (!aliasPath.startsWith(".") && !path.isAbsolute(aliasPath)) return null;
|
|
47555
|
+
const withoutPlaceholder = aliasPath.replace(/(?:\/)?(?:\*|\$\d+).*$/, "");
|
|
47556
|
+
return path.resolve(configDirectory, withoutPlaceholder);
|
|
47557
|
+
};
|
|
47558
|
+
const collectAliasRootsFromValue = (value, configDirectory, aliasRoots) => {
|
|
47559
|
+
walkAst(value, (node) => {
|
|
47560
|
+
if (!isPropertyNamed(node, "alias")) return;
|
|
47561
|
+
if (!isNodeOfType(node, "Property")) return;
|
|
47562
|
+
walkAst(node.value, (aliasNode) => {
|
|
47563
|
+
if (!isNodeOfType(aliasNode, "Literal") || typeof aliasNode.value !== "string") return;
|
|
47564
|
+
const aliasRoot = normalizeAliasRoot(configDirectory, aliasNode.value);
|
|
47565
|
+
if (aliasRoot) aliasRoots.add(aliasRoot);
|
|
47566
|
+
});
|
|
47567
|
+
return false;
|
|
47568
|
+
});
|
|
47569
|
+
};
|
|
47570
|
+
const getActivePackageAliasRoots = (packageDirectory, manifest) => {
|
|
47571
|
+
const aliasRoots = /* @__PURE__ */ new Set();
|
|
47572
|
+
const configPaths = [...getExplicitConfigPaths(packageDirectory, manifest), ...[
|
|
47573
|
+
"main.ts",
|
|
47574
|
+
"main.js",
|
|
47575
|
+
"main.mjs",
|
|
47576
|
+
"main.cjs"
|
|
47577
|
+
].map((filename) => path.join(packageDirectory, ".storybook", filename))];
|
|
47578
|
+
for (const configPath of configPaths) {
|
|
47579
|
+
const program = parseSourceFile(configPath);
|
|
47580
|
+
if (!program) continue;
|
|
47581
|
+
const scopes = analyzeScopes(program);
|
|
47582
|
+
const exportedBindings = getExportedBindings(program, scopes);
|
|
47583
|
+
walkAst(program, (node) => {
|
|
47584
|
+
if (!isPropertyNamed(node, "resolve") && !isPropertyNamed(node, "viteFinal")) return;
|
|
47585
|
+
if (!isNodeOfType(node, "Property")) return;
|
|
47586
|
+
if (!isExportedConfigProperty(node, exportedBindings, scopes)) return;
|
|
47587
|
+
collectAliasRootsFromValue(node.value, path.dirname(configPath), aliasRoots);
|
|
47588
|
+
return false;
|
|
47589
|
+
});
|
|
47590
|
+
}
|
|
47591
|
+
return aliasRoots;
|
|
47592
|
+
};
|
|
47593
|
+
const collectStringValues = (value, values) => {
|
|
47594
|
+
if (typeof value === "string") {
|
|
47595
|
+
values.push(value);
|
|
47596
|
+
return;
|
|
47597
|
+
}
|
|
47598
|
+
if (Array.isArray(value)) {
|
|
47599
|
+
for (const entry of value) collectStringValues(entry, values);
|
|
47600
|
+
return;
|
|
47601
|
+
}
|
|
47602
|
+
if (typeof value !== "object" || value === null) return;
|
|
47603
|
+
for (const entry of Object.values(value)) collectStringValues(entry, values);
|
|
47604
|
+
};
|
|
47605
|
+
const hasSourceRuntimeEntry = (manifest) => {
|
|
47606
|
+
const entryValues = [];
|
|
47607
|
+
collectStringValues(manifest.exports, entryValues);
|
|
47608
|
+
collectStringValues(manifest.main, entryValues);
|
|
47609
|
+
collectStringValues(manifest.module, entryValues);
|
|
47610
|
+
return entryValues.some((entry) => /^\.\/?src(?:\/|\.|$)/.test(entry));
|
|
47611
|
+
};
|
|
47612
|
+
const getWorkspaceDependencyVersion = (manifest, dependencyName) => manifest.dependencies?.[dependencyName] ?? manifest.optionalDependencies?.[dependencyName];
|
|
47613
|
+
const buildWorkspaceFastRefreshIndex = (workspaceRoot, rootManifest) => {
|
|
47614
|
+
const cached = cachedWorkspaceIndexByManifest.get(rootManifest);
|
|
47615
|
+
if (cached) return cached;
|
|
47616
|
+
const workspacePackages = collectWorkspacePackages(workspaceRoot);
|
|
47617
|
+
const activePackages = workspacePackages.filter((workspacePackage) => workspacePackage.status.isActive);
|
|
47618
|
+
const aliasOwners = [];
|
|
47619
|
+
for (const activePackage of activePackages) for (const rootDirectory of getActivePackageAliasRoots(activePackage.directory, activePackage.manifest)) aliasOwners.push({
|
|
47620
|
+
rootDirectory,
|
|
47621
|
+
status: activePackage.status
|
|
47622
|
+
});
|
|
47623
|
+
const sourceEntryOwners = /* @__PURE__ */ new Map();
|
|
47624
|
+
for (const producerPackage of workspacePackages) {
|
|
47625
|
+
const packageName = producerPackage.manifest.name;
|
|
47626
|
+
if (typeof packageName !== "string" || !hasSourceRuntimeEntry(producerPackage.manifest)) continue;
|
|
47627
|
+
const owner = activePackages.find((activePackage) => {
|
|
47628
|
+
const dependencyVersion = getWorkspaceDependencyVersion(activePackage.manifest, packageName);
|
|
47629
|
+
return typeof dependencyVersion === "string" && dependencyVersion.startsWith("workspace:");
|
|
47630
|
+
});
|
|
47631
|
+
if (owner) sourceEntryOwners.set(producerPackage.directory, owner.status);
|
|
47632
|
+
}
|
|
47633
|
+
const index = {
|
|
47634
|
+
aliasOwners,
|
|
47635
|
+
sourceEntryOwners
|
|
47636
|
+
};
|
|
47637
|
+
cachedWorkspaceIndexByManifest.set(rootManifest, index);
|
|
47638
|
+
return index;
|
|
47639
|
+
};
|
|
47640
|
+
const isPathInside = (filePath, directory) => {
|
|
47641
|
+
const relativePath = path.relative(directory, filePath);
|
|
47642
|
+
return relativePath === "" || !relativePath.startsWith(`..${path.sep}`) && relativePath !== ".." && !path.isAbsolute(relativePath);
|
|
47643
|
+
};
|
|
47644
|
+
const getWorkspaceOwnedStatus = (filename, packageDirectory) => {
|
|
47645
|
+
const workspaceRoot = findWorkspaceRoot(packageDirectory);
|
|
47646
|
+
if (!workspaceRoot) return null;
|
|
47647
|
+
const rootManifest = readPackageManifest(workspaceRoot);
|
|
47648
|
+
if (!rootManifest) return null;
|
|
47649
|
+
const index = buildWorkspaceFastRefreshIndex(workspaceRoot, rootManifest);
|
|
47650
|
+
const aliasOwner = index.aliasOwners.find((owner) => isPathInside(filename, owner.rootDirectory));
|
|
47651
|
+
if (aliasOwner) return aliasOwner.status;
|
|
47652
|
+
for (const [producerDirectory, status] of index.sourceEntryOwners) if (isPathInside(filename, producerDirectory)) return status;
|
|
47653
|
+
return null;
|
|
47654
|
+
};
|
|
47655
|
+
const resolveFastRefreshFileStatus = (filename) => {
|
|
47656
|
+
const manifest = readNearestPackageManifest(filename);
|
|
47657
|
+
if (!manifest) return INACTIVE_STATUS;
|
|
47658
|
+
const packageDirectory = findNearestPackageDirectory(filename);
|
|
47659
|
+
if (!packageDirectory) return INACTIVE_STATUS;
|
|
47660
|
+
const localStatus = getLocalFastRefreshStatus(packageDirectory, manifest);
|
|
47661
|
+
if (localStatus.isActive) return localStatus;
|
|
47662
|
+
return getWorkspaceOwnedStatus(filename, packageDirectory) ?? INACTIVE_STATUS;
|
|
47663
|
+
};
|
|
47664
|
+
const getConfiguredFallbackStatus = (context) => {
|
|
47665
|
+
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
47666
|
+
return {
|
|
47667
|
+
isActive: true,
|
|
47668
|
+
runtime: framework === "nextjs" ? "next" : framework === "expo" || framework === "react-native" ? "expo" : framework === "remix" ? "remix" : framework === "tanstack-start" ? "tanstack" : "generic"
|
|
47669
|
+
};
|
|
47670
|
+
};
|
|
47671
|
+
const getFastRefreshFileStatus = (context) => {
|
|
47672
|
+
let filename = context.filename;
|
|
47673
|
+
if (!filename) return getConfiguredFallbackStatus(context);
|
|
47674
|
+
if (!path.isAbsolute(filename)) {
|
|
47675
|
+
const absoluteFilename = path.resolve(filename);
|
|
47676
|
+
if (!fs.existsSync(absoluteFilename)) return getConfiguredFallbackStatus(context);
|
|
47677
|
+
filename = absoluteFilename;
|
|
47678
|
+
}
|
|
47679
|
+
if (!readNearestPackageManifest(filename)) return INACTIVE_STATUS;
|
|
47680
|
+
return resolveFastRefreshFileStatus(filename);
|
|
45760
47681
|
};
|
|
45761
47682
|
//#endregion
|
|
45762
47683
|
//#region src/plugin/rules/react-builtins/only-export-components-tables.ts
|
|
@@ -45768,6 +47689,7 @@ const NOT_REACT_COMPONENT_EXPRESSION_TYPES = new Set([
|
|
|
45768
47689
|
"ConditionalExpression",
|
|
45769
47690
|
"Literal",
|
|
45770
47691
|
"LogicalExpression",
|
|
47692
|
+
"NewExpression",
|
|
45771
47693
|
"ObjectExpression",
|
|
45772
47694
|
"TemplateLiteral",
|
|
45773
47695
|
"ThisExpression",
|
|
@@ -45786,124 +47708,25 @@ const NON_FAST_REFRESH_PATH_SEGMENTS = [
|
|
|
45786
47708
|
"/cypress/",
|
|
45787
47709
|
"/.storybook/",
|
|
45788
47710
|
"/stories/",
|
|
45789
|
-
"/__stories__/"
|
|
45790
|
-
"/playground/",
|
|
45791
|
-
"/playgrounds/",
|
|
45792
|
-
"/examples/",
|
|
45793
|
-
"/example/",
|
|
45794
|
-
"/demo/",
|
|
45795
|
-
"/demos/",
|
|
45796
|
-
"/sandbox/",
|
|
45797
|
-
"/sandboxes/"
|
|
47711
|
+
"/__stories__/"
|
|
45798
47712
|
];
|
|
45799
|
-
const
|
|
45800
|
-
"main.tsx",
|
|
45801
|
-
"main.jsx",
|
|
45802
|
-
"main.js",
|
|
45803
|
-
"index.tsx",
|
|
45804
|
-
"index.jsx",
|
|
45805
|
-
"entry.tsx",
|
|
45806
|
-
"entry.jsx",
|
|
45807
|
-
"bootstrap.tsx",
|
|
45808
|
-
"bootstrap.jsx",
|
|
45809
|
-
"client.tsx",
|
|
45810
|
-
"client.jsx",
|
|
45811
|
-
"server.tsx",
|
|
45812
|
-
"server.jsx",
|
|
45813
|
-
"app.tsx",
|
|
45814
|
-
"app.jsx",
|
|
45815
|
-
"App.tsx",
|
|
45816
|
-
"App.jsx"
|
|
45817
|
-
]);
|
|
45818
|
-
const UTILITY_FILE_BASENAMES = new Set([
|
|
45819
|
-
"utils.tsx",
|
|
45820
|
-
"utils.jsx",
|
|
45821
|
-
"util.tsx",
|
|
45822
|
-
"util.jsx",
|
|
45823
|
-
"helpers.tsx",
|
|
45824
|
-
"helpers.jsx",
|
|
45825
|
-
"helper.tsx",
|
|
45826
|
-
"helper.jsx",
|
|
45827
|
-
"shared.tsx",
|
|
45828
|
-
"shared.jsx",
|
|
45829
|
-
"common.tsx",
|
|
45830
|
-
"common.jsx",
|
|
45831
|
-
"lib.tsx",
|
|
45832
|
-
"lib.jsx",
|
|
45833
|
-
"nodeTypes.tsx",
|
|
45834
|
-
"nodeTypes.jsx",
|
|
45835
|
-
"node-types.tsx",
|
|
45836
|
-
"node-types.jsx",
|
|
45837
|
-
"edgeTypes.tsx",
|
|
45838
|
-
"edgeTypes.jsx",
|
|
45839
|
-
"edge-types.tsx",
|
|
45840
|
-
"edge-types.jsx",
|
|
45841
|
-
"cellTypes.tsx",
|
|
45842
|
-
"cellTypes.jsx",
|
|
45843
|
-
"columnTypes.tsx",
|
|
45844
|
-
"columnTypes.jsx",
|
|
45845
|
-
"columnDefs.tsx",
|
|
45846
|
-
"columnDefs.jsx",
|
|
45847
|
-
"columnRenderers.tsx",
|
|
45848
|
-
"columnRenderers.jsx",
|
|
45849
|
-
"columns.tsx",
|
|
45850
|
-
"columns.jsx",
|
|
45851
|
-
"mappings.tsx",
|
|
45852
|
-
"mappings.jsx",
|
|
45853
|
-
"mapping.tsx",
|
|
45854
|
-
"mapping.jsx",
|
|
45855
|
-
"lookups.tsx",
|
|
45856
|
-
"lookups.jsx",
|
|
45857
|
-
"lookup.tsx",
|
|
45858
|
-
"lookup.jsx",
|
|
45859
|
-
"registry.tsx",
|
|
45860
|
-
"registry.jsx",
|
|
45861
|
-
"toast.tsx",
|
|
45862
|
-
"toast.jsx",
|
|
45863
|
-
"toaster.tsx",
|
|
45864
|
-
"toaster.jsx",
|
|
45865
|
-
"theme.tsx",
|
|
45866
|
-
"theme.jsx",
|
|
45867
|
-
"tokens.tsx",
|
|
45868
|
-
"tokens.jsx",
|
|
45869
|
-
"palette.tsx",
|
|
45870
|
-
"palette.jsx",
|
|
45871
|
-
"colors.tsx",
|
|
45872
|
-
"colors.jsx",
|
|
45873
|
-
"colours.tsx",
|
|
45874
|
-
"colours.jsx",
|
|
45875
|
-
"constants.tsx",
|
|
45876
|
-
"constants.jsx",
|
|
45877
|
-
"enums.tsx",
|
|
45878
|
-
"enums.jsx",
|
|
45879
|
-
"types.tsx",
|
|
45880
|
-
"types.jsx",
|
|
45881
|
-
"schemas.tsx",
|
|
45882
|
-
"schemas.jsx",
|
|
45883
|
-
"schema.tsx",
|
|
45884
|
-
"schema.jsx",
|
|
45885
|
-
"definitions.tsx",
|
|
45886
|
-
"definitions.jsx",
|
|
45887
|
-
"config.tsx",
|
|
45888
|
-
"config.jsx",
|
|
45889
|
-
"defaults.tsx",
|
|
45890
|
-
"defaults.jsx"
|
|
45891
|
-
]);
|
|
45892
|
-
const ROUTE_FACTORY_CALLEE_NAMES = new Set([
|
|
47713
|
+
const TANSTACK_ROUTE_FACTORY_CALLEE_NAMES = new Set([
|
|
45893
47714
|
...TANSTACK_ROUTE_CREATION_FUNCTIONS,
|
|
45894
47715
|
"createLazyFileRoute",
|
|
45895
47716
|
"createLazyRoute",
|
|
45896
47717
|
"createAPIFileRoute",
|
|
45897
47718
|
"createServerFileRoute",
|
|
45898
47719
|
"createServerRootRoute",
|
|
45899
|
-
"createServerRoute"
|
|
47720
|
+
"createServerRoute"
|
|
47721
|
+
]);
|
|
47722
|
+
const REACT_ROUTER_FACTORY_CALLEE_NAMES = new Set([
|
|
45900
47723
|
"createBrowserRouter",
|
|
45901
47724
|
"createHashRouter",
|
|
45902
47725
|
"createMemoryRouter",
|
|
45903
47726
|
"createStaticRouter",
|
|
45904
47727
|
"createRouter"
|
|
45905
47728
|
]);
|
|
45906
|
-
const
|
|
47729
|
+
const REACT_ROUTER_ALLOWED_EXPORT_NAMES = new Set([
|
|
45907
47730
|
"loader",
|
|
45908
47731
|
"clientLoader",
|
|
45909
47732
|
"action",
|
|
@@ -45914,7 +47737,9 @@ const ROUTE_MODULE_ALLOWED_EXPORT_NAMES = new Set([
|
|
|
45914
47737
|
"handle",
|
|
45915
47738
|
"shouldRevalidate",
|
|
45916
47739
|
"middleware",
|
|
45917
|
-
"unstable_middleware"
|
|
47740
|
+
"unstable_middleware"
|
|
47741
|
+
]);
|
|
47742
|
+
const NEXT_ALLOWED_EXPORT_NAMES = new Set([
|
|
45918
47743
|
"getServerSideProps",
|
|
45919
47744
|
"getStaticProps",
|
|
45920
47745
|
"getStaticPaths",
|
|
@@ -45934,9 +47759,9 @@ const ROUTE_MODULE_ALLOWED_EXPORT_NAMES = new Set([
|
|
|
45934
47759
|
"runtime",
|
|
45935
47760
|
"preferredRegion",
|
|
45936
47761
|
"maxDuration",
|
|
45937
|
-
"experimental_ppr"
|
|
45938
|
-
"unstable_settings"
|
|
47762
|
+
"experimental_ppr"
|
|
45939
47763
|
]);
|
|
47764
|
+
const EXPO_ALLOWED_EXPORT_NAMES = new Set(["unstable_settings"]);
|
|
45940
47765
|
//#endregion
|
|
45941
47766
|
//#region src/plugin/rules/react-builtins/only-export-components.ts
|
|
45942
47767
|
const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh can't safely preserve component state.";
|
|
@@ -45949,6 +47774,8 @@ const DEFAULT_REACT_HOCS = [
|
|
|
45949
47774
|
"forwardRef",
|
|
45950
47775
|
"lazy"
|
|
45951
47776
|
];
|
|
47777
|
+
const EMPTY_NAME_SET = /* @__PURE__ */ new Set();
|
|
47778
|
+
const TEST_SUPPORT_FILE_PATTERN = /(?:^|\/)(?:test|spec)(?:[-_.]?(?:utils?|helpers?|setup|fixtures?))?\.(?:jsx?|tsx?)$/i;
|
|
45952
47779
|
const resolveSettings$6 = (settings) => {
|
|
45953
47780
|
const reactDoctor = settings?.["react-doctor"];
|
|
45954
47781
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.onlyExportComponents ?? {} : {};
|
|
@@ -45972,13 +47799,12 @@ const isReactCreateContext = (initializer) => {
|
|
|
45972
47799
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "createContext") return true;
|
|
45973
47800
|
return false;
|
|
45974
47801
|
};
|
|
45975
|
-
const
|
|
45976
|
-
const isRouteFactoryCall = (expression) => {
|
|
47802
|
+
const isRouteFactoryCall = (expression, bindings) => {
|
|
45977
47803
|
let currentCall = expression;
|
|
45978
47804
|
while (isNodeOfType(currentCall, "CallExpression")) {
|
|
45979
47805
|
const callee = currentCall.callee;
|
|
45980
|
-
if (isNodeOfType(callee, "Identifier") &&
|
|
45981
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") &&
|
|
47806
|
+
if (isNodeOfType(callee, "Identifier") && bindings.localNames.has(callee.name)) return true;
|
|
47807
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && bindings.namespaceNames.has(callee.object.name) && isNodeOfType(callee.property, "Identifier") && bindings.memberNames.has(callee.property.name)) return true;
|
|
45982
47808
|
if (!isNodeOfType(callee, "CallExpression")) return false;
|
|
45983
47809
|
currentCall = callee;
|
|
45984
47810
|
}
|
|
@@ -46006,10 +47832,11 @@ const isHocCallee = (callee, state) => {
|
|
|
46006
47832
|
const canBeReactFunctionComponent = (initializer, state) => {
|
|
46007
47833
|
if (!initializer) return false;
|
|
46008
47834
|
const expression = skipTsExpression(initializer);
|
|
46009
|
-
if (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression")) return
|
|
47835
|
+
if (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression")) return functionHasReactRenderSemantics(expression, state);
|
|
46010
47836
|
if (isNodeOfType(expression, "CallExpression")) return isHocCallee(expression.callee, state);
|
|
46011
47837
|
return false;
|
|
46012
47838
|
};
|
|
47839
|
+
const functionHasReactRenderSemantics = (functionNode, state) => functionContainsReactRenderOutput(functionNode, state.scopes, state.controlFlow) || functionHasReactElementReturnType(functionNode) || functionReturnsOnlyNull(functionNode);
|
|
46013
47840
|
const isReactComponentInitializer = (expression, state) => {
|
|
46014
47841
|
const stripped = skipTsExpression(expression);
|
|
46015
47842
|
if (isNodeOfType(stripped, "ArrowFunctionExpression")) return true;
|
|
@@ -46018,6 +47845,60 @@ const isReactComponentInitializer = (expression, state) => {
|
|
|
46018
47845
|
if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
|
|
46019
47846
|
return false;
|
|
46020
47847
|
};
|
|
47848
|
+
const isNextDynamicCall = (expression, nextDynamicImportSymbolIds, scopes) => {
|
|
47849
|
+
const stripped = skipTsExpression(expression);
|
|
47850
|
+
if (!isNodeOfType(stripped, "CallExpression")) return false;
|
|
47851
|
+
const callee = skipTsExpression(stripped.callee);
|
|
47852
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
47853
|
+
const symbol = scopes.symbolFor(callee);
|
|
47854
|
+
return symbol !== null && nextDynamicImportSymbolIds.has(symbol.id);
|
|
47855
|
+
};
|
|
47856
|
+
const functionReturnsNextDynamicComponent = (expression, nextDynamicImportSymbolIds, scopes) => {
|
|
47857
|
+
const stripped = skipTsExpression(expression);
|
|
47858
|
+
if (!isNodeOfType(stripped, "ArrowFunctionExpression") && !isNodeOfType(stripped, "FunctionExpression") && !isNodeOfType(stripped, "FunctionDeclaration")) return false;
|
|
47859
|
+
const body = stripped.body;
|
|
47860
|
+
if (!isNodeOfType(body, "BlockStatement")) return isNextDynamicCall(body, nextDynamicImportSymbolIds, scopes);
|
|
47861
|
+
if (body.body.length !== 1) return false;
|
|
47862
|
+
const statement = body.body[0];
|
|
47863
|
+
return Boolean(statement) && isNodeOfType(statement, "ReturnStatement") && Boolean(statement.argument) && isNextDynamicCall(statement.argument, nextDynamicImportSymbolIds, scopes);
|
|
47864
|
+
};
|
|
47865
|
+
const isComponentFactoryCall = (expression, state) => {
|
|
47866
|
+
const stripped = skipTsExpression(expression);
|
|
47867
|
+
if (!isNodeOfType(stripped, "CallExpression")) return false;
|
|
47868
|
+
const callee = skipTsExpression(stripped.callee);
|
|
47869
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
47870
|
+
const symbol = state.scopes.symbolFor(callee);
|
|
47871
|
+
return symbol !== null && state.componentFactorySymbolIds.has(symbol.id);
|
|
47872
|
+
};
|
|
47873
|
+
const isProvenComponentValue = (expression, state, inspectedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
47874
|
+
const stripped = skipTsExpression(expression);
|
|
47875
|
+
if (isNodeOfType(stripped, "Identifier")) {
|
|
47876
|
+
const symbol = state.scopes.symbolFor(stripped);
|
|
47877
|
+
if (!symbol) return false;
|
|
47878
|
+
if (state.localComponentNames.has(stripped.name)) return symbol.references.every((reference) => reference.flag === "read");
|
|
47879
|
+
if (isReactComponentName(stripped.name) && symbol.kind === "import") return true;
|
|
47880
|
+
if (inspectedSymbolIds.has(symbol.id)) return false;
|
|
47881
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
47882
|
+
if (!initializer) return false;
|
|
47883
|
+
const strippedInitializer = skipTsExpression(initializer);
|
|
47884
|
+
if (isNodeOfType(strippedInitializer, "ArrowFunctionExpression") || isNodeOfType(strippedInitializer, "FunctionExpression")) return false;
|
|
47885
|
+
return isProvenComponentValue(initializer, state, new Set([...inspectedSymbolIds, symbol.id]));
|
|
47886
|
+
}
|
|
47887
|
+
if (isNodeOfType(stripped, "MemberExpression") && !stripped.computed && isNodeOfType(stripped.object, "Identifier") && isNodeOfType(stripped.property, "Identifier") && isReactComponentName(stripped.property.name)) {
|
|
47888
|
+
const objectSymbol = state.scopes.symbolFor(stripped.object);
|
|
47889
|
+
return objectSymbol !== null && state.importSymbolIds.has(objectSymbol.id);
|
|
47890
|
+
}
|
|
47891
|
+
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "FunctionExpression")) return functionHasReactRenderSemantics(stripped, state);
|
|
47892
|
+
if (!isNodeOfType(stripped, "CallExpression")) return false;
|
|
47893
|
+
if (!isHocCallee(stripped.callee, state)) return false;
|
|
47894
|
+
return stripped.arguments.some((argument) => isProvenComponentValue(argument, state));
|
|
47895
|
+
};
|
|
47896
|
+
const isDirectRefreshWrapperCall = (call, state) => {
|
|
47897
|
+
const callee = skipTsExpression(call.callee);
|
|
47898
|
+
if (isHocCallee(callee, state)) return call.arguments.length > 0;
|
|
47899
|
+
if (!isNodeOfType(callee, "Identifier") && !isNodeOfType(callee, "MemberExpression")) return false;
|
|
47900
|
+
return call.arguments.some((argument) => isProvenComponentValue(argument, state));
|
|
47901
|
+
};
|
|
46021
47902
|
const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
46022
47903
|
for (const property of objectExpression.properties ?? []) {
|
|
46023
47904
|
if (!isNodeOfType(property, "Property")) continue;
|
|
@@ -46027,20 +47908,22 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
|
46027
47908
|
continue;
|
|
46028
47909
|
}
|
|
46029
47910
|
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
46030
|
-
if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) &&
|
|
47911
|
+
if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionHasReactRenderSemantics(value, state)) return true;
|
|
46031
47912
|
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
46032
47913
|
}
|
|
46033
47914
|
return false;
|
|
46034
47915
|
};
|
|
46035
47916
|
const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
47917
|
+
if (isNodeOfType(reportNode, "Identifier") && isProvenComponentValue(reportNode, state)) return { kind: "react-component" };
|
|
46036
47918
|
if (initializer) {
|
|
46037
47919
|
const expression = skipTsExpression(initializer);
|
|
46038
|
-
if (
|
|
47920
|
+
if (isNodeOfType(expression, "CallExpression") && isReactComponentName(name) && isComponentFactoryCall(expression, state)) return { kind: "react-component" };
|
|
47921
|
+
if (isRouteFactoryCall(expression, state.routeFactoryBindings)) return { kind: "react-component" };
|
|
46039
47922
|
if (isNodeOfType(expression, "CallExpression") && isHocCallee(expression.callee, state) && expression.arguments.length > 0 && isReactComponentName(name)) return { kind: "react-component" };
|
|
46040
47923
|
if (isNodeOfType(expression, "ConditionalExpression") && isReactComponentName(name) && isReactComponentInitializer(expression.consequent, state) && isReactComponentInitializer(expression.alternate, state)) return { kind: "react-component" };
|
|
46041
47924
|
}
|
|
46042
47925
|
if (state.allowExportNames.has(name)) return { kind: "allowed" };
|
|
46043
|
-
if (
|
|
47926
|
+
if (state.allowedRouteExportNames.has(name)) return { kind: "allowed" };
|
|
46044
47927
|
if (/^use[A-Z]/.test(name)) return { kind: "allowed" };
|
|
46045
47928
|
if (state.allowConstantExport && initializer) {
|
|
46046
47929
|
const expression = skipTsExpression(initializer);
|
|
@@ -46066,42 +47949,33 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
|
46066
47949
|
kind: "namespace-object",
|
|
46067
47950
|
reportNode
|
|
46068
47951
|
};
|
|
47952
|
+
if (isNodeOfType(stripped, "MemberExpression")) return isProvenComponentValue(stripped, state) ? { kind: "react-component" } : {
|
|
47953
|
+
kind: "non-component",
|
|
47954
|
+
reportNode
|
|
47955
|
+
};
|
|
47956
|
+
if (isNodeOfType(stripped, "Identifier")) return isProvenComponentValue(stripped, state) ? { kind: "react-component" } : {
|
|
47957
|
+
kind: "non-component",
|
|
47958
|
+
reportNode
|
|
47959
|
+
};
|
|
46069
47960
|
if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
|
|
46070
47961
|
kind: "non-component",
|
|
46071
47962
|
reportNode
|
|
46072
47963
|
};
|
|
47964
|
+
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "FunctionExpression")) return {
|
|
47965
|
+
kind: "non-component",
|
|
47966
|
+
reportNode
|
|
47967
|
+
};
|
|
46073
47968
|
}
|
|
46074
47969
|
return isReactComponentName(name) ? { kind: "react-component" } : {
|
|
46075
47970
|
kind: "non-component",
|
|
46076
47971
|
reportNode
|
|
46077
47972
|
};
|
|
46078
47973
|
};
|
|
46079
|
-
const isEntryPointFile = (filename) => {
|
|
46080
|
-
const lastSlash = Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\"));
|
|
46081
|
-
const basename = lastSlash === -1 ? filename : filename.slice(lastSlash + 1);
|
|
46082
|
-
return ENTRY_POINT_BASENAMES.has(basename);
|
|
46083
|
-
};
|
|
46084
|
-
const ASSET_FILE_BASENAME_PATTERN = /^([A-Za-z][\w-]*[-._])?(icons?|svgs?|svg[-_]?sprites?|sprites?|emojis?|flags?|logos?|lockups?|illustrations?|glyphs?|stickers?|emotes?|avatars?|backgrounds?|patterns?|assets?|gradients?|countryVectors?|paymentVectors?|brandVectors?|brandLogos?)\.(t|j)sx?$/;
|
|
46085
|
-
const UTILITY_BASENAME_SUFFIX_PATTERN = /^[A-Za-z][\w-]*(Utils|Util|Helpers|Helper|Shared|Constants|Constant|Types|Type|Mappings|Mapping|Lookups|Lookup|Registry|Renderers|Renderer|NodeTypes|EdgeTypes|CellTypes|ColumnDefs|ColumnTypes|ColumnRenderers|Schemas|Schema|Definitions|Definition|Config|Configuration|Defaults|Default|Tokens|Palette|Context|Provider|Providers|Logic|Scene|Page|Layout)\.(t|j)sx?$/;
|
|
46086
|
-
const HOOK_FILE_BASENAME_PATTERN = /^use[A-Z][\w-]*\.(t|j)sx?$/;
|
|
46087
|
-
const NODE_DEFINITION_BASENAME_PATTERN = /^[A-Z][\w-]*(NodeUtil|ShapeUtil|EdgeUtil|BindingUtil|InlineNode|BlockNode|NotebookNode)\.(t|j)sx?$/;
|
|
46088
|
-
const isAssetOrUtilityFile = (filename) => {
|
|
46089
|
-
const lastSlash = Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\"));
|
|
46090
|
-
const basename = lastSlash === -1 ? filename : filename.slice(lastSlash + 1);
|
|
46091
|
-
if (ASSET_FILE_BASENAME_PATTERN.test(basename)) return true;
|
|
46092
|
-
if (UTILITY_FILE_BASENAMES.has(basename)) return true;
|
|
46093
|
-
if (UTILITY_BASENAME_SUFFIX_PATTERN.test(basename)) return true;
|
|
46094
|
-
if (HOOK_FILE_BASENAME_PATTERN.test(basename)) return true;
|
|
46095
|
-
if (NODE_DEFINITION_BASENAME_PATTERN.test(basename)) return true;
|
|
46096
|
-
return false;
|
|
46097
|
-
};
|
|
46098
47974
|
const isFileNameAllowed = (filename, checkJS) => {
|
|
46099
47975
|
if (!filename) return true;
|
|
47976
|
+
if (TEST_SUPPORT_FILE_PATTERN.test(filename)) return false;
|
|
46100
47977
|
if (filename.includes(".test.") || filename.includes(".spec.") || filename.includes(".cy.") || filename.includes(".stories.")) return false;
|
|
46101
47978
|
for (const segment of NON_FAST_REFRESH_PATH_SEGMENTS) if (filename.includes(segment)) return false;
|
|
46102
|
-
if (isEntryPointFile(filename)) return false;
|
|
46103
|
-
if (isFrameworkRouteOrSpecialFilename(filename)) return false;
|
|
46104
|
-
if (isAssetOrUtilityFile(filename)) return false;
|
|
46105
47979
|
if (filename.endsWith(".tsx") || filename.endsWith(".jsx")) return true;
|
|
46106
47980
|
if (checkJS && filename.endsWith(".js")) return true;
|
|
46107
47981
|
return false;
|
|
@@ -46114,16 +47988,100 @@ const onlyExportComponents = defineRule({
|
|
|
46114
47988
|
category: "Architecture",
|
|
46115
47989
|
create: (context) => {
|
|
46116
47990
|
const settings = resolveSettings$6(context.settings);
|
|
46117
|
-
|
|
47991
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
47992
|
+
const fastRefreshStatus = getFastRefreshFileStatus(context);
|
|
47993
|
+
if (!fastRefreshStatus.isActive) return {};
|
|
47994
|
+
if (isFrameworkRouteOrSpecialFilename(context, fastRefreshStatus.runtime)) return {};
|
|
47995
|
+
if (!isFileNameAllowed(filename, settings.checkJS)) return {};
|
|
47996
|
+
const allowedRouteExportNames = fastRefreshStatus.runtime === "next" ? NEXT_ALLOWED_EXPORT_NAMES : fastRefreshStatus.runtime === "expo" ? EXPO_ALLOWED_EXPORT_NAMES : fastRefreshStatus.runtime === "react-router" || fastRefreshStatus.runtime === "remix" ? REACT_ROUTER_ALLOWED_EXPORT_NAMES : EMPTY_NAME_SET;
|
|
47997
|
+
const routeFactoryMemberNames = fastRefreshStatus.runtime === "tanstack" ? TANSTACK_ROUTE_FACTORY_CALLEE_NAMES : fastRefreshStatus.runtime === "react-router" || fastRefreshStatus.runtime === "remix" ? REACT_ROUTER_FACTORY_CALLEE_NAMES : EMPTY_NAME_SET;
|
|
47998
|
+
const routeFactoryLocalNames = /* @__PURE__ */ new Set();
|
|
47999
|
+
const routeFactoryNamespaceNames = /* @__PURE__ */ new Set();
|
|
48000
|
+
const nextDynamicImportSymbolIds = /* @__PURE__ */ new Set();
|
|
48001
|
+
const importSymbolIds = /* @__PURE__ */ new Set();
|
|
48002
|
+
const routeFactoryBindings = {
|
|
48003
|
+
localNames: routeFactoryLocalNames,
|
|
48004
|
+
memberNames: routeFactoryMemberNames,
|
|
48005
|
+
namespaceNames: routeFactoryNamespaceNames
|
|
48006
|
+
};
|
|
46118
48007
|
const exportNodes = [];
|
|
46119
48008
|
const componentCandidates = [];
|
|
48009
|
+
const createRootNames = /* @__PURE__ */ new Set();
|
|
48010
|
+
const hydrateRootNames = /* @__PURE__ */ new Set();
|
|
48011
|
+
const legacyRenderNames = /* @__PURE__ */ new Set();
|
|
48012
|
+
const reactDomNamespaceNames = /* @__PURE__ */ new Set();
|
|
48013
|
+
const rootNames = /* @__PURE__ */ new Set();
|
|
48014
|
+
let hasRootMount = false;
|
|
48015
|
+
const isCreateRootCall = (node) => {
|
|
48016
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
48017
|
+
if (isNodeOfType(node.callee, "Identifier")) return createRootNames.has(node.callee.name);
|
|
48018
|
+
return isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && reactDomNamespaceNames.has(node.callee.object.name) && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "createRoot";
|
|
48019
|
+
};
|
|
48020
|
+
const visitImportDeclaration = (node) => {
|
|
48021
|
+
if (!isNodeOfType(node, "ImportDeclaration")) return;
|
|
48022
|
+
const source = node.source.value;
|
|
48023
|
+
for (const specifier of node.specifiers) {
|
|
48024
|
+
const symbol = context.scopes.symbolFor(specifier.local);
|
|
48025
|
+
if (symbol) importSymbolIds.add(symbol.id);
|
|
48026
|
+
}
|
|
48027
|
+
if (typeof source === "string" && (fastRefreshStatus.runtime === "tanstack" && (source.startsWith("@tanstack/react-router") || source.startsWith("@tanstack/react-start")) || (fastRefreshStatus.runtime === "react-router" || fastRefreshStatus.runtime === "remix") && (source === "react-router" || source === "react-router-dom" || source === "@remix-run/react"))) for (const specifier of node.specifiers) {
|
|
48028
|
+
if (isNodeOfType(specifier, "ImportNamespaceSpecifier")) {
|
|
48029
|
+
routeFactoryNamespaceNames.add(specifier.local.name);
|
|
48030
|
+
continue;
|
|
48031
|
+
}
|
|
48032
|
+
const importedName = getImportedName(specifier);
|
|
48033
|
+
if (importedName && routeFactoryMemberNames.has(importedName)) routeFactoryLocalNames.add(specifier.local.name);
|
|
48034
|
+
}
|
|
48035
|
+
if (source === "next/dynamic") for (const specifier of node.specifiers) {
|
|
48036
|
+
if (!isNodeOfType(specifier, "ImportDefaultSpecifier")) continue;
|
|
48037
|
+
const symbol = context.scopes.symbolFor(specifier.local);
|
|
48038
|
+
if (symbol) nextDynamicImportSymbolIds.add(symbol.id);
|
|
48039
|
+
}
|
|
48040
|
+
if (source !== "react-dom" && source !== "react-dom/client") return;
|
|
48041
|
+
for (const specifier of node.specifiers) {
|
|
48042
|
+
if (isNodeOfType(specifier, "ImportDefaultSpecifier") || isNodeOfType(specifier, "ImportNamespaceSpecifier")) {
|
|
48043
|
+
reactDomNamespaceNames.add(specifier.local.name);
|
|
48044
|
+
continue;
|
|
48045
|
+
}
|
|
48046
|
+
const importedName = getImportedName(specifier);
|
|
48047
|
+
if (importedName === "createRoot") createRootNames.add(specifier.local.name);
|
|
48048
|
+
if (importedName === "hydrateRoot") hydrateRootNames.add(specifier.local.name);
|
|
48049
|
+
if (source === "react-dom" && (importedName === "render" || importedName === "hydrate")) legacyRenderNames.add(specifier.local.name);
|
|
48050
|
+
}
|
|
48051
|
+
};
|
|
48052
|
+
const visitCallExpression = (node) => {
|
|
48053
|
+
if (!isNodeOfType(node, "CallExpression")) return;
|
|
48054
|
+
if (isInsideFunctionScope(node)) return;
|
|
48055
|
+
if (isNodeOfType(node.callee, "Identifier")) {
|
|
48056
|
+
if (hydrateRootNames.has(node.callee.name) || legacyRenderNames.has(node.callee.name)) hasRootMount = true;
|
|
48057
|
+
return;
|
|
48058
|
+
}
|
|
48059
|
+
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
|
|
48060
|
+
const methodName = node.callee.property.name;
|
|
48061
|
+
if (isNodeOfType(node.callee.object, "Identifier") && reactDomNamespaceNames.has(node.callee.object.name) && (methodName === "render" || methodName === "hydrate" || methodName === "hydrateRoot")) {
|
|
48062
|
+
hasRootMount = true;
|
|
48063
|
+
return;
|
|
48064
|
+
}
|
|
48065
|
+
if (methodName !== "render") return;
|
|
48066
|
+
if (isCreateRootCall(node.callee.object)) {
|
|
48067
|
+
hasRootMount = true;
|
|
48068
|
+
return;
|
|
48069
|
+
}
|
|
48070
|
+
if (isNodeOfType(node.callee.object, "Identifier") && rootNames.has(node.callee.object.name)) hasRootMount = true;
|
|
48071
|
+
};
|
|
46120
48072
|
const pushExportNode = (node) => {
|
|
46121
48073
|
exportNodes.push(node);
|
|
46122
48074
|
};
|
|
46123
48075
|
const pushComponentCandidate = (node) => {
|
|
46124
48076
|
componentCandidates.push(node);
|
|
48077
|
+
if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.init && isCreateRootCall(node.init) && !isInsideFunctionScope(node)) rootNames.add(node.id.name);
|
|
46125
48078
|
};
|
|
46126
48079
|
return {
|
|
48080
|
+
ImportDeclaration: visitImportDeclaration,
|
|
48081
|
+
CallExpression: visitCallExpression,
|
|
48082
|
+
AssignmentExpression(node) {
|
|
48083
|
+
if (isNodeOfType(node.left, "Identifier") && rootNames.has(node.left.name)) rootNames.delete(node.left.name);
|
|
48084
|
+
},
|
|
46127
48085
|
ExportAllDeclaration: pushExportNode,
|
|
46128
48086
|
ExportDefaultDeclaration: pushExportNode,
|
|
46129
48087
|
ExportNamedDeclaration: pushExportNode,
|
|
@@ -46131,31 +48089,53 @@ const onlyExportComponents = defineRule({
|
|
|
46131
48089
|
VariableDeclarator: pushComponentCandidate,
|
|
46132
48090
|
ClassDeclaration: pushComponentCandidate,
|
|
46133
48091
|
"Program:exit"() {
|
|
48092
|
+
if (hasRootMount) return;
|
|
46134
48093
|
const localComponentNames = /* @__PURE__ */ new Set();
|
|
48094
|
+
const componentFactorySymbolIds = /* @__PURE__ */ new Set();
|
|
48095
|
+
for (const child of componentCandidates) {
|
|
48096
|
+
if (isInsideFunctionScope(child)) continue;
|
|
48097
|
+
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
48098
|
+
if (functionReturnsNextDynamicComponent(child, nextDynamicImportSymbolIds, context.scopes)) {
|
|
48099
|
+
const symbol = context.scopes.symbolFor(child.id);
|
|
48100
|
+
if (symbol?.references.every((reference) => reference.flag === "read")) componentFactorySymbolIds.add(symbol.id);
|
|
48101
|
+
}
|
|
48102
|
+
continue;
|
|
48103
|
+
}
|
|
48104
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier") && child.init && functionReturnsNextDynamicComponent(child.init, nextDynamicImportSymbolIds, context.scopes)) {
|
|
48105
|
+
const symbol = context.scopes.symbolFor(child.id);
|
|
48106
|
+
if (symbol?.references.every((reference) => reference.flag === "read")) componentFactorySymbolIds.add(symbol.id);
|
|
48107
|
+
}
|
|
48108
|
+
}
|
|
46135
48109
|
const state = {
|
|
46136
48110
|
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
46137
48111
|
allowExportNames: new Set(settings.allowExportNames),
|
|
46138
48112
|
allowConstantExport: settings.allowConstantExport,
|
|
48113
|
+
allowedRouteExportNames,
|
|
48114
|
+
routeFactoryBindings,
|
|
48115
|
+
componentFactorySymbolIds,
|
|
48116
|
+
importSymbolIds,
|
|
46139
48117
|
localComponentNames,
|
|
46140
48118
|
scopes: context.scopes,
|
|
46141
48119
|
controlFlow: context.cfg
|
|
46142
48120
|
};
|
|
46143
48121
|
for (const child of componentCandidates) {
|
|
46144
48122
|
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
46145
|
-
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) &&
|
|
48123
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionHasReactRenderSemantics(child, state)) localComponentNames.add(child.id.name);
|
|
46146
48124
|
}
|
|
46147
48125
|
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
46148
48126
|
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
46149
48127
|
}
|
|
46150
48128
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
46151
48129
|
const initializer = child.init;
|
|
46152
|
-
|
|
46153
|
-
|
|
46154
|
-
|
|
48130
|
+
const expression = initializer ? skipTsExpression(initializer) : null;
|
|
48131
|
+
const isDirectFunction = expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"));
|
|
48132
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (expression ? isEs6Component(expression) : false)) && !isInsideFunctionScope(child)) {
|
|
48133
|
+
if (!isDirectFunction || functionHasReactRenderSemantics(expression, state)) localComponentNames.add(child.id.name);
|
|
46155
48134
|
}
|
|
46156
48135
|
}
|
|
46157
48136
|
}
|
|
46158
48137
|
const exports = [];
|
|
48138
|
+
const exportAllNodes = [];
|
|
46159
48139
|
let hasReactExport = false;
|
|
46160
48140
|
let hasAnyExports = false;
|
|
46161
48141
|
const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
|
|
@@ -46163,10 +48143,8 @@ const onlyExportComponents = defineRule({
|
|
|
46163
48143
|
if (isNodeOfType(child, "ExportAllDeclaration")) {
|
|
46164
48144
|
if (child.exportKind === "type") continue;
|
|
46165
48145
|
hasAnyExports = true;
|
|
46166
|
-
|
|
46167
|
-
|
|
46168
|
-
message: EXPORT_ALL_MESSAGE
|
|
46169
|
-
});
|
|
48146
|
+
const source = child.source.value;
|
|
48147
|
+
if (typeof source !== "string" || exportAllAddsRuntimeValues(filename, source)) exportAllNodes.push(child);
|
|
46170
48148
|
continue;
|
|
46171
48149
|
}
|
|
46172
48150
|
if (isNodeOfType(child, "ExportDefaultDeclaration")) {
|
|
@@ -46174,17 +48152,24 @@ const onlyExportComponents = defineRule({
|
|
|
46174
48152
|
const declaration = child.declaration;
|
|
46175
48153
|
const stripped = skipTsExpression(declaration);
|
|
46176
48154
|
if (isNodeOfType(stripped, "FunctionDeclaration") || isNodeOfType(stripped, "FunctionExpression")) {
|
|
48155
|
+
const hasRenderOutput = functionHasReactRenderSemantics(stripped, state);
|
|
46177
48156
|
if (stripped.id) {
|
|
46178
48157
|
const idNode = stripped.id;
|
|
46179
48158
|
isExportedNodeIds.add(stripped);
|
|
46180
|
-
exports.push(classifyExport(idNode.name, idNode, true, null, state)
|
|
46181
|
-
|
|
48159
|
+
exports.push(hasRenderOutput ? classifyExport(idNode.name, idNode, true, null, state) : {
|
|
48160
|
+
kind: "non-component",
|
|
48161
|
+
reportNode: idNode
|
|
48162
|
+
});
|
|
48163
|
+
} else if (hasRenderOutput) {
|
|
46182
48164
|
context.report({
|
|
46183
48165
|
node: stripped,
|
|
46184
48166
|
message: ANONYMOUS_MESSAGE
|
|
46185
48167
|
});
|
|
46186
48168
|
hasReactExport = true;
|
|
46187
|
-
}
|
|
48169
|
+
} else exports.push({
|
|
48170
|
+
kind: "non-component",
|
|
48171
|
+
reportNode: stripped
|
|
48172
|
+
});
|
|
46188
48173
|
continue;
|
|
46189
48174
|
}
|
|
46190
48175
|
if (isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "ClassExpression")) {
|
|
@@ -46203,26 +48188,34 @@ const onlyExportComponents = defineRule({
|
|
|
46203
48188
|
continue;
|
|
46204
48189
|
}
|
|
46205
48190
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
46206
|
-
exports.push(
|
|
48191
|
+
exports.push(isProvenComponentValue(stripped, state) ? { kind: "react-component" } : {
|
|
48192
|
+
kind: "non-component",
|
|
48193
|
+
reportNode: stripped
|
|
48194
|
+
});
|
|
48195
|
+
continue;
|
|
48196
|
+
}
|
|
48197
|
+
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
48198
|
+
if (isProvenComponentValue(stripped, state)) hasReactExport = true;
|
|
48199
|
+
else exports.push({
|
|
48200
|
+
kind: "non-component",
|
|
48201
|
+
reportNode: stripped
|
|
48202
|
+
});
|
|
46207
48203
|
continue;
|
|
46208
48204
|
}
|
|
46209
48205
|
if (isNodeOfType(stripped, "CallExpression")) {
|
|
46210
|
-
if (isRouteFactoryCall(stripped)) {
|
|
48206
|
+
if (isRouteFactoryCall(stripped, state.routeFactoryBindings)) {
|
|
46211
48207
|
hasReactExport = true;
|
|
46212
48208
|
continue;
|
|
46213
48209
|
}
|
|
46214
|
-
|
|
46215
|
-
|
|
46216
|
-
|
|
46217
|
-
|
|
46218
|
-
|
|
46219
|
-
|
|
46220
|
-
|
|
46221
|
-
|
|
46222
|
-
|
|
46223
|
-
})();
|
|
46224
|
-
if (isHoc && firstArgIsValid) hasReactExport = true;
|
|
46225
|
-
else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
|
|
48210
|
+
if (isReactCreateContext(stripped)) {
|
|
48211
|
+
exports.push({
|
|
48212
|
+
kind: "react-context",
|
|
48213
|
+
reportNode: stripped
|
|
48214
|
+
});
|
|
48215
|
+
continue;
|
|
48216
|
+
}
|
|
48217
|
+
if (isDirectRefreshWrapperCall(stripped, state)) hasReactExport = true;
|
|
48218
|
+
else if (isConfigOnlyFactoryCall(stripped)) exports.push({
|
|
46226
48219
|
kind: "non-component",
|
|
46227
48220
|
reportNode: stripped
|
|
46228
48221
|
});
|
|
@@ -46233,16 +48226,32 @@ const onlyExportComponents = defineRule({
|
|
|
46233
48226
|
continue;
|
|
46234
48227
|
}
|
|
46235
48228
|
if (isNodeOfType(stripped, "ObjectExpression")) {
|
|
46236
|
-
|
|
46237
|
-
|
|
46238
|
-
|
|
48229
|
+
exports.push(objectExpressionBundlesComponents(stripped, state) ? {
|
|
48230
|
+
kind: "namespace-object",
|
|
48231
|
+
reportNode: stripped
|
|
48232
|
+
} : {
|
|
48233
|
+
kind: "non-component",
|
|
48234
|
+
reportNode: stripped
|
|
46239
48235
|
});
|
|
46240
48236
|
continue;
|
|
46241
48237
|
}
|
|
46242
|
-
if (isNodeOfType(stripped, "ArrowFunctionExpression")
|
|
46243
|
-
|
|
46244
|
-
|
|
46245
|
-
|
|
48238
|
+
if (isNodeOfType(stripped, "ArrowFunctionExpression")) {
|
|
48239
|
+
if (functionHasReactRenderSemantics(stripped, state)) {
|
|
48240
|
+
context.report({
|
|
48241
|
+
node: stripped,
|
|
48242
|
+
message: ANONYMOUS_MESSAGE
|
|
48243
|
+
});
|
|
48244
|
+
hasReactExport = true;
|
|
48245
|
+
} else exports.push({
|
|
48246
|
+
kind: "non-component",
|
|
48247
|
+
reportNode: stripped
|
|
48248
|
+
});
|
|
48249
|
+
continue;
|
|
48250
|
+
}
|
|
48251
|
+
if (isNodeOfType(stripped, "Literal") || isNodeOfType(stripped, "NewExpression")) {
|
|
48252
|
+
exports.push({
|
|
48253
|
+
kind: "non-component",
|
|
48254
|
+
reportNode: stripped
|
|
46246
48255
|
});
|
|
46247
48256
|
continue;
|
|
46248
48257
|
}
|
|
@@ -46259,7 +48268,10 @@ const onlyExportComponents = defineRule({
|
|
|
46259
48268
|
const declaration = child.declaration;
|
|
46260
48269
|
if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
|
|
46261
48270
|
isExportedNodeIds.add(declaration);
|
|
46262
|
-
exports.push(classifyExport(declaration.id.name, declaration.id, true, null, state)
|
|
48271
|
+
exports.push(functionHasReactRenderSemantics(declaration, state) || localComponentNames.has(declaration.id.name) ? classifyExport(declaration.id.name, declaration.id, true, null, state) : {
|
|
48272
|
+
kind: "non-component",
|
|
48273
|
+
reportNode: declaration.id
|
|
48274
|
+
});
|
|
46263
48275
|
} else if (isNodeOfType(declaration, "ClassDeclaration") && declaration.id) {
|
|
46264
48276
|
isExportedNodeIds.add(declaration);
|
|
46265
48277
|
if (isReactComponentName(declaration.id.name) && isEs6Component(declaration)) exports.push({ kind: "react-component" });
|
|
@@ -46283,6 +48295,7 @@ const onlyExportComponents = defineRule({
|
|
|
46283
48295
|
const isReExportFromSource = Boolean(child.source);
|
|
46284
48296
|
for (const specifier of child.specifiers ?? []) {
|
|
46285
48297
|
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
48298
|
+
if (specifier.exportKind === "type") continue;
|
|
46286
48299
|
const exported = specifier.exported;
|
|
46287
48300
|
const local = specifier.local;
|
|
46288
48301
|
let exportedName = null;
|
|
@@ -46290,8 +48303,13 @@ const onlyExportComponents = defineRule({
|
|
|
46290
48303
|
const localName = local && isNodeOfType(local, "Identifier") ? local.name : null;
|
|
46291
48304
|
const reportNode = specifier;
|
|
46292
48305
|
let entry;
|
|
46293
|
-
if (
|
|
46294
|
-
else if (exportedName) entry =
|
|
48306
|
+
if (localName && localComponentNames.has(localName)) entry = { kind: "react-component" };
|
|
48307
|
+
else if (!isReExportFromSource && localName === exportedName && localName !== null && isReactComponentName(localName)) entry = { kind: "react-component" };
|
|
48308
|
+
else if (exportedName === "default" && localName && local) entry = isProvenComponentValue(local, state) ? { kind: "react-component" } : {
|
|
48309
|
+
kind: "non-component",
|
|
48310
|
+
reportNode
|
|
48311
|
+
};
|
|
48312
|
+
else if (exportedName) entry = classifyExport(exportedName, reportNode, false, isReExportFromSource ? null : local, state);
|
|
46295
48313
|
else {
|
|
46296
48314
|
entry = {
|
|
46297
48315
|
kind: "non-component",
|
|
@@ -46309,15 +48327,21 @@ const onlyExportComponents = defineRule({
|
|
|
46309
48327
|
node: entry.reportNode,
|
|
46310
48328
|
message: NAMESPACE_OBJECT_MESSAGE
|
|
46311
48329
|
});
|
|
46312
|
-
if (hasAnyExports && hasReactExport)
|
|
46313
|
-
|
|
46314
|
-
node:
|
|
46315
|
-
message:
|
|
46316
|
-
});
|
|
46317
|
-
if (entry.kind === "react-context") context.report({
|
|
46318
|
-
node: entry.reportNode,
|
|
46319
|
-
message: REACT_CONTEXT_MESSAGE
|
|
48330
|
+
if (hasAnyExports && hasReactExport) {
|
|
48331
|
+
for (const exportAllNode of exportAllNodes) context.report({
|
|
48332
|
+
node: exportAllNode,
|
|
48333
|
+
message: EXPORT_ALL_MESSAGE
|
|
46320
48334
|
});
|
|
48335
|
+
for (const entry of exports) {
|
|
48336
|
+
if (entry.kind === "non-component") context.report({
|
|
48337
|
+
node: entry.reportNode,
|
|
48338
|
+
message: NAMED_EXPORT_MESSAGE
|
|
48339
|
+
});
|
|
48340
|
+
if (entry.kind === "react-context") context.report({
|
|
48341
|
+
node: entry.reportNode,
|
|
48342
|
+
message: REACT_CONTEXT_MESSAGE
|
|
48343
|
+
});
|
|
48344
|
+
}
|
|
46321
48345
|
}
|
|
46322
48346
|
}
|
|
46323
48347
|
};
|
|
@@ -57980,6 +60004,78 @@ const inferDestructureSourceKey = (bindingIdentifier) => {
|
|
|
57980
60004
|
return null;
|
|
57981
60005
|
};
|
|
57982
60006
|
const isReactUseHook = (hookName) => hookName === "use";
|
|
60007
|
+
const isReactHookCapabilityValue = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
60008
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
60009
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
60010
|
+
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
60011
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
60012
|
+
visitedSymbolIds.add(symbol.id);
|
|
60013
|
+
if (symbol.kind === "import") return isReactImport(symbol) && isReactHookName(getImportedName(symbol.declarationNode) ?? "");
|
|
60014
|
+
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
60015
|
+
const destructuredPropertyName = getDestructuredBindingPropertyName(symbol.bindingIdentifier);
|
|
60016
|
+
if (destructuredPropertyName) {
|
|
60017
|
+
const namespaceExpression = stripParenExpression(symbol.initializer);
|
|
60018
|
+
return isReactHookName(destructuredPropertyName) && isNodeOfType(namespaceExpression, "Identifier") && isReactNamespaceImport(namespaceExpression, scopes) && !hasPossibleStaticPropertyMutationOrEscape(namespaceExpression, destructuredPropertyName, scopes);
|
|
60019
|
+
}
|
|
60020
|
+
if (symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
|
|
60021
|
+
return isReactHookCapabilityValue(symbol.initializer, scopes, visitedSymbolIds);
|
|
60022
|
+
}
|
|
60023
|
+
if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return false;
|
|
60024
|
+
const propertyName = getStaticPropertyName(unwrappedExpression);
|
|
60025
|
+
if (!propertyName || !isReactHookName(propertyName)) return false;
|
|
60026
|
+
const namespaceExpression = stripParenExpression(unwrappedExpression.object);
|
|
60027
|
+
return isNodeOfType(namespaceExpression, "Identifier") && isReactNamespaceImport(namespaceExpression, scopes) && !hasPossibleStaticPropertyMutationOrEscape(namespaceExpression, propertyName, scopes);
|
|
60028
|
+
};
|
|
60029
|
+
const isReactHookCapabilityComparisonOperand = (expression, scopes) => {
|
|
60030
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
60031
|
+
if (isReactHookCapabilityValue(unwrappedExpression, scopes)) return true;
|
|
60032
|
+
return isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "typeof" && isReactHookCapabilityValue(unwrappedExpression.argument, scopes);
|
|
60033
|
+
};
|
|
60034
|
+
const isStaticCapabilityComparisonValue = (expression, scopes) => {
|
|
60035
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
60036
|
+
if (isNodeOfType(unwrappedExpression, "Literal")) return true;
|
|
60037
|
+
return isNodeOfType(unwrappedExpression, "Identifier") && unwrappedExpression.name === "undefined" && scopes.isGlobalReference(unwrappedExpression);
|
|
60038
|
+
};
|
|
60039
|
+
const CAPABILITY_COMPARISON_OPERATORS = new Set([
|
|
60040
|
+
"==",
|
|
60041
|
+
"!=",
|
|
60042
|
+
"===",
|
|
60043
|
+
"!=="
|
|
60044
|
+
]);
|
|
60045
|
+
const isInvariantReactHookCapabilityCondition = (expression, scopes) => {
|
|
60046
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
60047
|
+
if (isReactHookCapabilityValue(unwrappedExpression, scopes)) return true;
|
|
60048
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return isInvariantReactHookCapabilityCondition(unwrappedExpression.argument, scopes);
|
|
60049
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return isInvariantReactHookCapabilityCondition(unwrappedExpression.left, scopes) && isInvariantReactHookCapabilityCondition(unwrappedExpression.right, scopes);
|
|
60050
|
+
if (!isNodeOfType(unwrappedExpression, "BinaryExpression") || !CAPABILITY_COMPARISON_OPERATORS.has(unwrappedExpression.operator)) return false;
|
|
60051
|
+
return isReactHookCapabilityComparisonOperand(unwrappedExpression.left, scopes) && isStaticCapabilityComparisonValue(unwrappedExpression.right, scopes) || isReactHookCapabilityComparisonOperand(unwrappedExpression.right, scopes) && isStaticCapabilityComparisonValue(unwrappedExpression.left, scopes);
|
|
60052
|
+
};
|
|
60053
|
+
const statementContainsOwnAbruptCompletion = (statement) => {
|
|
60054
|
+
let doesContainAbruptCompletion = false;
|
|
60055
|
+
walkAst(statement, (child) => {
|
|
60056
|
+
if (child !== statement && isFunctionLike$1(child)) return false;
|
|
60057
|
+
if (isNodeOfType(child, "ReturnStatement") || isNodeOfType(child, "ThrowStatement")) {
|
|
60058
|
+
doesContainAbruptCompletion = true;
|
|
60059
|
+
return false;
|
|
60060
|
+
}
|
|
60061
|
+
});
|
|
60062
|
+
return doesContainAbruptCompletion;
|
|
60063
|
+
};
|
|
60064
|
+
const isInvariantReactHookCapabilityExit = (statement, scopes) => isNodeOfType(statement, "IfStatement") && statement.alternate === null && statementAlwaysExits(statement.consequent) && isInvariantReactHookCapabilityCondition(statement.test, scopes);
|
|
60065
|
+
const isAfterOnlyInvariantReactHookCapabilityExits = (node, enclosingFunction, scopes) => {
|
|
60066
|
+
if (!isFunctionLike$1(enclosingFunction) || !isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
|
|
60067
|
+
let containingStatement = node;
|
|
60068
|
+
while (containingStatement.parent && containingStatement.parent !== enclosingFunction.body) {
|
|
60069
|
+
if (isFunctionLike$1(containingStatement.parent)) return false;
|
|
60070
|
+
if (isNodeOfType(containingStatement.parent, "IfStatement") || isNodeOfType(containingStatement.parent, "SwitchStatement") || isNodeOfType(containingStatement.parent, "SwitchCase") || isNodeOfType(containingStatement.parent, "ConditionalExpression") || isNodeOfType(containingStatement.parent, "LogicalExpression")) return false;
|
|
60071
|
+
containingStatement = containingStatement.parent;
|
|
60072
|
+
}
|
|
60073
|
+
if (containingStatement.parent !== enclosingFunction.body) return false;
|
|
60074
|
+
const statementIndex = enclosingFunction.body.body.findIndex((statement) => statement === containingStatement);
|
|
60075
|
+
if (statementIndex <= 0) return false;
|
|
60076
|
+
const bypassingStatements = enclosingFunction.body.body.slice(0, statementIndex).filter((statement) => statementContainsOwnAbruptCompletion(statement));
|
|
60077
|
+
return bypassingStatements.length > 0 && bypassingStatements.every((statement) => isInvariantReactHookCapabilityExit(statement, scopes));
|
|
60078
|
+
};
|
|
57983
60079
|
const getCallExpressionCalleeName = (callExpression) => {
|
|
57984
60080
|
const callee = callExpression.callee;
|
|
57985
60081
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
@@ -58275,7 +60371,7 @@ const rulesOfHooks = defineRule({
|
|
|
58275
60371
|
});
|
|
58276
60372
|
return;
|
|
58277
60373
|
}
|
|
58278
|
-
if (!context.cfg.isUnconditionalFromEntry(node)) context.report({
|
|
60374
|
+
if (!context.cfg.isUnconditionalFromEntry(node) && !isAfterOnlyInvariantReactHookCapabilityExits(node, enclosing.node, context.scopes)) context.report({
|
|
58279
60375
|
node: node.callee,
|
|
58280
60376
|
message: buildConditionalMessage(hookName)
|
|
58281
60377
|
});
|
|
@@ -66414,6 +68510,7 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
66414
68510
|
"no-effect-with-fresh-deps",
|
|
66415
68511
|
"no-initialize-state",
|
|
66416
68512
|
"no-mutating-reducer-state",
|
|
68513
|
+
"only-export-components",
|
|
66417
68514
|
"no-unguarded-browser-global-in-render-or-hook-init",
|
|
66418
68515
|
"prefer-dynamic-import",
|
|
66419
68516
|
"rendering-hydration-mismatch-time",
|
|
@@ -66621,7 +68718,7 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
66621
68718
|
* `CROSS_FILE_DEPENDENCY_COLLECTORS` (the core guard test enforces the
|
|
66622
68719
|
* partition), forcing a conscious classification.
|
|
66623
68720
|
*/
|
|
66624
|
-
const UNBOUNDED_CROSS_FILE_RULE_IDS =
|
|
68721
|
+
const UNBOUNDED_CROSS_FILE_RULE_IDS = new Set(["only-export-components"]);
|
|
66625
68722
|
/**
|
|
66626
68723
|
* Runs the collectors for `ruleIds` over one file and returns every
|
|
66627
68724
|
* filesystem probe they made — the file's cross-file dependency set.
|