oxlint-plugin-react-doctor 0.7.4-dev.70b5d99 → 0.7.4-dev.7bbb792
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.d.ts +185 -1
- package/dist/index.js +2755 -920
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1915,7 +1915,10 @@ const isGeneratedImageRenderContext = (context, node) => {
|
|
|
1915
1915
|
if (!node) return false;
|
|
1916
1916
|
const programRoot = findProgramRoot(node);
|
|
1917
1917
|
if (!programRoot) return false;
|
|
1918
|
-
|
|
1918
|
+
const generatedImageJsxNodes = collectGeneratedImageJsxNodes(programRoot);
|
|
1919
|
+
if (generatedImageJsxNodes.has(node)) return true;
|
|
1920
|
+
if (isNodeOfType(node, "JSXElement")) return generatedImageJsxNodes.has(node.openingElement);
|
|
1921
|
+
return false;
|
|
1919
1922
|
};
|
|
1920
1923
|
//#endregion
|
|
1921
1924
|
//#region src/plugin/utils/object-has-accessible-child.ts
|
|
@@ -8403,6 +8406,54 @@ const effectListenerCleanupMismatch = defineRule({
|
|
|
8403
8406
|
} })
|
|
8404
8407
|
});
|
|
8405
8408
|
//#endregion
|
|
8409
|
+
//#region src/plugin/utils/collect-effect-invoked-functions.ts
|
|
8410
|
+
const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
|
|
8411
|
+
"then",
|
|
8412
|
+
"catch",
|
|
8413
|
+
"finally"
|
|
8414
|
+
]);
|
|
8415
|
+
const collectEffectInvokedFunctions = (effectCallback) => {
|
|
8416
|
+
const invokedFunctions = new Set([effectCallback]);
|
|
8417
|
+
const localFunctionBindings = /* @__PURE__ */ new Map();
|
|
8418
|
+
const calledBindingNames = /* @__PURE__ */ new Set();
|
|
8419
|
+
const pendingFunctions = [effectCallback];
|
|
8420
|
+
const enqueue = (candidate) => {
|
|
8421
|
+
const strippedCandidate = candidate ? stripParenExpression(candidate) : candidate;
|
|
8422
|
+
if (!isFunctionLike$1(strippedCandidate) || invokedFunctions.has(strippedCandidate)) return;
|
|
8423
|
+
invokedFunctions.add(strippedCandidate);
|
|
8424
|
+
pendingFunctions.push(strippedCandidate);
|
|
8425
|
+
};
|
|
8426
|
+
const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
|
|
8427
|
+
while (pendingFunctions.length > 0) {
|
|
8428
|
+
const currentFunction = pendingFunctions.pop();
|
|
8429
|
+
if (!currentFunction) break;
|
|
8430
|
+
walkAst(currentFunction, (child) => {
|
|
8431
|
+
if (child !== currentFunction && isFunctionLike$1(child)) {
|
|
8432
|
+
if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) localFunctionBindings.set(child.id.name, child);
|
|
8433
|
+
return false;
|
|
8434
|
+
}
|
|
8435
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
8436
|
+
const initializer = child.init ? stripParenExpression(child.init) : null;
|
|
8437
|
+
if (isFunctionLike$1(initializer)) localFunctionBindings.set(child.id.name, initializer);
|
|
8438
|
+
return;
|
|
8439
|
+
}
|
|
8440
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8441
|
+
const callee = stripParenExpression(child.callee);
|
|
8442
|
+
if (isFunctionLike$1(callee)) {
|
|
8443
|
+
enqueue(callee);
|
|
8444
|
+
return;
|
|
8445
|
+
}
|
|
8446
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
8447
|
+
calledBindingNames.add(callee.name);
|
|
8448
|
+
return;
|
|
8449
|
+
}
|
|
8450
|
+
if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
8451
|
+
});
|
|
8452
|
+
for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
|
|
8453
|
+
}
|
|
8454
|
+
return invokedFunctions;
|
|
8455
|
+
};
|
|
8456
|
+
//#endregion
|
|
8406
8457
|
//#region src/plugin/utils/is-react-hook-name.ts
|
|
8407
8458
|
const isReactHookName = (name) => {
|
|
8408
8459
|
if (!name.startsWith("use")) return false;
|
|
@@ -8458,46 +8509,90 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
8458
8509
|
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
8459
8510
|
};
|
|
8460
8511
|
//#endregion
|
|
8512
|
+
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
8513
|
+
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
8514
|
+
let ancestor = bindingIdentifier.parent;
|
|
8515
|
+
while (ancestor) {
|
|
8516
|
+
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
8517
|
+
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
8518
|
+
ancestor = ancestor.parent ?? null;
|
|
8519
|
+
}
|
|
8520
|
+
return null;
|
|
8521
|
+
};
|
|
8522
|
+
//#endregion
|
|
8523
|
+
//#region src/plugin/utils/executes-during-render.ts
|
|
8524
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
|
|
8525
|
+
"map",
|
|
8526
|
+
"filter",
|
|
8527
|
+
"forEach",
|
|
8528
|
+
"flatMap",
|
|
8529
|
+
"reduce",
|
|
8530
|
+
"reduceRight",
|
|
8531
|
+
"some",
|
|
8532
|
+
"every",
|
|
8533
|
+
"find",
|
|
8534
|
+
"findIndex",
|
|
8535
|
+
"findLast",
|
|
8536
|
+
"findLastIndex",
|
|
8537
|
+
"sort",
|
|
8538
|
+
"toSorted"
|
|
8539
|
+
]);
|
|
8540
|
+
const REACT_RENDER_PHASE_HOOK_NAMES = new Set(["useMemo", "useState"]);
|
|
8541
|
+
const isGlobalArrayFromMember = (node, scopes) => {
|
|
8542
|
+
const unwrappedNode = stripParenExpression(node);
|
|
8543
|
+
if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed || !isNodeOfType(unwrappedNode.object, "Identifier") || unwrappedNode.object.name !== "Array" || !scopes.isGlobalReference(unwrappedNode.object) || !isNodeOfType(unwrappedNode.property, "Identifier")) return false;
|
|
8544
|
+
return unwrappedNode.property.name === "from";
|
|
8545
|
+
};
|
|
8546
|
+
const isConstAliasOfGlobalArrayFrom = (node, scopes) => {
|
|
8547
|
+
const unwrappedNode = stripParenExpression(node);
|
|
8548
|
+
if (!isNodeOfType(unwrappedNode, "Identifier")) return false;
|
|
8549
|
+
const binding = findVariableInitializer(unwrappedNode, unwrappedNode.name);
|
|
8550
|
+
if (!binding?.initializer) return false;
|
|
8551
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
8552
|
+
return Boolean(declarator && scopes.symbolFor(unwrappedNode)?.declarationNode === declarator && declarator.init && declarator.parent && isNodeOfType(declarator.parent, "VariableDeclaration") && declarator.parent.kind === "const" && isGlobalArrayFromMember(declarator.init, scopes));
|
|
8553
|
+
};
|
|
8554
|
+
const executesDuringRender = (functionNode, scopes) => {
|
|
8555
|
+
const parent = functionNode.parent;
|
|
8556
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
8557
|
+
if (parent.callee === functionNode) return true;
|
|
8558
|
+
if ((scopes ? isReactApiCall(parent, REACT_RENDER_PHASE_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }) : isHookCall$2(parent, REACT_RENDER_PHASE_HOOK_NAMES)) && parent.arguments?.[0] === functionNode) return true;
|
|
8559
|
+
if (scopes && parent.arguments?.[1] === functionNode && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
|
|
8560
|
+
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
|
|
8561
|
+
};
|
|
8562
|
+
//#endregion
|
|
8563
|
+
//#region src/plugin/utils/find-render-phase-component-or-hook.ts
|
|
8564
|
+
const findRenderPhaseComponentOrHook = (node, scopes) => {
|
|
8565
|
+
let functionNode = findEnclosingFunction(node);
|
|
8566
|
+
while (functionNode) {
|
|
8567
|
+
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
8568
|
+
if (!executesDuringRender(functionNode, scopes)) return null;
|
|
8569
|
+
functionNode = findEnclosingFunction(functionNode);
|
|
8570
|
+
}
|
|
8571
|
+
return null;
|
|
8572
|
+
};
|
|
8573
|
+
//#endregion
|
|
8574
|
+
//#region src/plugin/utils/get-function-binding-name.ts
|
|
8575
|
+
const getFunctionBindingIdentifier = (functionNode) => {
|
|
8576
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
8577
|
+
const parent = functionNode.parent;
|
|
8578
|
+
if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
|
|
8579
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionNode && isNodeOfType(parent.left, "Identifier")) return parent.left;
|
|
8580
|
+
if (isNodeOfType(parent, "CallExpression")) {
|
|
8581
|
+
const callParent = parent.parent;
|
|
8582
|
+
if (isNodeOfType(callParent, "VariableDeclarator") && isNodeOfType(callParent.id, "Identifier")) return callParent.id;
|
|
8583
|
+
}
|
|
8584
|
+
return null;
|
|
8585
|
+
};
|
|
8586
|
+
const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(functionNode)?.name ?? null;
|
|
8587
|
+
//#endregion
|
|
8461
8588
|
//#region src/plugin/utils/get-range-start.ts
|
|
8462
8589
|
const getRangeStart = (node) => {
|
|
8463
8590
|
const rangeStart = node.range?.[0];
|
|
8464
8591
|
return typeof rangeStart === "number" ? rangeStart : null;
|
|
8465
8592
|
};
|
|
8466
8593
|
//#endregion
|
|
8467
|
-
//#region src/plugin/utils/is-
|
|
8468
|
-
const
|
|
8469
|
-
let node = callExpression;
|
|
8470
|
-
let parent = node.parent;
|
|
8471
|
-
while (parent) {
|
|
8472
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
8473
|
-
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
8474
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
8475
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
8476
|
-
node = parent;
|
|
8477
|
-
parent = node.parent;
|
|
8478
|
-
continue;
|
|
8479
|
-
}
|
|
8480
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
8481
|
-
node = parent;
|
|
8482
|
-
parent = node.parent;
|
|
8483
|
-
continue;
|
|
8484
|
-
}
|
|
8485
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
8486
|
-
node = parent;
|
|
8487
|
-
parent = node.parent;
|
|
8488
|
-
continue;
|
|
8489
|
-
}
|
|
8490
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
8491
|
-
const expressions = parent.expressions ?? [];
|
|
8492
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
8493
|
-
node = parent;
|
|
8494
|
-
parent = node.parent;
|
|
8495
|
-
continue;
|
|
8496
|
-
}
|
|
8497
|
-
return false;
|
|
8498
|
-
}
|
|
8499
|
-
return false;
|
|
8500
|
-
};
|
|
8594
|
+
//#region src/plugin/utils/is-event-handler-attribute.ts
|
|
8595
|
+
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
|
|
8501
8596
|
//#endregion
|
|
8502
8597
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
8503
8598
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
@@ -8532,125 +8627,9 @@ const isCleanupReturningSubscribeLikeCallExpression = (node) => {
|
|
|
8532
8627
|
return true;
|
|
8533
8628
|
};
|
|
8534
8629
|
//#endregion
|
|
8535
|
-
//#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
|
|
8536
|
-
const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
|
|
8537
|
-
"each",
|
|
8538
|
-
"every",
|
|
8539
|
-
"filter",
|
|
8540
|
-
"find",
|
|
8541
|
-
"findIndex",
|
|
8542
|
-
"findLast",
|
|
8543
|
-
"findLastIndex",
|
|
8544
|
-
"flatMap",
|
|
8545
|
-
"forEach",
|
|
8546
|
-
"map",
|
|
8547
|
-
"reduce",
|
|
8548
|
-
"reduceRight",
|
|
8549
|
-
"some",
|
|
8550
|
-
"sort",
|
|
8551
|
-
"toSorted"
|
|
8552
|
-
]);
|
|
8553
|
-
const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
8554
|
-
"from",
|
|
8555
|
-
"fromAsync",
|
|
8556
|
-
"groupBy"
|
|
8557
|
-
]);
|
|
8558
|
-
const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8559
|
-
const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
|
|
8560
|
-
const isListenerRemovalViaNullHandler = (callNode) => {
|
|
8561
|
-
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
8562
|
-
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8563
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
|
|
8564
|
-
};
|
|
8565
|
-
const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
|
|
8566
|
-
"off",
|
|
8567
|
-
"removeEventListener",
|
|
8568
|
-
"removeListener"
|
|
8569
|
-
]);
|
|
8570
|
-
const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
|
|
8571
|
-
if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
|
|
8572
|
-
const handlerArgument = callNode.arguments?.[1];
|
|
8573
|
-
return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
|
|
8574
|
-
};
|
|
8575
|
-
const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8576
|
-
const callNode = unwrapChainExpression$3(node);
|
|
8577
|
-
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
8578
|
-
if (isListenerRemovalViaNullHandler(callNode)) return true;
|
|
8579
|
-
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8580
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
8581
|
-
if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
|
|
8582
|
-
if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
|
|
8583
|
-
if (knownCleanupFunctionNames.has(callee.name)) return true;
|
|
8584
|
-
return false;
|
|
8585
|
-
}
|
|
8586
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
8587
|
-
if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
|
|
8588
|
-
if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
|
|
8589
|
-
return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
|
|
8590
|
-
}
|
|
8591
|
-
return false;
|
|
8592
|
-
};
|
|
8593
|
-
const isStaticIteratorCallbackCallee = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && isNodeOfType(callee.property, "Identifier") && (callee.object.name === "Array" || callee.object.name === "Map" || callee.object.name === "Object") && STATIC_ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
|
|
8594
|
-
const isIteratorCallbackArgument = (node) => {
|
|
8595
|
-
const parentNode = node.parent;
|
|
8596
|
-
if (!isNodeOfType(parentNode, "CallExpression")) return false;
|
|
8597
|
-
if (!parentNode.arguments?.some((argument) => argument === node)) return false;
|
|
8598
|
-
const callee = unwrapChainExpression$3(parentNode.callee);
|
|
8599
|
-
if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
|
|
8600
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(callee.property.name);
|
|
8601
|
-
};
|
|
8602
|
-
const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8603
|
-
let didFindRelease = false;
|
|
8604
|
-
walkAst(node, (child) => {
|
|
8605
|
-
if (didFindRelease) return false;
|
|
8606
|
-
if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
|
|
8607
|
-
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
8608
|
-
didFindRelease = true;
|
|
8609
|
-
return false;
|
|
8610
|
-
}
|
|
8611
|
-
});
|
|
8612
|
-
return didFindRelease;
|
|
8613
|
-
};
|
|
8614
|
-
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8615
|
-
if (!isFunctionLike$1(node)) return false;
|
|
8616
|
-
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
8617
|
-
};
|
|
8618
|
-
const isProvablyNoOpCleanupFunction = (node) => {
|
|
8619
|
-
if (!isFunctionLike$1(node)) return false;
|
|
8620
|
-
let sawNoOpRemoval = false;
|
|
8621
|
-
let sawOtherCall = false;
|
|
8622
|
-
walkAst(node.body, (child) => {
|
|
8623
|
-
if (sawOtherCall) return false;
|
|
8624
|
-
if (isFunctionLike$1(child)) return false;
|
|
8625
|
-
const callNode = unwrapChainExpression$3(child);
|
|
8626
|
-
if (!isNodeOfType(callNode, "CallExpression")) return;
|
|
8627
|
-
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8628
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
|
|
8629
|
-
sawNoOpRemoval = true;
|
|
8630
|
-
return;
|
|
8631
|
-
}
|
|
8632
|
-
sawOtherCall = true;
|
|
8633
|
-
});
|
|
8634
|
-
return sawNoOpRemoval && !sawOtherCall;
|
|
8635
|
-
};
|
|
8636
|
-
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
|
|
8637
|
-
if (!returnedValue) return false;
|
|
8638
|
-
const unwrappedValue = unwrapChainExpression$3(returnedValue);
|
|
8639
|
-
if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
|
|
8640
|
-
if (isNodeOfType(unwrappedValue, "Identifier")) {
|
|
8641
|
-
if (unwrappedValue.name === "undefined") return false;
|
|
8642
|
-
if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
|
|
8643
|
-
return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
|
|
8644
|
-
}
|
|
8645
|
-
if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
8646
|
-
if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
|
|
8647
|
-
if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
8648
|
-
if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
|
|
8649
|
-
return false;
|
|
8650
|
-
};
|
|
8651
|
-
//#endregion
|
|
8652
8630
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
8653
8631
|
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
8632
|
+
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
8654
8633
|
const RESOURCE_NOUN_BY_KIND = {
|
|
8655
8634
|
subscribe: "subscription",
|
|
8656
8635
|
timer: "timer",
|
|
@@ -8661,7 +8640,66 @@ const isSubscribeOrObserveCall = (node) => {
|
|
|
8661
8640
|
if (isSubscribeLikeCallExpression(node)) return true;
|
|
8662
8641
|
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
8663
8642
|
};
|
|
8664
|
-
const
|
|
8643
|
+
const resolveExpressionKey$1 = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
8644
|
+
if (!expression) return null;
|
|
8645
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
8646
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
8647
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
8648
|
+
if (!symbol) return context.scopes.isGlobalReference(unwrappedExpression) ? `global:${unwrappedExpression.name}` : null;
|
|
8649
|
+
if (visitedSymbolIds.has(symbol.id)) return `symbol:${symbol.id}`;
|
|
8650
|
+
visitedSymbolIds.add(symbol.id);
|
|
8651
|
+
const bindingProperty = symbol.bindingIdentifier.parent;
|
|
8652
|
+
const bindingPattern = bindingProperty?.parent;
|
|
8653
|
+
const variableDeclarator = bindingPattern?.parent;
|
|
8654
|
+
const bindingPropertyName = isNodeOfType(bindingProperty, "Property") ? getStaticPropertyKeyName(bindingProperty) : null;
|
|
8655
|
+
if (bindingPropertyName && isNodeOfType(bindingPattern, "ObjectPattern") && isNodeOfType(variableDeclarator, "VariableDeclarator") && variableDeclarator.id === bindingPattern) {
|
|
8656
|
+
const objectKey = resolveExpressionKey$1(variableDeclarator.init, context, visitedSymbolIds);
|
|
8657
|
+
return objectKey ? `${objectKey}.${bindingPropertyName}` : `symbol:${symbol.id}`;
|
|
8658
|
+
}
|
|
8659
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
8660
|
+
if (symbol.kind === "const" && initializer && (isNodeOfType(initializer, "Identifier") || isNodeOfType(initializer, "MemberExpression"))) return resolveExpressionKey$1(initializer, context, visitedSymbolIds) ?? `symbol:${symbol.id}`;
|
|
8661
|
+
return `symbol:${symbol.id}`;
|
|
8662
|
+
}
|
|
8663
|
+
if (isNodeOfType(unwrappedExpression, "MemberExpression") && !unwrappedExpression.computed) {
|
|
8664
|
+
if (!isNodeOfType(unwrappedExpression.property, "Identifier")) return null;
|
|
8665
|
+
const objectKey = resolveExpressionKey$1(unwrappedExpression.object, context, visitedSymbolIds);
|
|
8666
|
+
return objectKey ? `${objectKey}.${unwrappedExpression.property.name}` : null;
|
|
8667
|
+
}
|
|
8668
|
+
if (isNodeOfType(unwrappedExpression, "ThisExpression")) return "this";
|
|
8669
|
+
if (isNodeOfType(unwrappedExpression, "Literal") && (typeof unwrappedExpression.value === "string" || typeof unwrappedExpression.value === "number")) return `literal:${String(unwrappedExpression.value)}`;
|
|
8670
|
+
if (isFunctionLike$1(unwrappedExpression)) {
|
|
8671
|
+
const rangeStart = getRangeStart(unwrappedExpression);
|
|
8672
|
+
return rangeStart === null ? null : `function:${rangeStart}`;
|
|
8673
|
+
}
|
|
8674
|
+
return null;
|
|
8675
|
+
};
|
|
8676
|
+
const findAssignedResourceKey = (resourceNode, context) => {
|
|
8677
|
+
let currentNode = resourceNode;
|
|
8678
|
+
let parentNode = currentNode.parent;
|
|
8679
|
+
while (isNodeOfType(parentNode, "ChainExpression")) {
|
|
8680
|
+
currentNode = parentNode;
|
|
8681
|
+
parentNode = currentNode.parent;
|
|
8682
|
+
}
|
|
8683
|
+
if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode) return resolveExpressionKey$1(parentNode.id, context);
|
|
8684
|
+
if (isNodeOfType(parentNode, "AssignmentExpression") && parentNode.right === currentNode) return resolveExpressionKey$1(parentNode.left, context);
|
|
8685
|
+
return null;
|
|
8686
|
+
};
|
|
8687
|
+
const getCallRegistrationDetails = (callNode, context) => {
|
|
8688
|
+
const callee = stripParenExpression(callNode.callee);
|
|
8689
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return {
|
|
8690
|
+
receiverKey: null,
|
|
8691
|
+
registrationVerbName: null,
|
|
8692
|
+
eventKey: null,
|
|
8693
|
+
handlerKey: null
|
|
8694
|
+
};
|
|
8695
|
+
return {
|
|
8696
|
+
receiverKey: resolveExpressionKey$1(callee.object, context),
|
|
8697
|
+
registrationVerbName: callee.property.name,
|
|
8698
|
+
eventKey: resolveExpressionKey$1(callNode.arguments?.[0], context),
|
|
8699
|
+
handlerKey: resolveExpressionKey$1(callNode.arguments?.[1], context)
|
|
8700
|
+
};
|
|
8701
|
+
};
|
|
8702
|
+
const findSubscribeLikeUsages = (callback, context) => {
|
|
8665
8703
|
const usages = [];
|
|
8666
8704
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8667
8705
|
let cleanupArgument = null;
|
|
@@ -8670,13 +8708,22 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
8670
8708
|
const lastCallbackStatement = callbackStatements[callbackStatements.length - 1];
|
|
8671
8709
|
if (isNodeOfType(lastCallbackStatement, "ReturnStatement") && lastCallbackStatement.argument) cleanupArgument = lastCallbackStatement.argument;
|
|
8672
8710
|
}
|
|
8711
|
+
const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
|
|
8673
8712
|
walkAst(callback, (child) => {
|
|
8674
|
-
if (child
|
|
8713
|
+
if (child !== callback && isFunctionLike$1(child)) {
|
|
8714
|
+
if (child === cleanupArgument) return false;
|
|
8715
|
+
if (!effectInvokedFunctions.has(child) && !isSynchronousIteratorCallback(child)) return false;
|
|
8716
|
+
}
|
|
8675
8717
|
if (isSocketConstruction(child)) {
|
|
8676
8718
|
usages.push({
|
|
8677
8719
|
kind: "socket",
|
|
8678
8720
|
node: child,
|
|
8679
|
-
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8721
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket",
|
|
8722
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
8723
|
+
receiverKey: null,
|
|
8724
|
+
registrationVerbName: null,
|
|
8725
|
+
eventKey: null,
|
|
8726
|
+
handlerKey: null
|
|
8680
8727
|
});
|
|
8681
8728
|
return;
|
|
8682
8729
|
}
|
|
@@ -8685,118 +8732,348 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
8685
8732
|
usages.push({
|
|
8686
8733
|
kind: "timer",
|
|
8687
8734
|
node: child,
|
|
8688
|
-
resourceName: child.callee.name
|
|
8735
|
+
resourceName: child.callee.name,
|
|
8736
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
8737
|
+
receiverKey: null,
|
|
8738
|
+
registrationVerbName: child.callee.name,
|
|
8739
|
+
eventKey: null,
|
|
8740
|
+
handlerKey: null
|
|
8689
8741
|
});
|
|
8690
8742
|
return;
|
|
8691
8743
|
}
|
|
8692
|
-
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME))
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
const bindings = {
|
|
8702
|
-
cleanupFunctionNames: /* @__PURE__ */ new Set(),
|
|
8703
|
-
subscriptionNames: /* @__PURE__ */ new Set(),
|
|
8704
|
-
effectScopeVariableNames: /* @__PURE__ */ new Set()
|
|
8705
|
-
};
|
|
8706
|
-
if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
|
|
8707
|
-
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
|
|
8708
|
-
walkAst(effectCallback.body, (child) => {
|
|
8709
|
-
if (!isSubscribeOrObserveCall(child)) return;
|
|
8710
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8711
|
-
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
8712
|
-
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
8713
|
-
bindings.subscriptionNames.add(child.callee.object.name);
|
|
8714
|
-
});
|
|
8715
|
-
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
8716
|
-
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
8717
|
-
for (const declarator of child.declarations ?? []) {
|
|
8718
|
-
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
8719
|
-
const bindingName = declarator.id.name;
|
|
8720
|
-
bindings.effectScopeVariableNames.add(bindingName);
|
|
8721
|
-
const init = declarator.init;
|
|
8722
|
-
if (!init) continue;
|
|
8723
|
-
if (isSocketConstruction(init)) {
|
|
8724
|
-
bindings.subscriptionNames.add(bindingName);
|
|
8725
|
-
continue;
|
|
8726
|
-
}
|
|
8727
|
-
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
8728
|
-
if (isSubscribeLikeCallExpression(init)) {
|
|
8729
|
-
bindings.subscriptionNames.add(bindingName);
|
|
8730
|
-
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
8731
|
-
}
|
|
8732
|
-
}
|
|
8733
|
-
});
|
|
8734
|
-
walkAst(effectCallback.body, (child) => {
|
|
8735
|
-
if (child !== effectCallback.body && (isNodeOfType(child, "ArrowFunctionExpression") || isNodeOfType(child, "FunctionExpression"))) return false;
|
|
8736
|
-
if (isNodeOfType(child, "FunctionDeclaration") && child.id && isCleanupFunctionLike(child, bindings.cleanupFunctionNames, bindings.subscriptionNames)) {
|
|
8737
|
-
bindings.cleanupFunctionNames.add(child.id.name);
|
|
8738
|
-
return false;
|
|
8739
|
-
}
|
|
8740
|
-
});
|
|
8741
|
-
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
8742
|
-
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
8743
|
-
for (const declarator of child.declarations ?? []) {
|
|
8744
|
-
if (!isNodeOfType(declarator.id, "Identifier") || !declarator.init) continue;
|
|
8745
|
-
if (isCleanupFunctionLike(declarator.init, bindings.cleanupFunctionNames, bindings.subscriptionNames)) bindings.cleanupFunctionNames.add(declarator.id.name);
|
|
8744
|
+
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) {
|
|
8745
|
+
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
8746
|
+
usages.push({
|
|
8747
|
+
kind: "subscribe",
|
|
8748
|
+
node: child,
|
|
8749
|
+
resourceName: child.callee.property.name,
|
|
8750
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
8751
|
+
...registrationDetails
|
|
8752
|
+
});
|
|
8746
8753
|
}
|
|
8747
8754
|
});
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
const
|
|
8755
|
+
return usages.filter((usage) => isNodeReachableWithinFunction(usage.node, context));
|
|
8756
|
+
};
|
|
8757
|
+
const isNodeReachableWithinFunction = (node, context) => {
|
|
8758
|
+
const owner = context.cfg.enclosingFunction(node);
|
|
8759
|
+
if (!owner) return true;
|
|
8760
|
+
const functionCfg = context.cfg.cfgFor(owner);
|
|
8761
|
+
if (!functionCfg) return true;
|
|
8762
|
+
const targetBlock = functionCfg.blockOf(node);
|
|
8763
|
+
if (!targetBlock) return true;
|
|
8764
|
+
const visitedBlocks = new Set([functionCfg.entry]);
|
|
8765
|
+
const pendingBlocks = [functionCfg.entry];
|
|
8766
|
+
while (pendingBlocks.length > 0) {
|
|
8767
|
+
const currentBlock = pendingBlocks.pop();
|
|
8768
|
+
if (!currentBlock) break;
|
|
8769
|
+
if (currentBlock === targetBlock) return true;
|
|
8770
|
+
for (const edge of currentBlock.successors) {
|
|
8771
|
+
if (visitedBlocks.has(edge.to)) continue;
|
|
8772
|
+
visitedBlocks.add(edge.to);
|
|
8773
|
+
pendingBlocks.push(edge.to);
|
|
8774
|
+
}
|
|
8775
|
+
}
|
|
8776
|
+
return false;
|
|
8777
|
+
};
|
|
8778
|
+
const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, context) => {
|
|
8779
|
+
let pathAnchor = usageNode;
|
|
8780
|
+
let pathOwner = findEnclosingFunction(pathAnchor);
|
|
8781
|
+
while (pathOwner && isSynchronousIteratorCallback(pathOwner)) {
|
|
8782
|
+
if (matchingNodes.length > 0 && matchingNodes.every((matchingNode) => context.cfg.enclosingFunction(matchingNode) === pathOwner)) break;
|
|
8783
|
+
const iteratorCall = pathOwner.parent;
|
|
8784
|
+
if (!isNodeOfType(iteratorCall, "CallExpression")) break;
|
|
8785
|
+
pathAnchor = iteratorCall;
|
|
8786
|
+
pathOwner = findEnclosingFunction(pathAnchor);
|
|
8787
|
+
}
|
|
8788
|
+
const owner = context.cfg.enclosingFunction(pathAnchor);
|
|
8789
|
+
if (!owner) return false;
|
|
8790
|
+
const functionCfg = context.cfg.cfgFor(owner);
|
|
8791
|
+
if (!functionCfg) return false;
|
|
8792
|
+
const usageBlock = functionCfg.blockOf(pathAnchor);
|
|
8793
|
+
if (!usageBlock) return false;
|
|
8794
|
+
const usageStart = getRangeStart(usageNode);
|
|
8795
|
+
const matchingBlocks = new Set(matchingNodes.flatMap((matchingNode) => {
|
|
8796
|
+
if (context.cfg.enclosingFunction(matchingNode) !== owner) return [];
|
|
8797
|
+
const matchingBlock = functionCfg.blockOf(matchingNode);
|
|
8798
|
+
if (!matchingBlock) return [];
|
|
8799
|
+
const matchingStart = getRangeStart(matchingNode);
|
|
8800
|
+
if (matchingBlock === usageBlock && usageStart !== null && matchingStart !== null && matchingStart < usageStart) return [];
|
|
8801
|
+
return [matchingBlock];
|
|
8802
|
+
}));
|
|
8803
|
+
if (matchingBlocks.has(usageBlock)) return true;
|
|
8804
|
+
const visitedBlocks = new Set([usageBlock]);
|
|
8805
|
+
const pendingBlocks = [usageBlock];
|
|
8806
|
+
while (pendingBlocks.length > 0) {
|
|
8807
|
+
const currentBlock = pendingBlocks.pop();
|
|
8808
|
+
if (!currentBlock) break;
|
|
8809
|
+
for (const edge of currentBlock.successors) {
|
|
8810
|
+
if (matchingBlocks.has(edge.to)) continue;
|
|
8811
|
+
if (edge.to === functionCfg.exit) return false;
|
|
8812
|
+
if (visitedBlocks.has(edge.to)) continue;
|
|
8813
|
+
visitedBlocks.add(edge.to);
|
|
8814
|
+
pendingBlocks.push(edge.to);
|
|
8815
|
+
}
|
|
8816
|
+
}
|
|
8817
|
+
return matchingBlocks.size > 0;
|
|
8818
|
+
};
|
|
8819
|
+
const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
|
|
8754
8820
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8755
8821
|
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
8756
|
-
const
|
|
8822
|
+
const releaseCalls = [];
|
|
8757
8823
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8758
|
-
if (!
|
|
8759
|
-
|
|
8760
|
-
if (releaseStart !== null) releaseStarts.push(releaseStart);
|
|
8824
|
+
if (!isNodeOfType(isNodeOfType(child, "ChainExpression") ? child.expression : child, "CallExpression")) return;
|
|
8825
|
+
releaseCalls.push(child);
|
|
8761
8826
|
});
|
|
8762
|
-
if (
|
|
8827
|
+
if (releaseCalls.length === 0) return usages;
|
|
8763
8828
|
return usages.filter((usage) => {
|
|
8764
8829
|
const usageStart = getRangeStart(usage.node);
|
|
8765
8830
|
if (usageStart === null) return true;
|
|
8766
|
-
|
|
8831
|
+
const matchingReleaseCalls = releaseCalls.filter((releaseCall) => {
|
|
8832
|
+
const releaseStart = getRangeStart(releaseCall);
|
|
8833
|
+
return releaseStart !== null && releaseStart > usageStart && doesReleaseCallMatchUsage(releaseCall, usage, context);
|
|
8834
|
+
});
|
|
8835
|
+
return !doMatchingNodesCoverEveryPathAfterUsage(usage.node, matchingReleaseCalls, context);
|
|
8767
8836
|
});
|
|
8768
8837
|
};
|
|
8769
|
-
const
|
|
8770
|
-
if (
|
|
8771
|
-
const
|
|
8772
|
-
if (
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8838
|
+
const resolveIteratorCollectionKey = (expression, context) => {
|
|
8839
|
+
if (!expression || !isNodeOfType(expression, "Identifier")) return null;
|
|
8840
|
+
const symbol = context.scopes.symbolFor(expression);
|
|
8841
|
+
if (!symbol || symbol.kind !== "parameter") return null;
|
|
8842
|
+
let callbackNode = symbol.bindingIdentifier.parent;
|
|
8843
|
+
while (callbackNode && !isFunctionLike$1(callbackNode)) callbackNode = callbackNode.parent;
|
|
8844
|
+
if (!callbackNode || !isFunctionLike$1(callbackNode)) return null;
|
|
8845
|
+
const callNode = callbackNode.parent;
|
|
8846
|
+
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8847
|
+
const callee = stripParenExpression(callNode.callee);
|
|
8848
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
|
|
8849
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from" && callNode.arguments?.[1] === callbackNode) return resolveExpressionKey$1(callNode.arguments[0], context);
|
|
8850
|
+
if (callNode.arguments?.[0] === callbackNode) return resolveExpressionKey$1(callee.object, context);
|
|
8851
|
+
}
|
|
8852
|
+
return null;
|
|
8853
|
+
};
|
|
8854
|
+
const findCollectionMappingCall = (callbackNode) => {
|
|
8855
|
+
if (!isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression") || callbackNode.async || callbackNode.generator) return null;
|
|
8856
|
+
const callNode = callbackNode.parent;
|
|
8857
|
+
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8858
|
+
const callee = stripParenExpression(callNode.callee);
|
|
8859
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return null;
|
|
8860
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from" && callNode.arguments?.[1] === callbackNode) return callNode;
|
|
8861
|
+
return callee.property.name === "map" && callNode.arguments?.[0] === callbackNode ? callNode : null;
|
|
8862
|
+
};
|
|
8863
|
+
const findMappedResourceCollectionKey = (resourceNode, context) => {
|
|
8864
|
+
const callbackNode = findEnclosingFunction(resourceNode);
|
|
8865
|
+
if (!callbackNode || !isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression")) return null;
|
|
8866
|
+
const mappingCall = findCollectionMappingCall(callbackNode);
|
|
8867
|
+
if (!mappingCall) return null;
|
|
8868
|
+
if (isNodeOfType(callbackNode.body, "BlockStatement")) {
|
|
8869
|
+
const resourceRoot = findTransparentExpressionRoot(resourceNode);
|
|
8870
|
+
const resourceDeclarator = resourceRoot.parent;
|
|
8871
|
+
const resourceDeclaration = resourceDeclarator?.parent;
|
|
8872
|
+
if (!isNodeOfType(resourceDeclarator, "VariableDeclarator") || resourceDeclarator.init !== resourceRoot || !isNodeOfType(resourceDeclarator.id, "Identifier") || !isNodeOfType(resourceDeclaration, "VariableDeclaration") || resourceDeclaration.kind !== "const" || resourceDeclaration.parent !== callbackNode.body) return null;
|
|
8873
|
+
const returnStatements = [];
|
|
8874
|
+
walkAst(callbackNode.body, (child) => {
|
|
8875
|
+
if (child !== callbackNode.body && isFunctionLike$1(child)) return false;
|
|
8876
|
+
if (isNodeOfType(child, "ReturnStatement")) returnStatements.push(child);
|
|
8877
|
+
});
|
|
8878
|
+
const returnStatement = returnStatements[0];
|
|
8879
|
+
const callbackStatements = callbackNode.body.body ?? [];
|
|
8880
|
+
const returnedIdentifier = isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument ? stripParenExpression(returnStatement.argument) : null;
|
|
8881
|
+
const resourceSymbol = context.scopes.symbolFor(resourceDeclarator.id);
|
|
8882
|
+
if (returnStatements.length !== 1 || callbackStatements[callbackStatements.length - 1] !== returnStatement || !isNodeOfType(returnedIdentifier, "Identifier") || !resourceSymbol || context.scopes.symbolFor(returnedIdentifier)?.id !== resourceSymbol.id || !doMatchingNodesCoverEveryPathAfterUsage(resourceNode, [returnStatement], context)) return null;
|
|
8883
|
+
} else if (findTransparentExpressionRoot(resourceNode) !== callbackNode.body) return null;
|
|
8884
|
+
const mappingRoot = findTransparentExpressionRoot(mappingCall);
|
|
8885
|
+
const collectionDeclarator = mappingRoot.parent;
|
|
8886
|
+
return isNodeOfType(collectionDeclarator, "VariableDeclarator") && collectionDeclarator.init === mappingRoot ? resolveExpressionKey$1(collectionDeclarator.id, context) : null;
|
|
8887
|
+
};
|
|
8888
|
+
const findContainingCollectionKey = (resourceNode, context) => {
|
|
8889
|
+
const mappedCollectionKey = findMappedResourceCollectionKey(resourceNode, context);
|
|
8890
|
+
if (mappedCollectionKey !== null) return mappedCollectionKey;
|
|
8891
|
+
let currentNode = resourceNode;
|
|
8892
|
+
let parentNode = currentNode.parent;
|
|
8893
|
+
while (parentNode) {
|
|
8894
|
+
if (isFunctionLike$1(parentNode)) return null;
|
|
8895
|
+
if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode) return resolveExpressionKey$1(parentNode.id, context);
|
|
8896
|
+
currentNode = parentNode;
|
|
8897
|
+
parentNode = currentNode.parent;
|
|
8898
|
+
}
|
|
8899
|
+
return null;
|
|
8900
|
+
};
|
|
8901
|
+
const isWithinAssignmentTarget = (identifier) => {
|
|
8902
|
+
let currentNode = identifier;
|
|
8903
|
+
let parentNode = currentNode.parent;
|
|
8904
|
+
while (parentNode) {
|
|
8905
|
+
if (isNodeOfType(parentNode, "AssignmentExpression")) return parentNode.left === currentNode;
|
|
8906
|
+
if (isNodeOfType(parentNode, "UpdateExpression") || isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "delete") return parentNode.argument === currentNode;
|
|
8907
|
+
if (isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return parentNode.left === currentNode;
|
|
8908
|
+
currentNode = parentNode;
|
|
8909
|
+
parentNode = currentNode.parent;
|
|
8910
|
+
}
|
|
8911
|
+
return false;
|
|
8912
|
+
};
|
|
8913
|
+
const resolveStableValue = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
8914
|
+
if (!expression) return null;
|
|
8915
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
8916
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return unwrappedExpression;
|
|
8917
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
8918
|
+
const isUnreassignedMutableBinding = (symbol?.kind === "let" || symbol?.kind === "var") && isNodeOfType(symbol.declarationNode, "VariableDeclarator") && symbol.declarationNode.id === symbol.bindingIdentifier && symbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier)) && symbol.scope.symbols.filter((candidate) => candidate.name === symbol.name).length === 1;
|
|
8919
|
+
const recursiveFunctionSymbol = symbol?.kind === "function" && isFunctionLike$1(symbol.declarationNode) ? context.scopes.ownScopeFor(symbol.declarationNode)?.symbols.find((candidate) => candidate.name === symbol.name && candidate.declarationNode === symbol.declarationNode) : null;
|
|
8920
|
+
const isUnreassignedFunctionBinding = symbol?.kind === "function" && symbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier)) && (!recursiveFunctionSymbol || recursiveFunctionSymbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier))) && symbol.scope.symbols.filter((candidate) => candidate.name === symbol.name).length === 1;
|
|
8921
|
+
if (!symbol || symbol.kind !== "const" && !isUnreassignedMutableBinding && !isUnreassignedFunctionBinding || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return unwrappedExpression;
|
|
8922
|
+
visitedSymbolIds.add(symbol.id);
|
|
8923
|
+
return resolveStableValue(symbol.initializer, context, visitedSymbolIds);
|
|
8924
|
+
};
|
|
8925
|
+
const resolveObjectExpression = (expression, context) => {
|
|
8926
|
+
const resolvedExpression = resolveStableValue(expression, context);
|
|
8927
|
+
return isNodeOfType(resolvedExpression, "ObjectExpression") ? resolvedExpression : null;
|
|
8928
|
+
};
|
|
8929
|
+
const getListenerAbortControllerKey = (usage, context) => {
|
|
8930
|
+
if (usage.registrationVerbName !== "addEventListener" || !isNodeOfType(usage.node, "CallExpression")) return null;
|
|
8931
|
+
const optionsArgument = usage.node.arguments?.[2];
|
|
8932
|
+
const optionsObject = resolveObjectExpression(optionsArgument, context);
|
|
8933
|
+
if (!optionsObject) return null;
|
|
8934
|
+
for (const property of optionsObject.properties ?? []) {
|
|
8935
|
+
if (!isNodeOfType(property, "Property") || getStaticPropertyKeyName(property) !== "signal") continue;
|
|
8936
|
+
const signalKey = resolveExpressionKey$1(property.value, context);
|
|
8937
|
+
return signalKey?.endsWith(".signal") ? signalKey.slice(0, -7) : null;
|
|
8938
|
+
}
|
|
8939
|
+
return null;
|
|
8940
|
+
};
|
|
8941
|
+
const SYNCHRONOUS_ITERATOR_METHOD_NAMES$1 = new Set([
|
|
8942
|
+
"every",
|
|
8943
|
+
"filter",
|
|
8944
|
+
"flatMap",
|
|
8945
|
+
"forEach",
|
|
8946
|
+
"map",
|
|
8947
|
+
"reduce",
|
|
8948
|
+
"reduceRight",
|
|
8949
|
+
"some"
|
|
8950
|
+
]);
|
|
8951
|
+
const isSynchronousIteratorCallback = (functionNode) => {
|
|
8952
|
+
const callNode = functionNode.parent;
|
|
8953
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
8954
|
+
const callee = stripParenExpression(callNode.callee);
|
|
8955
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
8956
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from") return callNode.arguments?.[1] === functionNode;
|
|
8957
|
+
return SYNCHRONOUS_ITERATOR_METHOD_NAMES$1.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
|
|
8958
|
+
};
|
|
8959
|
+
const findDirectCallForReference = (identifier) => {
|
|
8960
|
+
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
8961
|
+
const callNode = expressionRoot.parent;
|
|
8962
|
+
return isNodeOfType(callNode, "CallExpression") && callNode.callee === expressionRoot ? callNode : null;
|
|
8963
|
+
};
|
|
8964
|
+
const findSingleDirectInvocation = (functionNode, caller, context) => {
|
|
8965
|
+
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
8966
|
+
if (!bindingIdentifier || resolveStableValue(bindingIdentifier, context) !== functionNode) return null;
|
|
8967
|
+
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
8968
|
+
if (!symbol) return null;
|
|
8969
|
+
const invocationCalls = symbol.references.flatMap((reference) => {
|
|
8970
|
+
const callNode = findDirectCallForReference(reference.identifier);
|
|
8971
|
+
return callNode ? [callNode] : [];
|
|
8972
|
+
});
|
|
8973
|
+
if (invocationCalls.length !== 1) return null;
|
|
8974
|
+
const invocationCall = invocationCalls[0];
|
|
8975
|
+
return findEnclosingFunction(invocationCall) === caller && isNodeReachableWithinFunction(invocationCall, context) ? invocationCall : null;
|
|
8976
|
+
};
|
|
8977
|
+
const resolveCleanupPathAnchor = (usageNode, effectCallback, context) => {
|
|
8978
|
+
const usageFunction = findEnclosingFunction(usageNode);
|
|
8979
|
+
if (!usageFunction || usageFunction === effectCallback) return usageNode;
|
|
8980
|
+
return findSingleDirectInvocation(usageFunction, effectCallback, context) ?? usageNode;
|
|
8981
|
+
};
|
|
8982
|
+
const resolveSingleAssignedCleanupFunction = (expression, usage, context) => {
|
|
8983
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
8984
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
|
|
8985
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
8986
|
+
const initializer = symbol?.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
8987
|
+
if (!symbol || symbol.kind !== "let" && symbol.kind !== "var" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || !isNodeOfType(initializer, "Literal") || initializer.value !== null || symbol.scope.symbols.filter((candidate) => candidate.name === symbol.name).length !== 1) return null;
|
|
8988
|
+
const assignmentReferences = symbol.references.filter((reference) => isWithinAssignmentTarget(reference.identifier));
|
|
8989
|
+
if (assignmentReferences.length !== 1) return null;
|
|
8990
|
+
const assignmentReference = assignmentReferences[0];
|
|
8991
|
+
const assignmentTarget = findTransparentExpressionRoot(assignmentReference.identifier);
|
|
8992
|
+
const assignmentNode = assignmentTarget.parent;
|
|
8993
|
+
if (!isNodeOfType(assignmentNode, "AssignmentExpression") || assignmentNode.operator !== "=" || assignmentNode.left !== assignmentTarget || findEnclosingFunction(assignmentNode) !== findEnclosingFunction(usage.node) || !doMatchingNodesCoverEveryPathAfterUsage(usage.node, [assignmentNode], context)) return null;
|
|
8994
|
+
const assignedValue = stripParenExpression(assignmentNode.right);
|
|
8995
|
+
return isFunctionLike$1(assignedValue) ? assignedValue : null;
|
|
8996
|
+
};
|
|
8997
|
+
const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visitedFunctions = /* @__PURE__ */ new Set()) => {
|
|
8998
|
+
if (!isFunctionLike$1(cleanupFunction) || visitedFunctions.has(cleanupFunction)) return false;
|
|
8999
|
+
visitedFunctions.add(cleanupFunction);
|
|
9000
|
+
let didCleanupFunctionMatch = false;
|
|
9001
|
+
walkAst(cleanupFunction.body, (cleanupChild) => {
|
|
9002
|
+
if (didCleanupFunctionMatch) return false;
|
|
9003
|
+
if (cleanupChild !== cleanupFunction.body && isFunctionLike$1(cleanupChild) && !isSynchronousIteratorCallback(cleanupChild)) return false;
|
|
9004
|
+
if (doesReleaseCallMatchUsage(cleanupChild, usage, context)) {
|
|
9005
|
+
didCleanupFunctionMatch = true;
|
|
9006
|
+
return false;
|
|
9007
|
+
}
|
|
9008
|
+
const helperCall = isNodeOfType(cleanupChild, "ChainExpression") ? cleanupChild.expression : cleanupChild;
|
|
9009
|
+
if (!isNodeOfType(helperCall, "CallExpression")) return;
|
|
9010
|
+
const stableHelperFunction = resolveStableValue(helperCall.callee, context);
|
|
9011
|
+
const helperFunction = isNodeOfType(stableHelperFunction, "Identifier") ? resolveSingleAssignedCleanupFunction(stableHelperFunction, usage, context) : stableHelperFunction;
|
|
9012
|
+
if (helperFunction && isFunctionLike$1(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context, visitedFunctions)) {
|
|
9013
|
+
didCleanupFunctionMatch = true;
|
|
9014
|
+
return false;
|
|
9015
|
+
}
|
|
8776
9016
|
});
|
|
9017
|
+
return didCleanupFunctionMatch;
|
|
8777
9018
|
};
|
|
8778
|
-
const
|
|
9019
|
+
const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
8779
9020
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
8780
|
-
if (
|
|
8781
|
-
|
|
8782
|
-
|
|
9021
|
+
if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
9022
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return callback.body === usage.node && isCleanupReturningSubscribeLikeCallExpression(callback.body);
|
|
9023
|
+
const matchingCleanupReturns = [];
|
|
8783
9024
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8784
|
-
if (didFindCleanupReturn) return;
|
|
8785
9025
|
if (!isNodeOfType(child, "ReturnStatement")) return;
|
|
8786
|
-
|
|
8787
|
-
|
|
9026
|
+
const returnStart = getRangeStart(child);
|
|
9027
|
+
const usageStart = getRangeStart(usage.node);
|
|
9028
|
+
if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
|
|
9029
|
+
const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
|
|
9030
|
+
if (!returnedValue) return;
|
|
9031
|
+
if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
|
|
9032
|
+
matchingCleanupReturns.push(child);
|
|
9033
|
+
return;
|
|
9034
|
+
}
|
|
9035
|
+
if (usage.kind === "subscribe" && isNodeOfType(returnedValue, "Identifier") && usage.handleKey !== null && resolveExpressionKey$1(returnedValue, context) === usage.handleKey && isCleanupReturningSubscribeLikeCallExpression(usage.node)) {
|
|
9036
|
+
matchingCleanupReturns.push(child);
|
|
9037
|
+
return;
|
|
9038
|
+
}
|
|
9039
|
+
if (isNodeOfType(returnedValue, "Identifier")) {
|
|
9040
|
+
if (returnedValue.name === "undefined" && context.scopes.isGlobalReference(returnedValue)) return;
|
|
9041
|
+
const returnedKey = resolveExpressionKey$1(returnedValue, context);
|
|
9042
|
+
if (usage.handleKey !== null && returnedKey === usage.handleKey) return;
|
|
9043
|
+
if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
|
|
9044
|
+
}
|
|
9045
|
+
const cleanupFunction = resolveStableValue(returnedValue, context);
|
|
9046
|
+
if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
|
|
9047
|
+
if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
|
|
8788
9048
|
});
|
|
8789
|
-
return
|
|
9049
|
+
return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
|
|
9050
|
+
};
|
|
9051
|
+
const findFirstUsageWithoutCleanup = (callback, usages, context) => {
|
|
9052
|
+
for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context)) return usage;
|
|
9053
|
+
return null;
|
|
9054
|
+
};
|
|
9055
|
+
const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
9056
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
9057
|
+
if (isNodeOfType(unwrappedExpression, "NewExpression") && isNodeOfType(unwrappedExpression.callee, "Identifier") && unwrappedExpression.callee.name === "AbortController") return true;
|
|
9058
|
+
if (isNodeOfType(unwrappedExpression, "MemberExpression")) return isLocalAbortControllerExpression(unwrappedExpression.object, context, visitedSymbolIds);
|
|
9059
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return false;
|
|
9060
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
9061
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
9062
|
+
visitedSymbolIds.add(symbol.id);
|
|
9063
|
+
if (symbol.initializer) return isLocalAbortControllerExpression(symbol.initializer, context, visitedSymbolIds);
|
|
9064
|
+
const bindingProperty = symbol.bindingIdentifier.parent;
|
|
9065
|
+
const bindingPattern = bindingProperty?.parent;
|
|
9066
|
+
const variableDeclarator = bindingPattern?.parent;
|
|
9067
|
+
return Boolean(isNodeOfType(bindingProperty, "Property") && isNodeOfType(bindingPattern, "ObjectPattern") && isNodeOfType(variableDeclarator, "VariableDeclarator") && variableDeclarator.init && isLocalAbortControllerExpression(variableDeclarator.init, context, visitedSymbolIds));
|
|
8790
9068
|
};
|
|
8791
|
-
const
|
|
8792
|
-
const isSelfReleasingListenerOptionProperty = (property) => {
|
|
9069
|
+
const isSelfReleasingListenerOptionProperty = (property, context) => {
|
|
8793
9070
|
if (!isNodeOfType(property, "Property")) return false;
|
|
8794
9071
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
|
|
8795
|
-
if (keyName === "signal") return
|
|
9072
|
+
if (keyName === "signal") return !isLocalAbortControllerExpression(property.value, context);
|
|
8796
9073
|
if (keyName !== "once") return false;
|
|
8797
9074
|
return isNodeOfType(property.value, "Literal") && property.value.value === true;
|
|
8798
9075
|
};
|
|
8799
|
-
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
|
|
9076
|
+
const hasSelfReleasingListenerOptions = (node, context) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some((property) => isSelfReleasingListenerOptionProperty(property, context)));
|
|
8800
9077
|
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
8801
9078
|
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
8802
9079
|
["addListener", new Set([
|
|
@@ -8822,85 +9099,185 @@ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
|
8822
9099
|
"teardown"
|
|
8823
9100
|
]);
|
|
8824
9101
|
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8825
|
-
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
8826
9102
|
const getReleaseVerbName = (node) => {
|
|
8827
|
-
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
8828
9103
|
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8829
9104
|
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8830
9105
|
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
8831
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8832
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier"))
|
|
9106
|
+
if (isNodeOfType(callee, "Identifier")) return TIMER_CLEANUP_CALLEE_NAMES.has(callee.name) || GLOBAL_RELEASE_METHOD_NAMES.has(callee.name) || UNIVERSAL_RELEASE_VERB_NAMES.has(callee.name) ? callee.name : null;
|
|
9107
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
9108
|
+
const methodName = callee.property.name;
|
|
9109
|
+
return GLOBAL_RELEASE_METHOD_NAMES.has(methodName) || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(methodName) || methodName === "on" ? methodName : null;
|
|
9110
|
+
}
|
|
8833
9111
|
return null;
|
|
8834
9112
|
};
|
|
9113
|
+
const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
9114
|
+
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
9115
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
9116
|
+
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
9117
|
+
if (usage.kind === "timer") {
|
|
9118
|
+
const expectedCleanupName = usage.registrationVerbName === "setInterval" ? "clearInterval" : "clearTimeout";
|
|
9119
|
+
if (!isNodeOfType(callee, "Identifier") || !TIMER_CLEANUP_CALLEE_NAMES.has(callee.name) || callee.name !== expectedCleanupName) return false;
|
|
9120
|
+
if (usage.handleKey !== null && resolveExpressionKey$1(callNode.arguments?.[0], context) === usage.handleKey) return true;
|
|
9121
|
+
const collectionKey = findContainingCollectionKey(usage.node, context);
|
|
9122
|
+
return collectionKey !== null && collectionKey === resolveIteratorCollectionKey(callNode.arguments?.[0], context);
|
|
9123
|
+
}
|
|
9124
|
+
if (isNodeOfType(callee, "Identifier") && usage.kind === "subscribe" && usage.handleKey !== null && resolveExpressionKey$1(callee, context) === usage.handleKey && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
9125
|
+
const releaseVerbName = getReleaseVerbName(callNode);
|
|
9126
|
+
if (!releaseVerbName) return false;
|
|
9127
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
9128
|
+
const releaseReceiverKey = resolveExpressionKey$1(callee.object, context);
|
|
9129
|
+
if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
|
|
9130
|
+
if (usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName))) return true;
|
|
9131
|
+
if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
|
|
9132
|
+
if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
|
|
9133
|
+
const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
9134
|
+
if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
|
|
9135
|
+
const releaseEventKey = resolveExpressionKey$1(callNode.arguments?.[0], context);
|
|
9136
|
+
if (usage.eventKey !== null && releaseEventKey !== null && usage.eventKey !== releaseEventKey) {
|
|
9137
|
+
const usageIteratorCollectionKey = isNodeOfType(usage.node, "CallExpression") ? resolveIteratorCollectionKey(usage.node.arguments?.[0], context) : null;
|
|
9138
|
+
const releaseIteratorCollectionKey = resolveIteratorCollectionKey(callNode.arguments?.[0], context);
|
|
9139
|
+
if (usageIteratorCollectionKey === null || usageIteratorCollectionKey !== releaseIteratorCollectionKey) return false;
|
|
9140
|
+
}
|
|
9141
|
+
if (releaseVerbName === "on") {
|
|
9142
|
+
const handlerArgument = callNode.arguments?.[1];
|
|
9143
|
+
return isNodeOfType(handlerArgument, "Literal") && handlerArgument.value === null;
|
|
9144
|
+
}
|
|
9145
|
+
if (releaseVerbName === "removeEventListener" || releaseVerbName === "removeListener" || releaseVerbName === "off") {
|
|
9146
|
+
const releaseHandler = callNode.arguments?.[1];
|
|
9147
|
+
if (!releaseHandler) return releaseVerbName === "off";
|
|
9148
|
+
return usage.handlerKey !== null && resolveExpressionKey$1(releaseHandler, context) === usage.handlerKey;
|
|
9149
|
+
}
|
|
9150
|
+
if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
|
|
9151
|
+
return true;
|
|
9152
|
+
};
|
|
8835
9153
|
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
8836
|
-
const
|
|
8837
|
-
let
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
9154
|
+
const isReturnedEffectCleanupFunction = (functionNode) => {
|
|
9155
|
+
let currentNode = functionNode;
|
|
9156
|
+
let parentNode = currentNode.parent;
|
|
9157
|
+
while (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
|
|
9158
|
+
currentNode = parentNode;
|
|
9159
|
+
parentNode = currentNode.parent;
|
|
9160
|
+
}
|
|
9161
|
+
if (!isNodeOfType(parentNode, "ReturnStatement") || parentNode.argument !== currentNode) return false;
|
|
9162
|
+
const effectCallback = findEnclosingFunction(parentNode);
|
|
9163
|
+
const effectCall = effectCallback?.parent;
|
|
9164
|
+
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isHookCall$2(effectCall, CLEANUP_EFFECT_HOOK_NAMES));
|
|
9165
|
+
};
|
|
9166
|
+
const isPotentiallyReachableFunction = (functionNode, context) => {
|
|
9167
|
+
if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode)) return true;
|
|
9168
|
+
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
9169
|
+
if (!bindingIdentifier) return false;
|
|
9170
|
+
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
9171
|
+
if (!symbol) return false;
|
|
9172
|
+
return symbol.references.some((reference) => findEnclosingFunction(reference.identifier) !== functionNode);
|
|
9173
|
+
};
|
|
9174
|
+
const isReleaseReachableForUsage = (releaseNode, usage, context) => {
|
|
9175
|
+
if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
|
|
9176
|
+
const releaseFunction = findEnclosingFunction(releaseNode);
|
|
9177
|
+
if (!releaseFunction) return true;
|
|
9178
|
+
if (releaseFunction === findEnclosingFunction(usage.node)) return true;
|
|
9179
|
+
return isPotentiallyReachableFunction(releaseFunction, context);
|
|
8848
9180
|
};
|
|
8849
|
-
const
|
|
8850
|
-
|
|
8851
|
-
let programNode = anyNode;
|
|
9181
|
+
const fileContainsReleaseForUsage = (usage, context) => {
|
|
9182
|
+
let programNode = usage.node;
|
|
8852
9183
|
while (programNode.parent) programNode = programNode.parent;
|
|
8853
|
-
|
|
8854
|
-
if (cached) return cached;
|
|
8855
|
-
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
9184
|
+
let didFindRelease = false;
|
|
8856
9185
|
walkAst(programNode, (child) => {
|
|
8857
|
-
|
|
8858
|
-
if (
|
|
9186
|
+
if (didFindRelease) return false;
|
|
9187
|
+
if (doesReleaseCallMatchUsage(child, usage, context) && isReleaseReachableForUsage(child, usage, context)) {
|
|
9188
|
+
didFindRelease = true;
|
|
9189
|
+
return false;
|
|
9190
|
+
}
|
|
8859
9191
|
});
|
|
8860
|
-
|
|
8861
|
-
|
|
9192
|
+
return didFindRelease;
|
|
9193
|
+
};
|
|
9194
|
+
const isUseSyncExternalStoreSubscribeFunction = (functionNode, context) => {
|
|
9195
|
+
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
9196
|
+
if (!bindingIdentifier) return false;
|
|
9197
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
9198
|
+
const isSubscribeBinding = (candidateBinding) => {
|
|
9199
|
+
const symbol = context.scopes.symbolFor(candidateBinding);
|
|
9200
|
+
if (!symbol || visitedSymbolIds.has(symbol.id) || symbol.references.length === 0) return false;
|
|
9201
|
+
visitedSymbolIds.add(symbol.id);
|
|
9202
|
+
return symbol.references.every((reference) => {
|
|
9203
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
9204
|
+
const referenceParent = referenceRoot.parent;
|
|
9205
|
+
if (isNodeOfType(referenceParent, "CallExpression") && referenceParent.arguments?.[0] === referenceRoot) return isReactApiCall(referenceParent, "useSyncExternalStore", context.scopes);
|
|
9206
|
+
const aliasDeclaration = referenceParent?.parent;
|
|
9207
|
+
return Boolean(isNodeOfType(referenceParent, "VariableDeclarator") && referenceParent.init === referenceRoot && isNodeOfType(referenceParent.id, "Identifier") && isNodeOfType(aliasDeclaration, "VariableDeclaration") && aliasDeclaration.kind === "const" && isSubscribeBinding(referenceParent.id));
|
|
9208
|
+
});
|
|
9209
|
+
};
|
|
9210
|
+
return isSubscribeBinding(bindingIdentifier);
|
|
8862
9211
|
};
|
|
8863
|
-
const
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
9212
|
+
const doesResourceResultEscape = (resourceNode, allowConciseReturnEscape) => {
|
|
9213
|
+
let currentNode = resourceNode;
|
|
9214
|
+
let parentNode = currentNode.parent;
|
|
9215
|
+
while (parentNode) {
|
|
9216
|
+
if (isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode) return true;
|
|
9217
|
+
if (isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode && allowConciseReturnEscape) return true;
|
|
9218
|
+
if (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
|
|
9219
|
+
currentNode = parentNode;
|
|
9220
|
+
parentNode = currentNode.parent;
|
|
9221
|
+
continue;
|
|
9222
|
+
}
|
|
9223
|
+
return false;
|
|
9224
|
+
}
|
|
8868
9225
|
return false;
|
|
8869
9226
|
};
|
|
8870
|
-
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
9227
|
+
const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
8871
9228
|
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
8872
9229
|
const body = retainedFunction.body;
|
|
8873
9230
|
if (!body) return null;
|
|
8874
|
-
const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
|
|
8875
9231
|
let leak = null;
|
|
9232
|
+
const allowConciseReturnEscape = !isInlineRetainedHandlerFunction(retainedFunction, context);
|
|
9233
|
+
const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
|
|
9234
|
+
const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context);
|
|
8876
9235
|
walkAst(body, (child) => {
|
|
8877
9236
|
if (leak !== null) return false;
|
|
8878
9237
|
if (isFunctionLike$1(child)) return false;
|
|
8879
|
-
if (child
|
|
8880
|
-
|
|
8881
|
-
leak = {
|
|
9238
|
+
if (isSocketConstruction(child) && !doesResourceResultEscape(child, false)) {
|
|
9239
|
+
const socketUsage = {
|
|
8882
9240
|
kind: "socket",
|
|
8883
9241
|
node: child,
|
|
8884
|
-
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
9242
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket",
|
|
9243
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
9244
|
+
receiverKey: null,
|
|
9245
|
+
registrationVerbName: null,
|
|
9246
|
+
eventKey: null,
|
|
9247
|
+
handlerKey: null
|
|
8885
9248
|
};
|
|
8886
|
-
|
|
9249
|
+
if (!hasReleaseForUsage(socketUsage)) {
|
|
9250
|
+
leak = socketUsage;
|
|
9251
|
+
return false;
|
|
9252
|
+
}
|
|
8887
9253
|
}
|
|
8888
9254
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8889
|
-
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" &&
|
|
8890
|
-
|
|
9255
|
+
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && !doesResourceResultEscape(child, allowConciseReturnEscape)) {
|
|
9256
|
+
const timerUsage = {
|
|
8891
9257
|
kind: "timer",
|
|
8892
9258
|
node: child,
|
|
8893
|
-
resourceName: "setInterval"
|
|
9259
|
+
resourceName: "setInterval",
|
|
9260
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
9261
|
+
receiverKey: null,
|
|
9262
|
+
registrationVerbName: "setInterval",
|
|
9263
|
+
eventKey: null,
|
|
9264
|
+
handlerKey: null
|
|
8894
9265
|
};
|
|
8895
|
-
|
|
9266
|
+
if (!hasReleaseForUsage(timerUsage)) {
|
|
9267
|
+
leak = timerUsage;
|
|
9268
|
+
return false;
|
|
9269
|
+
}
|
|
8896
9270
|
}
|
|
8897
|
-
if (isSubscribeOrObserveCall(child) &&
|
|
8898
|
-
const
|
|
8899
|
-
|
|
9271
|
+
if (isSubscribeOrObserveCall(child) && !doesResourceResultEscape(child, allowConciseReturnEscape)) {
|
|
9272
|
+
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
9273
|
+
const subscriptionUsage = {
|
|
8900
9274
|
kind: "subscribe",
|
|
8901
9275
|
node: child,
|
|
8902
|
-
resourceName: registrationVerbName
|
|
9276
|
+
resourceName: registrationDetails.registrationVerbName ?? "subscribe",
|
|
9277
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
9278
|
+
...registrationDetails
|
|
8903
9279
|
};
|
|
9280
|
+
if (!hasSelfReleasingListenerOptions(child, context) && !hasReleaseForUsage(subscriptionUsage)) leak = subscriptionUsage;
|
|
8904
9281
|
return false;
|
|
8905
9282
|
}
|
|
8906
9283
|
});
|
|
@@ -8912,6 +9289,26 @@ const isRetainedComponentScopeFunction = (functionNode) => {
|
|
|
8912
9289
|
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
8913
9290
|
return enclosingComponentOrHookName(functionNode) !== null;
|
|
8914
9291
|
};
|
|
9292
|
+
const isDirectJsxEventHandlerValue = (expression) => {
|
|
9293
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
9294
|
+
const expressionContainer = expressionRoot.parent;
|
|
9295
|
+
return isNodeOfType(expressionContainer, "JSXExpressionContainer") && expressionContainer.expression === expressionRoot && isEventHandlerAttribute(expressionContainer.parent);
|
|
9296
|
+
};
|
|
9297
|
+
const isInlineRetainedHandlerFunction = (functionNode, context) => {
|
|
9298
|
+
if (!isFunctionLike$1(functionNode)) return false;
|
|
9299
|
+
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
9300
|
+
const callbackCall = functionRoot.parent;
|
|
9301
|
+
if (isNodeOfType(callbackCall, "CallExpression") && callbackCall.arguments?.[0] === functionRoot && isHookCall$2(callbackCall, "useCallback") && isDirectJsxEventHandlerValue(callbackCall)) return true;
|
|
9302
|
+
const parentNode = functionNode.parent;
|
|
9303
|
+
if (isDirectJsxEventHandlerValue(functionNode)) return true;
|
|
9304
|
+
if (!isNodeOfType(parentNode, "Property") || parentNode.value !== functionNode || parentNode.computed) return false;
|
|
9305
|
+
const propertyName = getStaticPropertyKeyName(parentNode);
|
|
9306
|
+
if (!propertyName || !/^on[A-Z]/.test(propertyName)) return false;
|
|
9307
|
+
const objectExpression = parentNode.parent;
|
|
9308
|
+
if (!isNodeOfType(objectExpression, "ObjectExpression")) return false;
|
|
9309
|
+
const objectParent = objectExpression.parent;
|
|
9310
|
+
return (isNodeOfType(objectParent, "CallExpression") && objectParent.arguments.some((argument) => argument === objectExpression) || isNodeOfType(objectParent, "JSXExpressionContainer")) && findRenderPhaseComponentOrHook(parentNode, context.scopes) !== null;
|
|
9311
|
+
};
|
|
8915
9312
|
const effectNeedsCleanup = defineRule({
|
|
8916
9313
|
id: "effect-needs-cleanup",
|
|
8917
9314
|
title: "Effect subscription or timer never cleaned up",
|
|
@@ -8920,7 +9317,8 @@ const effectNeedsCleanup = defineRule({
|
|
|
8920
9317
|
recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",
|
|
8921
9318
|
create: (context) => {
|
|
8922
9319
|
const reportRetainedLeak = (retainedFunction) => {
|
|
8923
|
-
|
|
9320
|
+
if (!isPotentiallyReachableFunction(retainedFunction, context)) return;
|
|
9321
|
+
const leak = findRetainedFunctionLeak(retainedFunction, context);
|
|
8924
9322
|
if (!leak) return;
|
|
8925
9323
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
8926
9324
|
context.report({
|
|
@@ -8932,30 +9330,31 @@ const effectNeedsCleanup = defineRule({
|
|
|
8932
9330
|
CallExpression(node) {
|
|
8933
9331
|
if (isHookCall$2(node, "useCallback")) {
|
|
8934
9332
|
const retainedCallback = getEffectCallback(node);
|
|
8935
|
-
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
9333
|
+
if (retainedCallback && !isInlineRetainedHandlerFunction(retainedCallback, context)) reportRetainedLeak(retainedCallback);
|
|
8936
9334
|
return;
|
|
8937
9335
|
}
|
|
8938
|
-
if (!isHookCall$2(node,
|
|
9336
|
+
if (!isHookCall$2(node, CLEANUP_EFFECT_HOOK_NAMES)) return;
|
|
8939
9337
|
const callback = getEffectCallback(node);
|
|
8940
9338
|
if (!callback) return;
|
|
8941
|
-
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
|
|
9339
|
+
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback, context), context);
|
|
8942
9340
|
if (usages.length === 0) return;
|
|
8943
|
-
|
|
8944
|
-
|
|
9341
|
+
const firstUsage = findFirstUsageWithoutCleanup(callback, usages, context);
|
|
9342
|
+
if (!firstUsage) return;
|
|
8945
9343
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
9344
|
+
const hookName = getCalleeName$2(node) ?? "effect";
|
|
8946
9345
|
context.report({
|
|
8947
9346
|
node,
|
|
8948
|
-
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in
|
|
9347
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in ${hookName} without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
8949
9348
|
});
|
|
8950
9349
|
},
|
|
8951
9350
|
FunctionDeclaration(node) {
|
|
8952
9351
|
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8953
9352
|
},
|
|
8954
9353
|
ArrowFunctionExpression(node) {
|
|
8955
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9354
|
+
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
|
|
8956
9355
|
},
|
|
8957
9356
|
FunctionExpression(node) {
|
|
8958
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9357
|
+
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
|
|
8959
9358
|
}
|
|
8960
9359
|
};
|
|
8961
9360
|
}
|
|
@@ -9569,13 +9968,18 @@ const unwrapExpression$3 = (node) => {
|
|
|
9569
9968
|
return current;
|
|
9570
9969
|
};
|
|
9571
9970
|
/**
|
|
9572
|
-
* Get the hook name from a
|
|
9573
|
-
*
|
|
9574
|
-
* `React.useFoo()` (MemberExpression).
|
|
9971
|
+
* Get the hook name from a direct, wrapped, namespaced, or immutable
|
|
9972
|
+
* React import alias call.
|
|
9575
9973
|
*/
|
|
9576
|
-
const getHookName = (callee) => {
|
|
9577
|
-
|
|
9578
|
-
if (isNodeOfType(
|
|
9974
|
+
const getHookName = (callee, scopes) => {
|
|
9975
|
+
const strippedCallee = unwrapExpression$3(callee);
|
|
9976
|
+
if (isNodeOfType(strippedCallee, "Identifier")) {
|
|
9977
|
+
const resolvedSymbol = scopes ? resolveConstIdentifierAlias(strippedCallee, scopes) : null;
|
|
9978
|
+
const importDeclaration = resolvedSymbol?.declarationNode.parent;
|
|
9979
|
+
if (resolvedSymbol?.kind === "import" && importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "react") return getImportedName(resolvedSymbol.declarationNode) ?? strippedCallee.name;
|
|
9980
|
+
return strippedCallee.name;
|
|
9981
|
+
}
|
|
9982
|
+
if (isNodeOfType(strippedCallee, "MemberExpression") && !strippedCallee.computed && isNodeOfType(strippedCallee.property, "Identifier")) return strippedCallee.property.name;
|
|
9579
9983
|
return null;
|
|
9580
9984
|
};
|
|
9581
9985
|
const FUNCTION_SCOPE_KINDS = new Set([
|
|
@@ -9760,7 +10164,7 @@ const STABLE_IDENTITY_WRAPPER_HOOK_NAMES = new Set([
|
|
|
9760
10164
|
* - primitive-literal local consts (the value never changes
|
|
9761
10165
|
* between renders unless the literal does)
|
|
9762
10166
|
*/
|
|
9763
|
-
const symbolHasStableHookOrigin = (symbol) => {
|
|
10167
|
+
const symbolHasStableHookOrigin = (symbol, scopes) => {
|
|
9764
10168
|
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
9765
10169
|
let declarator = symbol.declarationNode;
|
|
9766
10170
|
while (declarator && declarator.type !== "VariableDeclarator") declarator = declarator.parent ?? null;
|
|
@@ -9773,7 +10177,7 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
9773
10177
|
if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
|
|
9774
10178
|
}
|
|
9775
10179
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
9776
|
-
const initializerHookName = getHookName(initializer.callee);
|
|
10180
|
+
const initializerHookName = getHookName(initializer.callee, scopes);
|
|
9777
10181
|
if (!initializerHookName) return false;
|
|
9778
10182
|
if (initializerHookName === "useRef") return true;
|
|
9779
10183
|
if (initializerHookName === "useEffectEvent") return true;
|
|
@@ -9808,7 +10212,7 @@ const symbolHasStableMemoizedOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
9808
10212
|
if (!declarator.init) return false;
|
|
9809
10213
|
const initializer = unwrapExpression$3(declarator.init);
|
|
9810
10214
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
9811
|
-
const initializerHookName = getHookName(initializer.callee);
|
|
10215
|
+
const initializerHookName = getHookName(initializer.callee, scopes);
|
|
9812
10216
|
if (!initializerHookName || !MEMOIZING_HOOK_NAMES.has(initializerHookName)) return false;
|
|
9813
10217
|
const depsArgument = initializer.arguments[1];
|
|
9814
10218
|
if (!depsArgument || !isAstNode(depsArgument)) return false;
|
|
@@ -9838,7 +10242,7 @@ const getObjectPropertyValue = (objectExpression, propertyName) => {
|
|
|
9838
10242
|
}
|
|
9839
10243
|
return null;
|
|
9840
10244
|
};
|
|
9841
|
-
const isStableRefContainerCapture = (symbol, depKey) => {
|
|
10245
|
+
const isStableRefContainerCapture = (symbol, depKey, scopes) => {
|
|
9842
10246
|
if (symbol.kind !== "const") return false;
|
|
9843
10247
|
if (!depKey.startsWith(`${symbol.name}.`)) return false;
|
|
9844
10248
|
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
@@ -9852,7 +10256,7 @@ const isStableRefContainerCapture = (symbol, depKey) => {
|
|
|
9852
10256
|
if (!propertyValue) return false;
|
|
9853
10257
|
currentValue = propertyValue;
|
|
9854
10258
|
}
|
|
9855
|
-
return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee) === "useRef";
|
|
10259
|
+
return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee, scopes) === "useRef";
|
|
9856
10260
|
};
|
|
9857
10261
|
const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
9858
10262
|
if (visitedSymbolIds.has(symbol.id)) return true;
|
|
@@ -9870,7 +10274,13 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
9870
10274
|
}
|
|
9871
10275
|
return true;
|
|
9872
10276
|
};
|
|
9873
|
-
const
|
|
10277
|
+
const symbolHasStableImportedAlias = (symbol, scopes) => {
|
|
10278
|
+
if (symbol.kind !== "const") return false;
|
|
10279
|
+
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
10280
|
+
const resolvedSymbol = resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes);
|
|
10281
|
+
return resolvedSymbol !== null && resolvedSymbol !== symbol && resolvedSymbol.kind === "import";
|
|
10282
|
+
};
|
|
10283
|
+
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol, scopes) || symbolHasStableImportedAlias(symbol, scopes) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
9874
10284
|
//#endregion
|
|
9875
10285
|
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
9876
10286
|
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
@@ -10058,10 +10468,18 @@ const collectCaptureDepKeys = (callback, scopes) => {
|
|
|
10058
10468
|
}
|
|
10059
10469
|
const depKey = computeDepKey(reference);
|
|
10060
10470
|
if (!depKey) continue;
|
|
10061
|
-
if (isStableRefContainerCapture(symbol, depKey)) {
|
|
10471
|
+
if (isStableRefContainerCapture(symbol, depKey, scopes)) {
|
|
10062
10472
|
stableCapturedNames.add(depKey);
|
|
10063
10473
|
continue;
|
|
10064
10474
|
}
|
|
10475
|
+
if (depKey === symbol.name) {
|
|
10476
|
+
const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
10477
|
+
if (identitySourceKeys) {
|
|
10478
|
+
if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
|
|
10479
|
+
for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
|
|
10480
|
+
continue;
|
|
10481
|
+
}
|
|
10482
|
+
}
|
|
10065
10483
|
keys.add(depKey);
|
|
10066
10484
|
}
|
|
10067
10485
|
return {
|
|
@@ -10090,12 +10508,60 @@ const hasComputedMemberExpression = (node) => {
|
|
|
10090
10508
|
if (stripped.computed) return true;
|
|
10091
10509
|
return hasComputedMemberExpression(stripped.object);
|
|
10092
10510
|
};
|
|
10511
|
+
const mergeIdentitySourceKeys = (expressions, scopes, visitedSymbolIds) => {
|
|
10512
|
+
const identitySourceKeys = /* @__PURE__ */ new Set();
|
|
10513
|
+
for (const expression of expressions) {
|
|
10514
|
+
const expressionSourceKeys = resolveIdentitySourceKeysFromExpression(expression, scopes, visitedSymbolIds);
|
|
10515
|
+
if (!expressionSourceKeys) return null;
|
|
10516
|
+
for (const expressionSourceKey of expressionSourceKeys) identitySourceKeys.add(expressionSourceKey);
|
|
10517
|
+
}
|
|
10518
|
+
return identitySourceKeys;
|
|
10519
|
+
};
|
|
10520
|
+
const resolveIdentitySourceKeysFromExpression = (expression, scopes, visitedSymbolIds) => {
|
|
10521
|
+
const stripped = unwrapExpression$3(expression);
|
|
10522
|
+
if (isNodeOfType(stripped, "Literal") && (stripped.value === null || typeof stripped.value === "string" || typeof stripped.value === "number" || typeof stripped.value === "boolean") || isNodeOfType(stripped, "TemplateLiteral") && getStaticTemplateLiteralValue(stripped) !== null) return /* @__PURE__ */ new Set();
|
|
10523
|
+
if (isNodeOfType(stripped, "Identifier")) {
|
|
10524
|
+
const sourceSymbol = scopes.symbolFor(stripped);
|
|
10525
|
+
if (!sourceSymbol) return null;
|
|
10526
|
+
if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
10527
|
+
if (sourceSymbol.kind === "const" && sourceSymbol.initializer && isNodeOfType(sourceSymbol.declarationNode, "VariableDeclarator") && sourceSymbol.declarationNode.id === sourceSymbol.bindingIdentifier && sourceSymbol.references.every((reference) => reference.flag === "read")) {
|
|
10528
|
+
if (visitedSymbolIds.has(sourceSymbol.id)) return null;
|
|
10529
|
+
visitedSymbolIds.add(sourceSymbol.id);
|
|
10530
|
+
const sourceKeys = resolveIdentitySourceKeysFromExpression(sourceSymbol.initializer, scopes, visitedSymbolIds);
|
|
10531
|
+
visitedSymbolIds.delete(sourceSymbol.id);
|
|
10532
|
+
if (sourceKeys) return sourceKeys;
|
|
10533
|
+
}
|
|
10534
|
+
return new Set([sourceSymbol.name]);
|
|
10535
|
+
}
|
|
10536
|
+
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
10537
|
+
if (hasComputedMemberExpression(stripped)) return null;
|
|
10538
|
+
const sourceKey = stringifyMemberChain(stripped);
|
|
10539
|
+
const rootIdentifier = getMemberRootIdentifier(stripped);
|
|
10540
|
+
const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
10541
|
+
if (!sourceKey || !rootSymbol) return null;
|
|
10542
|
+
if (isOutsideAllFunctions(rootSymbol)) return /* @__PURE__ */ new Set();
|
|
10543
|
+
if (isStableRefContainerCapture(rootSymbol, sourceKey, scopes)) return /* @__PURE__ */ new Set();
|
|
10544
|
+
if (symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
10545
|
+
return new Set([sourceKey]);
|
|
10546
|
+
}
|
|
10547
|
+
if (isNodeOfType(stripped, "LogicalExpression")) return mergeIdentitySourceKeys([stripped.left, stripped.right], scopes, visitedSymbolIds);
|
|
10548
|
+
if (isNodeOfType(stripped, "ConditionalExpression")) return mergeIdentitySourceKeys([
|
|
10549
|
+
stripped.test,
|
|
10550
|
+
stripped.consequent,
|
|
10551
|
+
stripped.alternate
|
|
10552
|
+
], scopes, visitedSymbolIds);
|
|
10553
|
+
return null;
|
|
10554
|
+
};
|
|
10555
|
+
const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
|
|
10556
|
+
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
|
|
10557
|
+
return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
|
|
10558
|
+
};
|
|
10093
10559
|
const isUseCallbackResultDep = (node, scopes) => {
|
|
10094
10560
|
const rootSymbol = getRootSymbol(node, scopes);
|
|
10095
10561
|
const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
|
|
10096
|
-
return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee) === "useCallback");
|
|
10562
|
+
return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee, scopes) === "useCallback");
|
|
10097
10563
|
};
|
|
10098
|
-
const
|
|
10564
|
+
const isExtraReactiveDepAllowed = (node, scopes) => {
|
|
10099
10565
|
const rootIdentifier = getMemberRootIdentifier(node);
|
|
10100
10566
|
if (!rootIdentifier) return false;
|
|
10101
10567
|
const symbol = scopes.symbolFor(rootIdentifier);
|
|
@@ -10123,10 +10589,17 @@ const isUnstableInitializer = (node) => {
|
|
|
10123
10589
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
10124
10590
|
return isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "ArrayExpression") || isNodeOfType(stripped, "ClassExpression") || isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment") || isNodeOfType(stripped, "AssignmentExpression") || isNodeOfType(stripped, "NewExpression");
|
|
10125
10591
|
};
|
|
10592
|
+
const isExtraDepAllowedForHook = (hookName, node, scopes) => {
|
|
10593
|
+
if (!isExtraReactiveDepAllowed(node, scopes)) return false;
|
|
10594
|
+
if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) return true;
|
|
10595
|
+
if (hookName !== "useMemo") return false;
|
|
10596
|
+
const rootSymbol = getRootSymbol(node, scopes);
|
|
10597
|
+
return Boolean(rootSymbol && !symbolHasStableValue(rootSymbol, scopes) && !isUnstableInitializer(rootSymbol.initializer));
|
|
10598
|
+
};
|
|
10126
10599
|
const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
|
|
10127
10600
|
const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
|
|
10128
|
-
const isStableSetterLikeSymbol = (symbol) => {
|
|
10129
|
-
if (!symbolHasStableHookOrigin(symbol)) return false;
|
|
10601
|
+
const isStableSetterLikeSymbol = (symbol, scopes) => {
|
|
10602
|
+
if (!symbolHasStableHookOrigin(symbol, scopes)) return false;
|
|
10130
10603
|
return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
|
|
10131
10604
|
};
|
|
10132
10605
|
const findStableSetterReference = (node, scopes) => {
|
|
@@ -10136,7 +10609,7 @@ const findStableSetterReference = (node, scopes) => {
|
|
|
10136
10609
|
if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
|
|
10137
10610
|
if (isNodeOfType(current, "Identifier")) {
|
|
10138
10611
|
const symbol = scopes.referenceFor(current)?.resolvedSymbol;
|
|
10139
|
-
if (symbol && isStableSetterLikeSymbol(symbol)) {
|
|
10612
|
+
if (symbol && isStableSetterLikeSymbol(symbol, scopes)) {
|
|
10140
10613
|
setterName = symbol.name;
|
|
10141
10614
|
return;
|
|
10142
10615
|
}
|
|
@@ -10252,11 +10725,11 @@ const hasRefCurrentAssignmentInComponent = (refSymbol) => {
|
|
|
10252
10725
|
}
|
|
10253
10726
|
return false;
|
|
10254
10727
|
};
|
|
10255
|
-
const isSeededDataRefSymbol = (refSymbol) => {
|
|
10728
|
+
const isSeededDataRefSymbol = (refSymbol, scopes) => {
|
|
10256
10729
|
if (!refSymbol) return false;
|
|
10257
10730
|
const initializer = refSymbol.initializer ? unwrapExpression$3(refSymbol.initializer) : null;
|
|
10258
10731
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
10259
|
-
if (getHookName(initializer.callee) !== "useRef") return false;
|
|
10732
|
+
if (getHookName(initializer.callee, scopes) !== "useRef") return false;
|
|
10260
10733
|
const firstArgument = initializer.arguments[0];
|
|
10261
10734
|
if (!firstArgument || !isAstNode(firstArgument)) return false;
|
|
10262
10735
|
const strippedArgument = unwrapExpression$3(firstArgument);
|
|
@@ -10296,7 +10769,9 @@ const isOuterFunctionScopeDep = (node, callback, scopes) => {
|
|
|
10296
10769
|
const componentOrHookScope = componentOrHookFunction ? scopes.ownScopeFor(componentOrHookFunction) : null;
|
|
10297
10770
|
return Boolean(componentOrHookScope && !isDescendantScope(symbol.scope, componentOrHookScope));
|
|
10298
10771
|
};
|
|
10299
|
-
const hasMemberCallForRoot = (node, rootName) => {
|
|
10772
|
+
const hasMemberCallForRoot = (node, rootName, scopes) => {
|
|
10773
|
+
const rootSymbol = closureCaptures(node, scopes).find((reference) => reference.resolvedSymbol?.name === rootName)?.resolvedSymbol ?? null;
|
|
10774
|
+
if (!rootSymbol) return false;
|
|
10300
10775
|
let didFindMemberCall = false;
|
|
10301
10776
|
const visit = (current) => {
|
|
10302
10777
|
if (didFindMemberCall) return;
|
|
@@ -10312,7 +10787,7 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
10312
10787
|
}
|
|
10313
10788
|
chainObject = unwrapExpression$3(chainObject.object);
|
|
10314
10789
|
}
|
|
10315
|
-
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
|
|
10790
|
+
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName && scopes.symbolFor(chainObject) === rootSymbol) {
|
|
10316
10791
|
didFindMemberCall = true;
|
|
10317
10792
|
return;
|
|
10318
10793
|
}
|
|
@@ -10330,9 +10805,11 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
10330
10805
|
visit(node);
|
|
10331
10806
|
return didFindMemberCall;
|
|
10332
10807
|
};
|
|
10333
|
-
const addAggregatePropsDependency = (captureKeys, declaredKeys, callback) => {
|
|
10334
|
-
|
|
10335
|
-
if (
|
|
10808
|
+
const addAggregatePropsDependency = (captureKeys, declaredKeys, callback, scopes) => {
|
|
10809
|
+
const propsCaptureKeys = [...captureKeys].filter((captureKey) => captureKey.startsWith("props."));
|
|
10810
|
+
if (propsCaptureKeys.length < 2 || declaredKeys.has("props")) return;
|
|
10811
|
+
if (propsCaptureKeys.every((captureKey) => [...declaredKeys].some((declaredKey) => isMatchingDepOrPrefix(declaredKey, captureKey)))) return;
|
|
10812
|
+
if (hasMemberCallForRoot(callback, "props", scopes)) captureKeys.add("props");
|
|
10336
10813
|
};
|
|
10337
10814
|
const exhaustiveDeps = defineRule({
|
|
10338
10815
|
id: "exhaustive-deps",
|
|
@@ -10387,7 +10864,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10387
10864
|
};
|
|
10388
10865
|
const isAutoDependenciesHook = (hookName) => settings.experimental_autoDependenciesHooks.includes(hookName);
|
|
10389
10866
|
return { CallExpression(node) {
|
|
10390
|
-
const hookName = getHookName(node.callee);
|
|
10867
|
+
const hookName = getHookName(node.callee, context.scopes);
|
|
10391
10868
|
if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
|
|
10392
10869
|
const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
|
|
10393
10870
|
const depsArgumentIndex = getDepsArgumentIndex(hookName);
|
|
@@ -10444,7 +10921,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10444
10921
|
if (outerAssignments.length > 0) return;
|
|
10445
10922
|
const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
|
|
10446
10923
|
const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
|
|
10447
|
-
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol)) context.report({
|
|
10924
|
+
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol, context.scopes)) context.report({
|
|
10448
10925
|
node: callbackToAnalyze,
|
|
10449
10926
|
message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
|
|
10450
10927
|
});
|
|
@@ -10543,7 +11020,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10543
11020
|
const fullChain = stringifyMemberChain(stripped);
|
|
10544
11021
|
if (fullChain && fullChain.endsWith(".current") && isNodeOfType(stripped, "MemberExpression") && isNodeOfType(stripped.object, "Identifier")) {
|
|
10545
11022
|
const refSymbol = context.scopes.symbolFor(stripped.object);
|
|
10546
|
-
if (refSymbol && symbolHasStableHookOrigin(refSymbol)) {
|
|
11023
|
+
if (refSymbol && symbolHasStableHookOrigin(refSymbol, context.scopes)) {
|
|
10547
11024
|
if (!didReportRefCurrentDep) {
|
|
10548
11025
|
context.report({
|
|
10549
11026
|
node: elementNode,
|
|
@@ -10581,7 +11058,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10581
11058
|
declaredKeys.add(key);
|
|
10582
11059
|
declaredKeyToReportNode.set(key, elementNode);
|
|
10583
11060
|
}
|
|
10584
|
-
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument);
|
|
11061
|
+
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument, context.scopes);
|
|
10585
11062
|
const missingCaptureKeys = [];
|
|
10586
11063
|
for (const captureKey of captureKeys) {
|
|
10587
11064
|
let isCoveredByDeclared = false;
|
|
@@ -10667,7 +11144,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10667
11144
|
if (stableCapturedNames.has(rootName) || stableCapturedNames.has(declaredKey)) continue;
|
|
10668
11145
|
if (outerFunctionCapturedNames.has(rootName)) continue;
|
|
10669
11146
|
const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
|
|
10670
|
-
if (
|
|
11147
|
+
if (isExtraDepAllowedForHook(hookName, reportNode, context.scopes)) continue;
|
|
10671
11148
|
if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
|
|
10672
11149
|
const rootSymbol = getRootSymbol(reportNode, context.scopes);
|
|
10673
11150
|
if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
|
|
@@ -12339,20 +12816,6 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
12339
12816
|
return names;
|
|
12340
12817
|
};
|
|
12341
12818
|
//#endregion
|
|
12342
|
-
//#region src/plugin/utils/get-function-binding-name.ts
|
|
12343
|
-
const getFunctionBindingIdentifier = (functionNode) => {
|
|
12344
|
-
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
12345
|
-
const parent = functionNode.parent;
|
|
12346
|
-
if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
|
|
12347
|
-
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionNode && isNodeOfType(parent.left, "Identifier")) return parent.left;
|
|
12348
|
-
if (isNodeOfType(parent, "CallExpression")) {
|
|
12349
|
-
const callParent = parent.parent;
|
|
12350
|
-
if (isNodeOfType(callParent, "VariableDeclarator") && isNodeOfType(callParent.id, "Identifier")) return callParent.id;
|
|
12351
|
-
}
|
|
12352
|
-
return null;
|
|
12353
|
-
};
|
|
12354
|
-
const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(functionNode)?.name ?? null;
|
|
12355
|
-
//#endregion
|
|
12356
12819
|
//#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
|
|
12357
12820
|
const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
|
|
12358
12821
|
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
@@ -13497,7 +13960,7 @@ const jsHoistIntl = defineRule({
|
|
|
13497
13960
|
});
|
|
13498
13961
|
//#endregion
|
|
13499
13962
|
//#region src/plugin/utils/create-loop-aware-visitors.ts
|
|
13500
|
-
const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
13963
|
+
const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
|
|
13501
13964
|
"map",
|
|
13502
13965
|
"flatMap",
|
|
13503
13966
|
"forEach",
|
|
@@ -13516,7 +13979,7 @@ const isIteratorCallback = (node) => {
|
|
|
13516
13979
|
const parent = node.parent;
|
|
13517
13980
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
13518
13981
|
if (!parent.arguments.includes(node)) return false;
|
|
13519
|
-
return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(parent.callee.property.name);
|
|
13982
|
+
return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(parent.callee.property.name);
|
|
13520
13983
|
};
|
|
13521
13984
|
const createLoopAwareVisitors = (innerVisitors, options = {}) => {
|
|
13522
13985
|
let loopDepth = 0;
|
|
@@ -13745,7 +14208,7 @@ const findIndexedArrayObject = (callbackBody, indexParameterName) => {
|
|
|
13745
14208
|
});
|
|
13746
14209
|
return indexedArrayObject;
|
|
13747
14210
|
};
|
|
13748
|
-
const unwrapChainExpression$
|
|
14211
|
+
const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
13749
14212
|
const LENGTH_EQUALITY_OPERATORS = new Set(["===", "=="]);
|
|
13750
14213
|
const LENGTH_MISMATCH_OPERATORS = new Set([
|
|
13751
14214
|
"!==",
|
|
@@ -13757,17 +14220,17 @@ const LENGTH_MISMATCH_OPERATORS = new Set([
|
|
|
13757
14220
|
]);
|
|
13758
14221
|
const LENGTH_ANY_COMPARISON_OPERATORS = new Set([...LENGTH_EQUALITY_OPERATORS, ...LENGTH_MISMATCH_OPERATORS]);
|
|
13759
14222
|
const isLengthComparison = (candidate, receiverArray, indexedArray, operators) => {
|
|
13760
|
-
const binaryGuard = unwrapChainExpression$
|
|
14223
|
+
const binaryGuard = unwrapChainExpression$3(candidate);
|
|
13761
14224
|
if (!isNodeOfType(binaryGuard, "BinaryExpression")) return false;
|
|
13762
14225
|
if (!operators.has(binaryGuard.operator)) return false;
|
|
13763
|
-
const leftSide = unwrapChainExpression$
|
|
13764
|
-
const rightSide = unwrapChainExpression$
|
|
14226
|
+
const leftSide = unwrapChainExpression$3(binaryGuard.left);
|
|
14227
|
+
const rightSide = unwrapChainExpression$3(binaryGuard.right);
|
|
13765
14228
|
if (!isMemberProperty(leftSide, "length")) return false;
|
|
13766
14229
|
if (!isMemberProperty(rightSide, "length")) return false;
|
|
13767
|
-
const leftLengthObject = unwrapChainExpression$
|
|
13768
|
-
const rightLengthObject = unwrapChainExpression$
|
|
13769
|
-
const normalizedReceiver = unwrapChainExpression$
|
|
13770
|
-
const normalizedIndexed = unwrapChainExpression$
|
|
14230
|
+
const leftLengthObject = unwrapChainExpression$3(leftSide.object);
|
|
14231
|
+
const rightLengthObject = unwrapChainExpression$3(rightSide.object);
|
|
14232
|
+
const normalizedReceiver = unwrapChainExpression$3(receiverArray);
|
|
14233
|
+
const normalizedIndexed = unwrapChainExpression$3(indexedArray);
|
|
13771
14234
|
const matchesReceiverThenIndexed = areExpressionsStructurallyEqual(leftLengthObject, normalizedReceiver) && areExpressionsStructurallyEqual(rightLengthObject, normalizedIndexed);
|
|
13772
14235
|
const matchesIndexedThenReceiver = areExpressionsStructurallyEqual(leftLengthObject, normalizedIndexed) && areExpressionsStructurallyEqual(rightLengthObject, normalizedReceiver);
|
|
13773
14236
|
return matchesReceiverThenIndexed || matchesIndexedThenReceiver;
|
|
@@ -13854,18 +14317,18 @@ const LENGTH_PRESERVING_METHOD_NAMES = new Set([
|
|
|
13854
14317
|
"toReversed"
|
|
13855
14318
|
]);
|
|
13856
14319
|
const peelLengthPreservingDerivation = (expression) => {
|
|
13857
|
-
let current = unwrapChainExpression$
|
|
14320
|
+
let current = unwrapChainExpression$3(expression);
|
|
13858
14321
|
for (;;) {
|
|
13859
14322
|
if (isNodeOfType(current, "ArrayExpression") && current.elements?.length === 1 && isNodeOfType(current.elements[0], "SpreadElement")) {
|
|
13860
|
-
current = unwrapChainExpression$
|
|
14323
|
+
current = unwrapChainExpression$3(current.elements[0].argument);
|
|
13861
14324
|
continue;
|
|
13862
14325
|
}
|
|
13863
14326
|
if (isNodeOfType(current, "CallExpression")) {
|
|
13864
|
-
const callee = unwrapChainExpression$
|
|
14327
|
+
const callee = unwrapChainExpression$3(current.callee);
|
|
13865
14328
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
13866
|
-
const calleeObject = unwrapChainExpression$
|
|
14329
|
+
const calleeObject = unwrapChainExpression$3(callee.object);
|
|
13867
14330
|
if (isNodeOfType(calleeObject, "Identifier") && calleeObject.name === "Array" && callee.property.name === "from" && current.arguments?.length === 1) {
|
|
13868
|
-
current = unwrapChainExpression$
|
|
14331
|
+
current = unwrapChainExpression$3(current.arguments[0]);
|
|
13869
14332
|
continue;
|
|
13870
14333
|
}
|
|
13871
14334
|
if (LENGTH_PRESERVING_METHOD_NAMES.has(callee.property.name)) {
|
|
@@ -13910,7 +14373,7 @@ const resolveComparedArraySource = (expression, scopeNode) => {
|
|
|
13910
14373
|
const initializer = findConstInitializer(current.name, scopeNode);
|
|
13911
14374
|
if (!initializer) return current;
|
|
13912
14375
|
const peeledInitializer = peelLengthPreservingDerivation(initializer);
|
|
13913
|
-
if (peeledInitializer === unwrapChainExpression$
|
|
14376
|
+
if (peeledInitializer === unwrapChainExpression$3(initializer)) return current;
|
|
13914
14377
|
current = peeledInitializer;
|
|
13915
14378
|
}
|
|
13916
14379
|
return current;
|
|
@@ -19640,54 +20103,6 @@ const nextjsNoAElement = defineRule({
|
|
|
19640
20103
|
} })
|
|
19641
20104
|
});
|
|
19642
20105
|
//#endregion
|
|
19643
|
-
//#region src/plugin/utils/collect-effect-invoked-functions.ts
|
|
19644
|
-
const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
|
|
19645
|
-
"then",
|
|
19646
|
-
"catch",
|
|
19647
|
-
"finally"
|
|
19648
|
-
]);
|
|
19649
|
-
const collectEffectInvokedFunctions = (effectCallback) => {
|
|
19650
|
-
const invokedFunctions = new Set([effectCallback]);
|
|
19651
|
-
const localFunctionBindings = /* @__PURE__ */ new Map();
|
|
19652
|
-
const calledBindingNames = /* @__PURE__ */ new Set();
|
|
19653
|
-
const pendingFunctions = [effectCallback];
|
|
19654
|
-
const enqueue = (candidate) => {
|
|
19655
|
-
const strippedCandidate = candidate ? stripParenExpression(candidate) : candidate;
|
|
19656
|
-
if (!isFunctionLike$1(strippedCandidate) || invokedFunctions.has(strippedCandidate)) return;
|
|
19657
|
-
invokedFunctions.add(strippedCandidate);
|
|
19658
|
-
pendingFunctions.push(strippedCandidate);
|
|
19659
|
-
};
|
|
19660
|
-
const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
|
|
19661
|
-
while (pendingFunctions.length > 0) {
|
|
19662
|
-
const currentFunction = pendingFunctions.pop();
|
|
19663
|
-
if (!currentFunction) break;
|
|
19664
|
-
walkAst(currentFunction, (child) => {
|
|
19665
|
-
if (child !== currentFunction && isFunctionLike$1(child)) {
|
|
19666
|
-
if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) localFunctionBindings.set(child.id.name, child);
|
|
19667
|
-
return false;
|
|
19668
|
-
}
|
|
19669
|
-
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
19670
|
-
const initializer = child.init ? stripParenExpression(child.init) : null;
|
|
19671
|
-
if (isFunctionLike$1(initializer)) localFunctionBindings.set(child.id.name, initializer);
|
|
19672
|
-
return;
|
|
19673
|
-
}
|
|
19674
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
19675
|
-
const callee = stripParenExpression(child.callee);
|
|
19676
|
-
if (isFunctionLike$1(callee)) {
|
|
19677
|
-
enqueue(callee);
|
|
19678
|
-
return;
|
|
19679
|
-
}
|
|
19680
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
19681
|
-
calledBindingNames.add(callee.name);
|
|
19682
|
-
return;
|
|
19683
|
-
}
|
|
19684
|
-
if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
19685
|
-
});
|
|
19686
|
-
for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
|
|
19687
|
-
}
|
|
19688
|
-
return invokedFunctions;
|
|
19689
|
-
};
|
|
19690
|
-
//#endregion
|
|
19691
20106
|
//#region src/plugin/utils/contains-fetch-call.ts
|
|
19692
20107
|
const isFetchCall$1 = (node) => {
|
|
19693
20108
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
@@ -20671,6 +21086,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
20671
21086
|
continue;
|
|
20672
21087
|
}
|
|
20673
21088
|
if (isNodeOfType(statement, "ExportNamedDeclaration")) {
|
|
21089
|
+
if (statement.exportKind === "type") continue;
|
|
20674
21090
|
const declaration = statement.declaration;
|
|
20675
21091
|
if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
|
|
20676
21092
|
recordVariableDeclaration(declaration);
|
|
@@ -20685,6 +21101,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
20685
21101
|
}
|
|
20686
21102
|
for (const specifier of statement.specifiers ?? []) {
|
|
20687
21103
|
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
21104
|
+
if (specifier.exportKind === "type") continue;
|
|
20688
21105
|
const local = specifier.local;
|
|
20689
21106
|
const exported = specifier.exported;
|
|
20690
21107
|
if (!isNodeOfType(local, "Identifier")) continue;
|
|
@@ -20733,25 +21150,37 @@ const resolveImportedExportName = (importSpecifier) => {
|
|
|
20733
21150
|
if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
|
|
20734
21151
|
return null;
|
|
20735
21152
|
};
|
|
20736
|
-
const
|
|
21153
|
+
const findReExportTargetsForName = (programRoot, exportedName) => {
|
|
20737
21154
|
if (!isNodeOfType(programRoot, "Program")) return [];
|
|
20738
|
-
const
|
|
21155
|
+
const exportAllTargets = [];
|
|
20739
21156
|
for (const statement of programRoot.body ?? []) {
|
|
20740
21157
|
if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
|
|
21158
|
+
if (statement.exportKind === "type") continue;
|
|
20741
21159
|
const sourceValue = statement.source.value;
|
|
20742
21160
|
if (typeof sourceValue !== "string") continue;
|
|
20743
21161
|
for (const specifier of statement.specifiers ?? []) {
|
|
20744
21162
|
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
21163
|
+
if (specifier.exportKind === "type") continue;
|
|
20745
21164
|
const exported = specifier.exported;
|
|
20746
|
-
if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null)
|
|
21165
|
+
if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) !== exportedName) continue;
|
|
21166
|
+
const local = specifier.local;
|
|
21167
|
+
const importedName = isNodeOfType(local, "Identifier") ? local.name : isNodeOfType(local, "Literal") && typeof local.value === "string" ? local.value : null;
|
|
21168
|
+
if (importedName) return [{
|
|
21169
|
+
importedName,
|
|
21170
|
+
source: sourceValue
|
|
21171
|
+
}];
|
|
20747
21172
|
}
|
|
20748
21173
|
}
|
|
20749
21174
|
if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
|
|
21175
|
+
if (statement.exportKind === "type" || statement.exported) continue;
|
|
20750
21176
|
const sourceValue = statement.source.value;
|
|
20751
|
-
if (typeof sourceValue === "string")
|
|
21177
|
+
if (typeof sourceValue === "string") exportAllTargets.push({
|
|
21178
|
+
importedName: exportedName,
|
|
21179
|
+
source: sourceValue
|
|
21180
|
+
});
|
|
20752
21181
|
}
|
|
20753
21182
|
}
|
|
20754
|
-
return
|
|
21183
|
+
return exportAllTargets;
|
|
20755
21184
|
};
|
|
20756
21185
|
//#endregion
|
|
20757
21186
|
//#region src/plugin/utils/attach-parent-references.ts
|
|
@@ -20840,139 +21269,6 @@ const parseSourceFile = (absoluteFilePath) => {
|
|
|
20840
21269
|
return parsedProgram;
|
|
20841
21270
|
};
|
|
20842
21271
|
//#endregion
|
|
20843
|
-
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
20844
|
-
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
20845
|
-
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
20846
|
-
const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
20847
|
-
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
20848
|
-
const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
|
|
20849
|
-
const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
|
|
20850
|
-
const createNonBarrelInfo = () => ({
|
|
20851
|
-
isBarrel: false,
|
|
20852
|
-
exportsByName: /* @__PURE__ */ new Map(),
|
|
20853
|
-
starExportSources: []
|
|
20854
|
-
});
|
|
20855
|
-
const addImportedBinding = (importedBindings, binding) => {
|
|
20856
|
-
importedBindings.set(binding.localName, {
|
|
20857
|
-
...binding,
|
|
20858
|
-
didExport: false
|
|
20859
|
-
});
|
|
20860
|
-
};
|
|
20861
|
-
const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
|
|
20862
|
-
for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
|
|
20863
|
-
localName: specifier.exportedName,
|
|
20864
|
-
importedName: specifier.localName,
|
|
20865
|
-
source,
|
|
20866
|
-
isTypeOnly: specifier.isTypeOnly
|
|
20867
|
-
});
|
|
20868
|
-
};
|
|
20869
|
-
const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
|
|
20870
|
-
const trimmedImportClause = importClause.trim();
|
|
20871
|
-
const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
|
|
20872
|
-
if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
|
|
20873
|
-
localName: namespaceMatch[1],
|
|
20874
|
-
importedName: "*",
|
|
20875
|
-
source,
|
|
20876
|
-
isTypeOnly: declarationIsTypeOnly
|
|
20877
|
-
});
|
|
20878
|
-
const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
|
|
20879
|
-
if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
|
|
20880
|
-
const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
|
|
20881
|
-
if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
|
|
20882
|
-
localName: defaultImportName,
|
|
20883
|
-
importedName: "default",
|
|
20884
|
-
source,
|
|
20885
|
-
isTypeOnly: declarationIsTypeOnly
|
|
20886
|
-
});
|
|
20887
|
-
};
|
|
20888
|
-
const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
|
|
20889
|
-
let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
|
|
20890
|
-
collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
|
|
20891
|
-
return "";
|
|
20892
|
-
});
|
|
20893
|
-
withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
|
|
20894
|
-
const isTypeOnly = Boolean(typeKeyword);
|
|
20895
|
-
if (namespaceExportName) {
|
|
20896
|
-
exportsByName.set(namespaceExportName, {
|
|
20897
|
-
exportedName: namespaceExportName,
|
|
20898
|
-
importedName: "*",
|
|
20899
|
-
source,
|
|
20900
|
-
isTypeOnly
|
|
20901
|
-
});
|
|
20902
|
-
return "";
|
|
20903
|
-
}
|
|
20904
|
-
if (specifiersText) {
|
|
20905
|
-
for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
|
|
20906
|
-
exportedName: specifier.exportedName,
|
|
20907
|
-
importedName: specifier.localName,
|
|
20908
|
-
source,
|
|
20909
|
-
isTypeOnly: specifier.isTypeOnly
|
|
20910
|
-
});
|
|
20911
|
-
return "";
|
|
20912
|
-
}
|
|
20913
|
-
starExportSources.push(source);
|
|
20914
|
-
return "";
|
|
20915
|
-
});
|
|
20916
|
-
withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
|
|
20917
|
-
for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
|
|
20918
|
-
const importedBinding = importedBindings.get(specifier.localName);
|
|
20919
|
-
if (!importedBinding) return _match;
|
|
20920
|
-
importedBinding.didExport = true;
|
|
20921
|
-
exportsByName.set(specifier.exportedName, {
|
|
20922
|
-
exportedName: specifier.exportedName,
|
|
20923
|
-
importedName: importedBinding.importedName,
|
|
20924
|
-
source: importedBinding.source,
|
|
20925
|
-
isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
|
|
20926
|
-
});
|
|
20927
|
-
}
|
|
20928
|
-
return "";
|
|
20929
|
-
});
|
|
20930
|
-
return withoutKnownDeclarations;
|
|
20931
|
-
};
|
|
20932
|
-
const hasUnexportedRuntimeImport = (importedBindings) => {
|
|
20933
|
-
for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
|
|
20934
|
-
return false;
|
|
20935
|
-
};
|
|
20936
|
-
const classifyBarrelModule = (sourceText) => {
|
|
20937
|
-
const strippedSource = stripJsComments(sourceText).trim();
|
|
20938
|
-
if (!strippedSource) return createNonBarrelInfo();
|
|
20939
|
-
const importedBindings = /* @__PURE__ */ new Map();
|
|
20940
|
-
const exportsByName = /* @__PURE__ */ new Map();
|
|
20941
|
-
const starExportSources = [];
|
|
20942
|
-
if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
|
|
20943
|
-
return {
|
|
20944
|
-
isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
|
|
20945
|
-
exportsByName,
|
|
20946
|
-
starExportSources
|
|
20947
|
-
};
|
|
20948
|
-
};
|
|
20949
|
-
const getBarrelIndexModuleInfo = (filePath) => {
|
|
20950
|
-
if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
|
|
20951
|
-
recordContentProbe(filePath);
|
|
20952
|
-
let fileStat;
|
|
20953
|
-
try {
|
|
20954
|
-
fileStat = fs.statSync(filePath);
|
|
20955
|
-
} catch {
|
|
20956
|
-
fileStat = null;
|
|
20957
|
-
}
|
|
20958
|
-
const cachedResult = barrelIndexModuleInfoCache.get(filePath);
|
|
20959
|
-
if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
|
|
20960
|
-
if (fileStat === null) return createNonBarrelInfo();
|
|
20961
|
-
let moduleInfo = createNonBarrelInfo();
|
|
20962
|
-
try {
|
|
20963
|
-
moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
|
|
20964
|
-
} catch {
|
|
20965
|
-
moduleInfo = createNonBarrelInfo();
|
|
20966
|
-
}
|
|
20967
|
-
barrelIndexModuleInfoCache.set(filePath, {
|
|
20968
|
-
mtimeMs: fileStat.mtimeMs,
|
|
20969
|
-
size: fileStat.size,
|
|
20970
|
-
moduleInfo
|
|
20971
|
-
});
|
|
20972
|
-
return moduleInfo;
|
|
20973
|
-
};
|
|
20974
|
-
const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
|
|
20975
|
-
//#endregion
|
|
20976
21272
|
//#region src/plugin/utils/resolve-relative-import-path.ts
|
|
20977
21273
|
const MODULE_FILE_EXTENSIONS = [
|
|
20978
21274
|
".ts",
|
|
@@ -21089,35 +21385,6 @@ const resolveModuleFileFromAbsolutePath = (importPath) => {
|
|
|
21089
21385
|
};
|
|
21090
21386
|
const resolveRelativeImportPath = (filename, source) => resolveModuleFileFromAbsolutePath(path.resolve(path.dirname(filename), source));
|
|
21091
21387
|
//#endregion
|
|
21092
|
-
//#region src/plugin/utils/resolve-barrel-export-file-path.ts
|
|
21093
|
-
const getUniqueFilePath = (filePaths) => {
|
|
21094
|
-
const uniqueFilePaths = new Set(filePaths);
|
|
21095
|
-
if (uniqueFilePaths.size !== 1) return null;
|
|
21096
|
-
const [filePath] = uniqueFilePaths;
|
|
21097
|
-
return filePath ?? null;
|
|
21098
|
-
};
|
|
21099
|
-
const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
|
|
21100
|
-
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
|
|
21101
|
-
if (!resolvedTargetPath) return null;
|
|
21102
|
-
const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
|
|
21103
|
-
if (nestedTargetPath) return nestedTargetPath;
|
|
21104
|
-
return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
|
|
21105
|
-
};
|
|
21106
|
-
const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
|
|
21107
|
-
if (visitedFilePaths.has(barrelFilePath)) return null;
|
|
21108
|
-
visitedFilePaths.add(barrelFilePath);
|
|
21109
|
-
const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
|
|
21110
|
-
if (!moduleInfo.isBarrel) return null;
|
|
21111
|
-
const target = moduleInfo.exportsByName.get(exportedName);
|
|
21112
|
-
if (target) {
|
|
21113
|
-
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
|
|
21114
|
-
if (!resolvedTargetPath) return null;
|
|
21115
|
-
return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
|
|
21116
|
-
}
|
|
21117
|
-
if (exportedName === "default") return null;
|
|
21118
|
-
return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
|
|
21119
|
-
};
|
|
21120
|
-
//#endregion
|
|
21121
21388
|
//#region src/plugin/utils/resolve-tsconfig-alias.ts
|
|
21122
21389
|
const TSCONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
|
|
21123
21390
|
const isObjectRecord = (value) => typeof value === "object" && value !== null;
|
|
@@ -21296,25 +21563,29 @@ const resolveTsconfigAliasPath = (fromFilename, source) => {
|
|
|
21296
21563
|
};
|
|
21297
21564
|
//#endregion
|
|
21298
21565
|
//#region src/plugin/utils/resolve-module-path.ts
|
|
21299
|
-
const resolveModulePath = (fromFilename, source) =>
|
|
21566
|
+
const resolveModulePath = (fromFilename, source) => {
|
|
21567
|
+
if (path.isAbsolute(source)) return null;
|
|
21568
|
+
return source.startsWith(".") ? resolveRelativeImportPath(fromFilename, source) : resolveTsconfigAliasPath(fromFilename, source);
|
|
21569
|
+
};
|
|
21300
21570
|
//#endregion
|
|
21301
21571
|
//#region src/plugin/utils/resolve-cross-file-function-export.ts
|
|
21302
21572
|
const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
|
|
21303
21573
|
if (visitedFilePaths.size >= 4) return null;
|
|
21304
21574
|
if (visitedFilePaths.has(filePath)) return null;
|
|
21305
21575
|
visitedFilePaths.add(filePath);
|
|
21306
|
-
const
|
|
21307
|
-
const programRoot = parseSourceFile(actualFilePath);
|
|
21576
|
+
const programRoot = parseSourceFile(filePath);
|
|
21308
21577
|
if (!programRoot) return null;
|
|
21309
21578
|
const exported = findExportedFunctionBody(programRoot, exportedName);
|
|
21310
21579
|
if (exported) return exported;
|
|
21311
|
-
|
|
21312
|
-
|
|
21580
|
+
const resolvedCandidates = /* @__PURE__ */ new Set();
|
|
21581
|
+
for (const target of findReExportTargetsForName(programRoot, exportedName)) {
|
|
21582
|
+
const nextFilePath = resolveModulePath(filePath, target.source);
|
|
21313
21583
|
if (!nextFilePath) continue;
|
|
21314
|
-
const resolved = resolveFunctionExportInFile(nextFilePath,
|
|
21315
|
-
if (resolved)
|
|
21584
|
+
const resolved = resolveFunctionExportInFile(nextFilePath, target.importedName, new Set(visitedFilePaths));
|
|
21585
|
+
if (resolved) resolvedCandidates.add(resolved);
|
|
21316
21586
|
}
|
|
21317
|
-
return null;
|
|
21587
|
+
if (resolvedCandidates.size !== 1) return null;
|
|
21588
|
+
return resolvedCandidates.values().next().value ?? null;
|
|
21318
21589
|
};
|
|
21319
21590
|
const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
|
|
21320
21591
|
const resolvedFilePath = resolveModulePath(fromFilename, source);
|
|
@@ -22099,6 +22370,26 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
22099
22370
|
const importDeclaration = declarationNode.parent;
|
|
22100
22371
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22101
22372
|
}));
|
|
22373
|
+
const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.some((def) => {
|
|
22374
|
+
if (def.type !== "ImportBinding") return false;
|
|
22375
|
+
const declarationNode = def.node;
|
|
22376
|
+
if (!isNodeOfType(declarationNode, "ImportNamespaceSpecifier") && !isNodeOfType(declarationNode, "ImportDefaultSpecifier")) return false;
|
|
22377
|
+
const importDeclaration = declarationNode.parent;
|
|
22378
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22379
|
+
}));
|
|
22380
|
+
const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
22381
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
|
|
22382
|
+
const callee = stripParenExpression(declarator.init.callee);
|
|
22383
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
22384
|
+
const reference = getRef(analysis, callee);
|
|
22385
|
+
if (!reference?.resolved) return callee.name === hookName;
|
|
22386
|
+
return isReactNamedImportReference(reference, hookName);
|
|
22387
|
+
}
|
|
22388
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
|
|
22389
|
+
const namespaceReference = getRef(analysis, callee.object);
|
|
22390
|
+
if (!namespaceReference?.resolved) return callee.object.name === "React";
|
|
22391
|
+
return isReactNamespaceImportReference(namespaceReference);
|
|
22392
|
+
};
|
|
22102
22393
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
22103
22394
|
if (!node) return false;
|
|
22104
22395
|
if (isNodeOfType(node, "Identifier")) {
|
|
@@ -22497,6 +22788,46 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
|
|
|
22497
22788
|
"parseInt"
|
|
22498
22789
|
]);
|
|
22499
22790
|
const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
|
|
22791
|
+
const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
|
|
22792
|
+
"isRawJSON",
|
|
22793
|
+
"parse",
|
|
22794
|
+
"rawJSON",
|
|
22795
|
+
"stringify"
|
|
22796
|
+
])], ["Math", new Set([
|
|
22797
|
+
"abs",
|
|
22798
|
+
"acos",
|
|
22799
|
+
"acosh",
|
|
22800
|
+
"asin",
|
|
22801
|
+
"asinh",
|
|
22802
|
+
"atan",
|
|
22803
|
+
"atan2",
|
|
22804
|
+
"atanh",
|
|
22805
|
+
"cbrt",
|
|
22806
|
+
"ceil",
|
|
22807
|
+
"clz32",
|
|
22808
|
+
"cos",
|
|
22809
|
+
"cosh",
|
|
22810
|
+
"exp",
|
|
22811
|
+
"floor",
|
|
22812
|
+
"fround",
|
|
22813
|
+
"hypot",
|
|
22814
|
+
"imul",
|
|
22815
|
+
"log",
|
|
22816
|
+
"log10",
|
|
22817
|
+
"log1p",
|
|
22818
|
+
"log2",
|
|
22819
|
+
"max",
|
|
22820
|
+
"min",
|
|
22821
|
+
"pow",
|
|
22822
|
+
"round",
|
|
22823
|
+
"sign",
|
|
22824
|
+
"sin",
|
|
22825
|
+
"sinh",
|
|
22826
|
+
"sqrt",
|
|
22827
|
+
"tan",
|
|
22828
|
+
"tanh",
|
|
22829
|
+
"trunc"
|
|
22830
|
+
])]]);
|
|
22500
22831
|
const PURE_MEMBER_TRANSFORM_NAMES = new Set([
|
|
22501
22832
|
"concat",
|
|
22502
22833
|
"filter",
|
|
@@ -22543,6 +22874,42 @@ const getParameterBindingIdentity = (analysis, functionNode, parameter) => {
|
|
|
22543
22874
|
}
|
|
22544
22875
|
return parameter;
|
|
22545
22876
|
};
|
|
22877
|
+
const isAsyncOrGeneratorFunction = (functionNode) => Boolean(functionNode.async === true || functionNode.generator === true);
|
|
22878
|
+
const isModuleFunction = (functionNode) => {
|
|
22879
|
+
let ancestor = functionNode.parent;
|
|
22880
|
+
while (ancestor) {
|
|
22881
|
+
if (isFunctionLike$1(ancestor)) return false;
|
|
22882
|
+
if (isNodeOfType(ancestor, "Program")) return true;
|
|
22883
|
+
ancestor = ancestor.parent;
|
|
22884
|
+
}
|
|
22885
|
+
return false;
|
|
22886
|
+
};
|
|
22887
|
+
const getFunctionBindingNames = (functionNode) => {
|
|
22888
|
+
const names = /* @__PURE__ */ new Set();
|
|
22889
|
+
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) names.add(functionNode.id.name);
|
|
22890
|
+
const parent = functionNode.parent;
|
|
22891
|
+
if (parent && isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) names.add(parent.id.name);
|
|
22892
|
+
return names;
|
|
22893
|
+
};
|
|
22894
|
+
const collectModuleBindingNames = (functionNode) => {
|
|
22895
|
+
let program = functionNode.parent;
|
|
22896
|
+
while (program && !isNodeOfType(program, "Program")) program = program.parent;
|
|
22897
|
+
const bindingNames = /* @__PURE__ */ new Set();
|
|
22898
|
+
if (!program || !isNodeOfType(program, "Program")) return bindingNames;
|
|
22899
|
+
for (const statement of program.body ?? []) {
|
|
22900
|
+
if (isNodeOfType(statement, "ImportDeclaration")) {
|
|
22901
|
+
for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier.local, "Identifier")) bindingNames.add(specifier.local.name);
|
|
22902
|
+
continue;
|
|
22903
|
+
}
|
|
22904
|
+
const declaration = isNodeOfType(statement, "ExportNamedDeclaration") || isNodeOfType(statement, "ExportDefaultDeclaration") ? statement.declaration : statement;
|
|
22905
|
+
if (isNodeOfType(declaration, "VariableDeclaration")) {
|
|
22906
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, bindingNames);
|
|
22907
|
+
continue;
|
|
22908
|
+
}
|
|
22909
|
+
if ((isNodeOfType(declaration, "FunctionDeclaration") || isNodeOfType(declaration, "ClassDeclaration")) && isNodeOfType(declaration.id, "Identifier")) bindingNames.add(declaration.id.name);
|
|
22910
|
+
}
|
|
22911
|
+
return bindingNames;
|
|
22912
|
+
};
|
|
22546
22913
|
const buildSubstitutions = (analysis, functionNode, argumentExpressions, parentFrame) => {
|
|
22547
22914
|
const substitutions = /* @__PURE__ */ new Map();
|
|
22548
22915
|
const parameters = getFunctionParameters(functionNode);
|
|
@@ -22673,7 +23040,7 @@ const collectIntroducedBindings = (analysis, functionNode) => {
|
|
|
22673
23040
|
for (const parameter of getFunctionParameters(functionNode)) if (isNodeOfType(parameter, "Identifier")) introducedBindings.add(getParameterBindingIdentity(analysis, functionNode, parameter));
|
|
22674
23041
|
return introducedBindings;
|
|
22675
23042
|
};
|
|
22676
|
-
const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
|
|
23043
|
+
const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilename) => {
|
|
22677
23044
|
const effectFunction = getEffectFn(analysis, effectNode);
|
|
22678
23045
|
if (!effectFunction || !isFunctionLike$1(effectFunction) || effectFunction.async === true) return [];
|
|
22679
23046
|
const invokedFunctionEvidence = collectEffectInvokedFunctions(effectFunction);
|
|
@@ -22682,7 +23049,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
|
|
|
22682
23049
|
invocation: null,
|
|
22683
23050
|
isDeferred: false,
|
|
22684
23051
|
introducedBindings: /* @__PURE__ */ new Set(),
|
|
22685
|
-
substitutions: /* @__PURE__ */ new Map()
|
|
23052
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
23053
|
+
currentFilename
|
|
22686
23054
|
};
|
|
22687
23055
|
const frames = [rootFrame];
|
|
22688
23056
|
walkAst(effectFunction, (child) => {
|
|
@@ -22703,7 +23071,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
|
|
|
22703
23071
|
invocation: child,
|
|
22704
23072
|
isDeferred,
|
|
22705
23073
|
introducedBindings,
|
|
22706
|
-
substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame)
|
|
23074
|
+
substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame),
|
|
23075
|
+
currentFilename
|
|
22707
23076
|
});
|
|
22708
23077
|
};
|
|
22709
23078
|
if (isFunctionLike$1(callee)) {
|
|
@@ -22756,6 +23125,174 @@ const isOpaqueHookCall = (callExpression) => {
|
|
|
22756
23125
|
const calleeName = getCallCalleeName$1(callExpression);
|
|
22757
23126
|
return Boolean(calleeName && /^use[A-Z0-9]/.test(calleeName) && calleeName !== "useMemo" && calleeName !== "useCallback" && calleeName !== "useEffectEvent");
|
|
22758
23127
|
};
|
|
23128
|
+
const analyzeHelperStatements = (statements, environment, usedParameterIndices, analyzeExpression) => {
|
|
23129
|
+
let canContinue = true;
|
|
23130
|
+
for (const statement of statements) {
|
|
23131
|
+
if (!canContinue) {
|
|
23132
|
+
if (!isNodeOfType(statement, "EmptyStatement")) return {
|
|
23133
|
+
canContinue: false,
|
|
23134
|
+
isValid: false
|
|
23135
|
+
};
|
|
23136
|
+
continue;
|
|
23137
|
+
}
|
|
23138
|
+
if (isNodeOfType(statement, "EmptyStatement")) continue;
|
|
23139
|
+
if (isNodeOfType(statement, "ReturnStatement")) {
|
|
23140
|
+
if (!statement.argument || !analyzeExpression(statement.argument, environment, usedParameterIndices)) return {
|
|
23141
|
+
canContinue: false,
|
|
23142
|
+
isValid: false
|
|
23143
|
+
};
|
|
23144
|
+
canContinue = false;
|
|
23145
|
+
continue;
|
|
23146
|
+
}
|
|
23147
|
+
if (isNodeOfType(statement, "BlockStatement")) {
|
|
23148
|
+
const blockSummary = analyzeHelperStatements(statement.body ?? [], environment, usedParameterIndices, analyzeExpression);
|
|
23149
|
+
if (!blockSummary.isValid) return blockSummary;
|
|
23150
|
+
canContinue = blockSummary.canContinue;
|
|
23151
|
+
continue;
|
|
23152
|
+
}
|
|
23153
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
23154
|
+
if (!analyzeExpression(statement.test, environment, usedParameterIndices)) return {
|
|
23155
|
+
canContinue: false,
|
|
23156
|
+
isValid: false
|
|
23157
|
+
};
|
|
23158
|
+
const consequentSummary = analyzeHelperStatements([statement.consequent], environment, usedParameterIndices, analyzeExpression);
|
|
23159
|
+
if (!consequentSummary.isValid) return consequentSummary;
|
|
23160
|
+
const alternateSummary = statement.alternate ? analyzeHelperStatements([statement.alternate], environment, usedParameterIndices, analyzeExpression) : {
|
|
23161
|
+
canContinue: true,
|
|
23162
|
+
isValid: true
|
|
23163
|
+
};
|
|
23164
|
+
if (!alternateSummary.isValid) return alternateSummary;
|
|
23165
|
+
canContinue = consequentSummary.canContinue || alternateSummary.canContinue;
|
|
23166
|
+
continue;
|
|
23167
|
+
}
|
|
23168
|
+
return {
|
|
23169
|
+
canContinue: false,
|
|
23170
|
+
isValid: false
|
|
23171
|
+
};
|
|
23172
|
+
}
|
|
23173
|
+
return {
|
|
23174
|
+
canContinue,
|
|
23175
|
+
isValid: true
|
|
23176
|
+
};
|
|
23177
|
+
};
|
|
23178
|
+
const analyzeHelperExpression = (expression, environment, usedParameterIndices) => {
|
|
23179
|
+
const node = stripParenExpression(expression);
|
|
23180
|
+
if (isNodeOfType(node, "Literal") || isNodeOfType(node, "TemplateElement")) return true;
|
|
23181
|
+
if (isNodeOfType(node, "Identifier")) {
|
|
23182
|
+
const parameterIndex = environment.parameterIndices.get(node.name);
|
|
23183
|
+
if (parameterIndex !== void 0) {
|
|
23184
|
+
if (parameterIndex !== null) usedParameterIndices.add(parameterIndex);
|
|
23185
|
+
return true;
|
|
23186
|
+
}
|
|
23187
|
+
return node.name === "undefined" || node.name === "NaN" || node.name === "Infinity";
|
|
23188
|
+
}
|
|
23189
|
+
if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => !element || analyzeHelperExpression(element, environment, usedParameterIndices));
|
|
23190
|
+
if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).every((property) => {
|
|
23191
|
+
if (isNodeOfType(property, "SpreadElement")) return analyzeHelperExpression(property.argument, environment, usedParameterIndices);
|
|
23192
|
+
if (!isNodeOfType(property, "Property") || property.kind !== "init" || property.method === true) return false;
|
|
23193
|
+
if (property.computed && !analyzeHelperExpression(property.key, environment, usedParameterIndices)) return false;
|
|
23194
|
+
return analyzeHelperExpression(property.value, environment, usedParameterIndices);
|
|
23195
|
+
});
|
|
23196
|
+
if (isNodeOfType(node, "TemplateLiteral")) return (node.expressions ?? []).every((templateExpression) => analyzeHelperExpression(templateExpression, environment, usedParameterIndices));
|
|
23197
|
+
if (isNodeOfType(node, "UnaryExpression")) return node.operator !== "delete" && analyzeHelperExpression(node.argument, environment, usedParameterIndices);
|
|
23198
|
+
if (isNodeOfType(node, "BinaryExpression") || isNodeOfType(node, "LogicalExpression")) return analyzeHelperExpression(node.left, environment, usedParameterIndices) && analyzeHelperExpression(node.right, environment, usedParameterIndices);
|
|
23199
|
+
if (isNodeOfType(node, "ConditionalExpression")) return analyzeHelperExpression(node.test, environment, usedParameterIndices) && analyzeHelperExpression(node.consequent, environment, usedParameterIndices) && analyzeHelperExpression(node.alternate, environment, usedParameterIndices);
|
|
23200
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
23201
|
+
if (!analyzeHelperExpression(node.object, environment, usedParameterIndices)) return false;
|
|
23202
|
+
return !node.computed || analyzeHelperExpression(node.property, environment, usedParameterIndices);
|
|
23203
|
+
}
|
|
23204
|
+
if (isFunctionLike$1(node)) {
|
|
23205
|
+
if (isAsyncOrGeneratorFunction(node)) return false;
|
|
23206
|
+
const callbackParameterIndices = new Map(environment.parameterIndices);
|
|
23207
|
+
for (const parameter of getFunctionParameters(node)) {
|
|
23208
|
+
if (!isNodeOfType(parameter, "Identifier")) return false;
|
|
23209
|
+
callbackParameterIndices.set(parameter.name, null);
|
|
23210
|
+
}
|
|
23211
|
+
const callbackEnvironment = {
|
|
23212
|
+
parameterIndices: callbackParameterIndices,
|
|
23213
|
+
recursiveNames: environment.recursiveNames,
|
|
23214
|
+
shadowedGlobalNames: environment.shadowedGlobalNames
|
|
23215
|
+
};
|
|
23216
|
+
if (!isNodeOfType(node.body, "BlockStatement")) return analyzeHelperExpression(node.body, callbackEnvironment, usedParameterIndices);
|
|
23217
|
+
const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
|
|
23218
|
+
return callbackSummary.isValid && !callbackSummary.canContinue;
|
|
23219
|
+
}
|
|
23220
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
23221
|
+
const callee = stripParenExpression(node.callee);
|
|
23222
|
+
const calleeRoot = getMemberRoot(callee);
|
|
23223
|
+
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && !environment.parameterIndices.has(callee.name) && !environment.recursiveNames.has(callee.name) && !environment.shadowedGlobalNames.has(callee.name);
|
|
23224
|
+
const namespaceName = isNodeOfType(calleeRoot, "Identifier") && !environment.parameterIndices.has(calleeRoot.name) && !environment.recursiveNames.has(calleeRoot.name) && !environment.shadowedGlobalNames.has(calleeRoot.name) ? calleeRoot.name : null;
|
|
23225
|
+
const namespaceMemberName = getStaticMemberName(callee);
|
|
23226
|
+
const isPureNamespaceCall = isNodeOfType(callee, "MemberExpression") && namespaceName !== null && namespaceMemberName !== null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(namespaceName)?.has(namespaceMemberName) === true;
|
|
23227
|
+
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
|
|
23228
|
+
if (!isPureGlobalCall && !isPureNamespaceCall && !isPureMemberTransform) return false;
|
|
23229
|
+
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression") && !analyzeHelperExpression(callee.object, environment, usedParameterIndices)) return false;
|
|
23230
|
+
return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
|
|
23231
|
+
}
|
|
23232
|
+
if (isNodeOfType(node, "SpreadElement")) return analyzeHelperExpression(node.argument, environment, usedParameterIndices);
|
|
23233
|
+
return false;
|
|
23234
|
+
};
|
|
23235
|
+
const helperSummaryCache = /* @__PURE__ */ new WeakMap();
|
|
23236
|
+
const summarizeHelperReturn = (functionNode) => {
|
|
23237
|
+
if (!isFunctionLike$1(functionNode) || !isModuleFunction(functionNode)) return null;
|
|
23238
|
+
if (helperSummaryCache.has(functionNode)) return helperSummaryCache.get(functionNode) ?? null;
|
|
23239
|
+
if (isAsyncOrGeneratorFunction(functionNode)) {
|
|
23240
|
+
helperSummaryCache.set(functionNode, null);
|
|
23241
|
+
return null;
|
|
23242
|
+
}
|
|
23243
|
+
const parameterIndices = /* @__PURE__ */ new Map();
|
|
23244
|
+
const parameters = getFunctionParameters(functionNode);
|
|
23245
|
+
for (let parameterIndex = 0; parameterIndex < parameters.length; parameterIndex += 1) {
|
|
23246
|
+
const parameter = parameters[parameterIndex];
|
|
23247
|
+
if (!parameter || !isNodeOfType(parameter, "Identifier") || parameterIndices.has(parameter.name)) {
|
|
23248
|
+
helperSummaryCache.set(functionNode, null);
|
|
23249
|
+
return null;
|
|
23250
|
+
}
|
|
23251
|
+
parameterIndices.set(parameter.name, parameterIndex);
|
|
23252
|
+
}
|
|
23253
|
+
const environment = {
|
|
23254
|
+
parameterIndices,
|
|
23255
|
+
recursiveNames: getFunctionBindingNames(functionNode),
|
|
23256
|
+
shadowedGlobalNames: collectModuleBindingNames(functionNode)
|
|
23257
|
+
};
|
|
23258
|
+
const usedParameterIndices = /* @__PURE__ */ new Set();
|
|
23259
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) {
|
|
23260
|
+
if (!analyzeHelperExpression(functionNode.body, environment, usedParameterIndices)) {
|
|
23261
|
+
helperSummaryCache.set(functionNode, null);
|
|
23262
|
+
return null;
|
|
23263
|
+
}
|
|
23264
|
+
} else {
|
|
23265
|
+
const controlFlowSummary = analyzeHelperStatements(functionNode.body.body ?? [], environment, usedParameterIndices, analyzeHelperExpression);
|
|
23266
|
+
if (!controlFlowSummary.isValid || controlFlowSummary.canContinue) {
|
|
23267
|
+
helperSummaryCache.set(functionNode, null);
|
|
23268
|
+
return null;
|
|
23269
|
+
}
|
|
23270
|
+
}
|
|
23271
|
+
const summary = { usedParameterIndices };
|
|
23272
|
+
helperSummaryCache.set(functionNode, summary);
|
|
23273
|
+
return summary;
|
|
23274
|
+
};
|
|
23275
|
+
const resolveValueHelperFunction = (analysis, callee, currentFilename) => {
|
|
23276
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
23277
|
+
const reference = getRef(analysis, callee);
|
|
23278
|
+
if (!reference?.resolved) return null;
|
|
23279
|
+
const importDefinition = reference.resolved.defs.find((definition) => definition.type === "ImportBinding");
|
|
23280
|
+
if (importDefinition) {
|
|
23281
|
+
if (!currentFilename) return null;
|
|
23282
|
+
const specifier = importDefinition.node;
|
|
23283
|
+
if (!isNodeOfType(specifier, "ImportSpecifier") && !isNodeOfType(specifier, "ImportDefaultSpecifier")) return null;
|
|
23284
|
+
const importDeclaration = specifier.parent;
|
|
23285
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
|
|
23286
|
+
if (importDeclaration.importKind === "type" || isNodeOfType(specifier, "ImportSpecifier") && specifier.importKind === "type") return null;
|
|
23287
|
+
const source = importDeclaration.source?.value;
|
|
23288
|
+
if (typeof source !== "string") return null;
|
|
23289
|
+
const exportedName = resolveImportedExportName(specifier);
|
|
23290
|
+
if (!exportedName) return null;
|
|
23291
|
+
return resolveCrossFileFunctionExport(currentFilename, source, exportedName);
|
|
23292
|
+
}
|
|
23293
|
+
const callable = resolveWrappedCallable(analysis, callee);
|
|
23294
|
+
return callable && isModuleFunction(callable) ? callable : null;
|
|
23295
|
+
};
|
|
22759
23296
|
const isLocallyConstructedObjectMember = (reference, memberExpression) => isNodeOfType(memberExpression, "MemberExpression") && reference.resolved?.defs.some((definition) => {
|
|
22760
23297
|
const definitionNode = definition.node;
|
|
22761
23298
|
return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
|
|
@@ -22841,38 +23378,55 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
22841
23378
|
const calleeRoot = getMemberRoot(callee);
|
|
22842
23379
|
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(calleeRoot, "Identifier") && PURE_GLOBAL_NAMESPACE_NAMES.has(calleeRoot.name) && getIdentifierBindingIdentity(analysis, calleeRoot) === null;
|
|
22843
23380
|
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
|
|
22844
|
-
if (isPureMemberTransform)
|
|
22845
|
-
|
|
22846
|
-
|
|
22847
|
-
|
|
22848
|
-
|
|
22849
|
-
|
|
22850
|
-
if (isNodeOfType(callee, "MemberExpression")) {
|
|
22851
|
-
evidence.hasUnknownSource = true;
|
|
23381
|
+
if (isPureGlobalCall || isPureMemberTransform) {
|
|
23382
|
+
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
23383
|
+
for (const argument of node.arguments ?? []) {
|
|
23384
|
+
if (isFunctionLike$1(argument)) continue;
|
|
23385
|
+
mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
23386
|
+
}
|
|
22852
23387
|
return evidence;
|
|
22853
23388
|
}
|
|
22854
23389
|
if (remainingCallFrames <= 0) {
|
|
22855
23390
|
evidence.hasUnknownSource = true;
|
|
22856
23391
|
return evidence;
|
|
22857
23392
|
}
|
|
22858
|
-
const
|
|
22859
|
-
if (
|
|
22860
|
-
|
|
23393
|
+
const localHelperFunction = resolveWrappedCallable(analysis, callee);
|
|
23394
|
+
if (localHelperFunction && !isModuleFunction(localHelperFunction)) {
|
|
23395
|
+
if (isAsyncOrGeneratorFunction(localHelperFunction) || functionInvokesItself(analysis, localHelperFunction)) {
|
|
23396
|
+
evidence.hasUnknownSource = true;
|
|
23397
|
+
return evidence;
|
|
23398
|
+
}
|
|
23399
|
+
const localHelperFrame = {
|
|
23400
|
+
functionNode: localHelperFunction,
|
|
23401
|
+
invocation: node,
|
|
23402
|
+
isDeferred: false,
|
|
23403
|
+
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23404
|
+
substitutions: buildSubstitutions(analysis, localHelperFunction, node.arguments ?? [], frame),
|
|
23405
|
+
currentFilename: frame.currentFilename
|
|
23406
|
+
};
|
|
23407
|
+
const returnedExpressions = getReturnedExpressions(localHelperFunction);
|
|
23408
|
+
if (returnedExpressions.length === 0) {
|
|
23409
|
+
evidence.hasUnknownSource = true;
|
|
23410
|
+
return evidence;
|
|
23411
|
+
}
|
|
23412
|
+
for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, localHelperFrame, remainingCallFrames - 1, new Set(visitedBindings)));
|
|
22861
23413
|
return evidence;
|
|
22862
23414
|
}
|
|
22863
|
-
const
|
|
22864
|
-
|
|
22865
|
-
|
|
22866
|
-
isDeferred: false,
|
|
22867
|
-
introducedBindings: /* @__PURE__ */ new Set(),
|
|
22868
|
-
substitutions: buildSubstitutions(analysis, callable, node.arguments ?? [], frame)
|
|
22869
|
-
};
|
|
22870
|
-
const returnedExpressions = getReturnedExpressions(callable);
|
|
22871
|
-
if (returnedExpressions.length === 0) {
|
|
23415
|
+
const helperFunction = resolveValueHelperFunction(analysis, callee, frame.currentFilename);
|
|
23416
|
+
const helperSummary = helperFunction ? summarizeHelperReturn(helperFunction) : null;
|
|
23417
|
+
if (!helperSummary) {
|
|
22872
23418
|
evidence.hasUnknownSource = true;
|
|
22873
23419
|
return evidence;
|
|
22874
23420
|
}
|
|
22875
|
-
|
|
23421
|
+
const argumentsForHelper = node.arguments ?? [];
|
|
23422
|
+
for (const parameterIndex of helperSummary.usedParameterIndices) {
|
|
23423
|
+
const argument = argumentsForHelper[parameterIndex];
|
|
23424
|
+
if (!argument) {
|
|
23425
|
+
evidence.hasUnknownSource = true;
|
|
23426
|
+
return evidence;
|
|
23427
|
+
}
|
|
23428
|
+
mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames - 1, new Set(visitedBindings)));
|
|
23429
|
+
}
|
|
22876
23430
|
return evidence;
|
|
22877
23431
|
}
|
|
22878
23432
|
if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
|
|
@@ -22890,6 +23444,20 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
22890
23444
|
}
|
|
22891
23445
|
return evidence;
|
|
22892
23446
|
};
|
|
23447
|
+
const collectRenderValueEvidence = (analysis, expression, componentFunction, currentFilename) => {
|
|
23448
|
+
const evidence = collectValueEvidence(analysis, expression, {
|
|
23449
|
+
functionNode: componentFunction,
|
|
23450
|
+
invocation: null,
|
|
23451
|
+
isDeferred: false,
|
|
23452
|
+
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23453
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
23454
|
+
currentFilename
|
|
23455
|
+
}, 1);
|
|
23456
|
+
return {
|
|
23457
|
+
sourceReferences: evidence.sourceReferences,
|
|
23458
|
+
isExclusivelyRenderKnown: evidence.sourceReferences.size > 0 && !evidence.hasUnknownSource && !evidence.hasDeferredIntroducedValue && !evidence.readsExternalValue
|
|
23459
|
+
};
|
|
23460
|
+
};
|
|
22893
23461
|
const findStateSetterReference = (analysis, callExpression) => {
|
|
22894
23462
|
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
22895
23463
|
const callee = stripParenExpression(callExpression.callee);
|
|
@@ -22952,8 +23520,8 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
|
|
|
22952
23520
|
}
|
|
22953
23521
|
return false;
|
|
22954
23522
|
};
|
|
22955
|
-
const collectEffectStateWriteFacts = (analysis, effectNode) => {
|
|
22956
|
-
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode);
|
|
23523
|
+
const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
|
|
23524
|
+
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
|
|
22957
23525
|
if (frames.length === 0) return [];
|
|
22958
23526
|
const effectHasCleanup = hasCleanup(analysis, effectNode);
|
|
22959
23527
|
const cleanupManagedStateDeclarators = /* @__PURE__ */ new Set();
|
|
@@ -22973,7 +23541,8 @@ const collectEffectStateWriteFacts = (analysis, effectNode) => {
|
|
|
22973
23541
|
invocation: callExpression,
|
|
22974
23542
|
isDeferred: frame.isDeferred,
|
|
22975
23543
|
introducedBindings: collectIntroducedBindings(analysis, writtenValue),
|
|
22976
|
-
substitutions: /* @__PURE__ */ new Map()
|
|
23544
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
23545
|
+
currentFilename
|
|
22977
23546
|
};
|
|
22978
23547
|
valueEvidence = emptyEvidence();
|
|
22979
23548
|
const returnedExpressions = getReturnedExpressions(writtenValue);
|
|
@@ -23022,7 +23591,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
23022
23591
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
23023
23592
|
if (!dependencyReferences) return;
|
|
23024
23593
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
23025
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
23594
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
23026
23595
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
23027
23596
|
context.report({
|
|
23028
23597
|
node: fact.callExpression,
|
|
@@ -24283,6 +24852,139 @@ const createRelativeImportSource = (filename, targetFilePath) => {
|
|
|
24283
24852
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
24284
24853
|
};
|
|
24285
24854
|
//#endregion
|
|
24855
|
+
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
24856
|
+
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
24857
|
+
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24858
|
+
const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24859
|
+
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24860
|
+
const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
|
|
24861
|
+
const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
|
|
24862
|
+
const createNonBarrelInfo = () => ({
|
|
24863
|
+
isBarrel: false,
|
|
24864
|
+
exportsByName: /* @__PURE__ */ new Map(),
|
|
24865
|
+
starExportSources: []
|
|
24866
|
+
});
|
|
24867
|
+
const addImportedBinding = (importedBindings, binding) => {
|
|
24868
|
+
importedBindings.set(binding.localName, {
|
|
24869
|
+
...binding,
|
|
24870
|
+
didExport: false
|
|
24871
|
+
});
|
|
24872
|
+
};
|
|
24873
|
+
const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
|
|
24874
|
+
for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
|
|
24875
|
+
localName: specifier.exportedName,
|
|
24876
|
+
importedName: specifier.localName,
|
|
24877
|
+
source,
|
|
24878
|
+
isTypeOnly: specifier.isTypeOnly
|
|
24879
|
+
});
|
|
24880
|
+
};
|
|
24881
|
+
const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
|
|
24882
|
+
const trimmedImportClause = importClause.trim();
|
|
24883
|
+
const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
|
|
24884
|
+
if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
|
|
24885
|
+
localName: namespaceMatch[1],
|
|
24886
|
+
importedName: "*",
|
|
24887
|
+
source,
|
|
24888
|
+
isTypeOnly: declarationIsTypeOnly
|
|
24889
|
+
});
|
|
24890
|
+
const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
|
|
24891
|
+
if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
|
|
24892
|
+
const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
|
|
24893
|
+
if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
|
|
24894
|
+
localName: defaultImportName,
|
|
24895
|
+
importedName: "default",
|
|
24896
|
+
source,
|
|
24897
|
+
isTypeOnly: declarationIsTypeOnly
|
|
24898
|
+
});
|
|
24899
|
+
};
|
|
24900
|
+
const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
|
|
24901
|
+
let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
|
|
24902
|
+
collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
|
|
24903
|
+
return "";
|
|
24904
|
+
});
|
|
24905
|
+
withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
|
|
24906
|
+
const isTypeOnly = Boolean(typeKeyword);
|
|
24907
|
+
if (namespaceExportName) {
|
|
24908
|
+
exportsByName.set(namespaceExportName, {
|
|
24909
|
+
exportedName: namespaceExportName,
|
|
24910
|
+
importedName: "*",
|
|
24911
|
+
source,
|
|
24912
|
+
isTypeOnly
|
|
24913
|
+
});
|
|
24914
|
+
return "";
|
|
24915
|
+
}
|
|
24916
|
+
if (specifiersText) {
|
|
24917
|
+
for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
|
|
24918
|
+
exportedName: specifier.exportedName,
|
|
24919
|
+
importedName: specifier.localName,
|
|
24920
|
+
source,
|
|
24921
|
+
isTypeOnly: specifier.isTypeOnly
|
|
24922
|
+
});
|
|
24923
|
+
return "";
|
|
24924
|
+
}
|
|
24925
|
+
starExportSources.push(source);
|
|
24926
|
+
return "";
|
|
24927
|
+
});
|
|
24928
|
+
withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
|
|
24929
|
+
for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
|
|
24930
|
+
const importedBinding = importedBindings.get(specifier.localName);
|
|
24931
|
+
if (!importedBinding) return _match;
|
|
24932
|
+
importedBinding.didExport = true;
|
|
24933
|
+
exportsByName.set(specifier.exportedName, {
|
|
24934
|
+
exportedName: specifier.exportedName,
|
|
24935
|
+
importedName: importedBinding.importedName,
|
|
24936
|
+
source: importedBinding.source,
|
|
24937
|
+
isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
|
|
24938
|
+
});
|
|
24939
|
+
}
|
|
24940
|
+
return "";
|
|
24941
|
+
});
|
|
24942
|
+
return withoutKnownDeclarations;
|
|
24943
|
+
};
|
|
24944
|
+
const hasUnexportedRuntimeImport = (importedBindings) => {
|
|
24945
|
+
for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
|
|
24946
|
+
return false;
|
|
24947
|
+
};
|
|
24948
|
+
const classifyBarrelModule = (sourceText) => {
|
|
24949
|
+
const strippedSource = stripJsComments(sourceText).trim();
|
|
24950
|
+
if (!strippedSource) return createNonBarrelInfo();
|
|
24951
|
+
const importedBindings = /* @__PURE__ */ new Map();
|
|
24952
|
+
const exportsByName = /* @__PURE__ */ new Map();
|
|
24953
|
+
const starExportSources = [];
|
|
24954
|
+
if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
|
|
24955
|
+
return {
|
|
24956
|
+
isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
|
|
24957
|
+
exportsByName,
|
|
24958
|
+
starExportSources
|
|
24959
|
+
};
|
|
24960
|
+
};
|
|
24961
|
+
const getBarrelIndexModuleInfo = (filePath) => {
|
|
24962
|
+
if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
|
|
24963
|
+
recordContentProbe(filePath);
|
|
24964
|
+
let fileStat;
|
|
24965
|
+
try {
|
|
24966
|
+
fileStat = fs.statSync(filePath);
|
|
24967
|
+
} catch {
|
|
24968
|
+
fileStat = null;
|
|
24969
|
+
}
|
|
24970
|
+
const cachedResult = barrelIndexModuleInfoCache.get(filePath);
|
|
24971
|
+
if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
|
|
24972
|
+
if (fileStat === null) return createNonBarrelInfo();
|
|
24973
|
+
let moduleInfo = createNonBarrelInfo();
|
|
24974
|
+
try {
|
|
24975
|
+
moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
|
|
24976
|
+
} catch {
|
|
24977
|
+
moduleInfo = createNonBarrelInfo();
|
|
24978
|
+
}
|
|
24979
|
+
barrelIndexModuleInfoCache.set(filePath, {
|
|
24980
|
+
mtimeMs: fileStat.mtimeMs,
|
|
24981
|
+
size: fileStat.size,
|
|
24982
|
+
moduleInfo
|
|
24983
|
+
});
|
|
24984
|
+
return moduleInfo;
|
|
24985
|
+
};
|
|
24986
|
+
const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
|
|
24987
|
+
//#endregion
|
|
24286
24988
|
//#region src/react-native-dependency-names.ts
|
|
24287
24989
|
const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
|
|
24288
24990
|
"expo",
|
|
@@ -24485,6 +25187,35 @@ const classifyReactNativeFileTarget = (context) => {
|
|
|
24485
25187
|
};
|
|
24486
25188
|
const isReactNativeFileActive = (context) => classifyReactNativeFileTarget(context) !== "web";
|
|
24487
25189
|
//#endregion
|
|
25190
|
+
//#region src/plugin/utils/resolve-barrel-export-file-path.ts
|
|
25191
|
+
const getUniqueFilePath = (filePaths) => {
|
|
25192
|
+
const uniqueFilePaths = new Set(filePaths);
|
|
25193
|
+
if (uniqueFilePaths.size !== 1) return null;
|
|
25194
|
+
const [filePath] = uniqueFilePaths;
|
|
25195
|
+
return filePath ?? null;
|
|
25196
|
+
};
|
|
25197
|
+
const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
|
|
25198
|
+
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
|
|
25199
|
+
if (!resolvedTargetPath) return null;
|
|
25200
|
+
const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
|
|
25201
|
+
if (nestedTargetPath) return nestedTargetPath;
|
|
25202
|
+
return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
|
|
25203
|
+
};
|
|
25204
|
+
const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
|
|
25205
|
+
if (visitedFilePaths.has(barrelFilePath)) return null;
|
|
25206
|
+
visitedFilePaths.add(barrelFilePath);
|
|
25207
|
+
const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
|
|
25208
|
+
if (!moduleInfo.isBarrel) return null;
|
|
25209
|
+
const target = moduleInfo.exportsByName.get(exportedName);
|
|
25210
|
+
if (target) {
|
|
25211
|
+
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
|
|
25212
|
+
if (!resolvedTargetPath) return null;
|
|
25213
|
+
return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
|
|
25214
|
+
}
|
|
25215
|
+
if (exportedName === "default") return null;
|
|
25216
|
+
return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
|
|
25217
|
+
};
|
|
25218
|
+
//#endregion
|
|
24488
25219
|
//#region src/plugin/rules/bundle-size/no-barrel-import.ts
|
|
24489
25220
|
const TYPE_DECLARATION_FILE_PATTERN = /\.d\.[cm]?ts$/;
|
|
24490
25221
|
const SERVER_ONLY_FILE_PATTERN = /\.server\.[cm]?[jt]sx?$/;
|
|
@@ -24814,7 +25545,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
24814
25545
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
24815
25546
|
return false;
|
|
24816
25547
|
};
|
|
24817
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES
|
|
25548
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
24818
25549
|
"forEach",
|
|
24819
25550
|
"map",
|
|
24820
25551
|
"filter",
|
|
@@ -24835,7 +25566,7 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
24835
25566
|
if (parent.callee === functionNode) return true;
|
|
24836
25567
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
24837
25568
|
const callee = parent.callee;
|
|
24838
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES
|
|
25569
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
24839
25570
|
};
|
|
24840
25571
|
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
24841
25572
|
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
@@ -25779,57 +26510,6 @@ const noDefaultProps = defineRule({
|
|
|
25779
26510
|
} })
|
|
25780
26511
|
});
|
|
25781
26512
|
//#endregion
|
|
25782
|
-
//#region src/plugin/rules/state-and-effects/no-derived-state.ts
|
|
25783
|
-
const getStateName$1 = (stateDeclarator) => {
|
|
25784
|
-
if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
|
|
25785
|
-
if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
|
|
25786
|
-
const stateBinding = stateDeclarator.id.elements?.[0];
|
|
25787
|
-
if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
|
|
25788
|
-
const setterBinding = stateDeclarator.id.elements?.[1];
|
|
25789
|
-
if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
|
|
25790
|
-
if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
|
|
25791
|
-
return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
|
|
25792
|
-
};
|
|
25793
|
-
const noDerivedState = defineRule({
|
|
25794
|
-
id: "no-derived-state",
|
|
25795
|
-
title: "Derived value copied into state",
|
|
25796
|
-
severity: "warn",
|
|
25797
|
-
tags: ["test-noise"],
|
|
25798
|
-
recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into useState through a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
|
|
25799
|
-
create: (context) => ({ CallExpression(node) {
|
|
25800
|
-
if (!isUseEffect(node)) return;
|
|
25801
|
-
const analysis = getProgramAnalysis(node);
|
|
25802
|
-
if (!analysis) return;
|
|
25803
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
25804
|
-
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
25805
|
-
const stateName = getStateName$1(fact.stateDeclarator);
|
|
25806
|
-
context.report({
|
|
25807
|
-
node: fact.callExpression,
|
|
25808
|
-
message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
|
|
25809
|
-
});
|
|
25810
|
-
}
|
|
25811
|
-
} })
|
|
25812
|
-
});
|
|
25813
|
-
//#endregion
|
|
25814
|
-
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
25815
|
-
const noDerivedStateEffect = defineRule({
|
|
25816
|
-
id: "no-derived-state-effect",
|
|
25817
|
-
title: "Derived state stored in an effect",
|
|
25818
|
-
severity: "warn",
|
|
25819
|
-
tags: ["test-noise"],
|
|
25820
|
-
recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
25821
|
-
create: (context) => ({ CallExpression(node) {
|
|
25822
|
-
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
25823
|
-
const analysis = getProgramAnalysis(node);
|
|
25824
|
-
if (!analysis) return;
|
|
25825
|
-
if (!collectEffectStateWriteFacts(analysis, node).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
25826
|
-
context.report({
|
|
25827
|
-
node,
|
|
25828
|
-
message: "You pay an extra render for state you can derive from other values."
|
|
25829
|
-
});
|
|
25830
|
-
} })
|
|
25831
|
-
});
|
|
25832
|
-
//#endregion
|
|
25833
26513
|
//#region src/plugin/utils/is-component-function.ts
|
|
25834
26514
|
const isFunctionAssignedToComponent = (functionNode) => {
|
|
25835
26515
|
let cursor = functionNode.parent ?? null;
|
|
@@ -25963,6 +26643,236 @@ const createComponentPropStackTracker = (callbacks) => {
|
|
|
25963
26643
|
};
|
|
25964
26644
|
};
|
|
25965
26645
|
//#endregion
|
|
26646
|
+
//#region src/plugin/rules/state-and-effects/utils/collect-render-state-write-facts.ts
|
|
26647
|
+
const findReferenceDeclarator = (reference) => {
|
|
26648
|
+
for (const definition of reference.resolved?.defs ?? []) {
|
|
26649
|
+
const definitionNode = definition.node;
|
|
26650
|
+
if (isNodeOfType(definitionNode, "VariableDeclarator")) return definitionNode;
|
|
26651
|
+
}
|
|
26652
|
+
return null;
|
|
26653
|
+
};
|
|
26654
|
+
const resolveSimpleRenderSource = (analysis, expression, visitedBindings = /* @__PURE__ */ new Set()) => {
|
|
26655
|
+
const node = stripParenExpression(expression);
|
|
26656
|
+
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26657
|
+
const reference = getRef(analysis, node);
|
|
26658
|
+
if (!reference?.resolved || visitedBindings.has(reference.resolved)) return null;
|
|
26659
|
+
if (isProp(analysis, reference)) {
|
|
26660
|
+
if (isWholePropsObjectReference(analysis, reference)) return null;
|
|
26661
|
+
return { bindingIdentity: reference.resolved };
|
|
26662
|
+
}
|
|
26663
|
+
if (isState(analysis, reference)) {
|
|
26664
|
+
const stateDeclarator = getUseStateDecl(analysis, reference);
|
|
26665
|
+
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || isExternallyDrivenState(analysis, reference)) return null;
|
|
26666
|
+
return { bindingIdentity: reference.resolved };
|
|
26667
|
+
}
|
|
26668
|
+
if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
|
|
26669
|
+
const declarator = findReferenceDeclarator(reference);
|
|
26670
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
26671
|
+
const nextVisitedBindings = new Set(visitedBindings);
|
|
26672
|
+
nextVisitedBindings.add(reference.resolved);
|
|
26673
|
+
return resolveSimpleRenderSource(analysis, declarator.init, nextVisitedBindings);
|
|
26674
|
+
};
|
|
26675
|
+
const doesEvidenceMatchSource = (evidence, source) => evidence.isExclusivelyRenderKnown && evidence.sourceReferences.size > 0 && [...evidence.sourceReferences].every((sourceReference) => sourceReference.resolved === source.bindingIdentity);
|
|
26676
|
+
const getDirectBranchStatements = (branch) => {
|
|
26677
|
+
if (isNodeOfType(branch, "BlockStatement")) return branch.body ?? [];
|
|
26678
|
+
return [branch];
|
|
26679
|
+
};
|
|
26680
|
+
const getDirectCallExpression = (statement) => {
|
|
26681
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26682
|
+
const expression = stripParenExpression(statement.expression);
|
|
26683
|
+
return isNodeOfType(expression, "CallExpression") ? expression : null;
|
|
26684
|
+
};
|
|
26685
|
+
const getDirectAssignmentExpression = (statement) => {
|
|
26686
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26687
|
+
const expression = stripParenExpression(statement.expression);
|
|
26688
|
+
return isNodeOfType(expression, "AssignmentExpression") && expression.operator === "=" ? expression : null;
|
|
26689
|
+
};
|
|
26690
|
+
const findDirectStateSetterReference = (analysis, callExpression) => {
|
|
26691
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
26692
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
26693
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
26694
|
+
const reference = getRef(analysis, callee);
|
|
26695
|
+
return reference && isStateSetter(analysis, reference) ? reference : null;
|
|
26696
|
+
};
|
|
26697
|
+
const getStaticRefCurrentReference = (analysis, expression) => {
|
|
26698
|
+
const node = stripParenExpression(expression);
|
|
26699
|
+
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.object, "Identifier") || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
|
|
26700
|
+
const reference = getRef(analysis, node.object);
|
|
26701
|
+
if (!reference) return null;
|
|
26702
|
+
const declarator = findReferenceDeclarator(reference);
|
|
26703
|
+
return declarator && isGenuineReactHookDeclarator(analysis, declarator, "useRef") ? reference : null;
|
|
26704
|
+
};
|
|
26705
|
+
const getStateTracker = (analysis, expression) => {
|
|
26706
|
+
const node = stripParenExpression(expression);
|
|
26707
|
+
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26708
|
+
const reference = getRef(analysis, node);
|
|
26709
|
+
if (!reference || !isState(analysis, reference)) return null;
|
|
26710
|
+
const declarator = getUseStateDecl(analysis, reference);
|
|
26711
|
+
if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useState")) return null;
|
|
26712
|
+
return {
|
|
26713
|
+
kind: "state",
|
|
26714
|
+
declarator
|
|
26715
|
+
};
|
|
26716
|
+
};
|
|
26717
|
+
const getRefTracker = (analysis, expression) => {
|
|
26718
|
+
const reference = getStaticRefCurrentReference(analysis, expression);
|
|
26719
|
+
return reference ? {
|
|
26720
|
+
kind: "ref",
|
|
26721
|
+
reference
|
|
26722
|
+
} : null;
|
|
26723
|
+
};
|
|
26724
|
+
const getTrackerInitializer = (tracker) => {
|
|
26725
|
+
const declarator = tracker.kind === "state" ? tracker.declarator : findReferenceDeclarator(tracker.reference);
|
|
26726
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return null;
|
|
26727
|
+
const initializer = declarator.init.arguments?.[0];
|
|
26728
|
+
return initializer ? initializer : null;
|
|
26729
|
+
};
|
|
26730
|
+
const isTrackerSynchronized = (analysis, guard) => {
|
|
26731
|
+
for (const statement of guard.statements) {
|
|
26732
|
+
if (guard.tracker.kind === "state") {
|
|
26733
|
+
const callExpression = getDirectCallExpression(statement);
|
|
26734
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26735
|
+
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26736
|
+
if (!setterReference || getUseStateDecl(analysis, setterReference) !== guard.tracker.declarator || (callExpression.arguments ?? []).length !== 1) continue;
|
|
26737
|
+
const writtenValue = callExpression.arguments?.[0];
|
|
26738
|
+
if (writtenValue && resolveSimpleRenderSource(analysis, writtenValue)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26739
|
+
continue;
|
|
26740
|
+
}
|
|
26741
|
+
const assignmentExpression = getDirectAssignmentExpression(statement);
|
|
26742
|
+
if (!assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression")) continue;
|
|
26743
|
+
if (getStaticRefCurrentReference(analysis, assignmentExpression.left)?.resolved !== guard.tracker.reference.resolved) continue;
|
|
26744
|
+
if (resolveSimpleRenderSource(analysis, assignmentExpression.right)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26745
|
+
}
|
|
26746
|
+
return false;
|
|
26747
|
+
};
|
|
26748
|
+
const parseRenderTrackerGuard = (analysis, statement) => {
|
|
26749
|
+
if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isNodeOfType(statement.test, "BinaryExpression") || statement.test.operator !== "!==" || !isNodeOfType(statement.consequent, "BlockStatement")) return null;
|
|
26750
|
+
const left = statement.test.left;
|
|
26751
|
+
const right = statement.test.right;
|
|
26752
|
+
const candidates = [{
|
|
26753
|
+
sourceExpression: left,
|
|
26754
|
+
trackerExpression: right
|
|
26755
|
+
}, {
|
|
26756
|
+
sourceExpression: right,
|
|
26757
|
+
trackerExpression: left
|
|
26758
|
+
}];
|
|
26759
|
+
for (const candidate of candidates) {
|
|
26760
|
+
const source = resolveSimpleRenderSource(analysis, candidate.sourceExpression);
|
|
26761
|
+
if (!source) continue;
|
|
26762
|
+
const tracker = getStateTracker(analysis, candidate.trackerExpression) ?? getRefTracker(analysis, candidate.trackerExpression);
|
|
26763
|
+
if (!tracker) continue;
|
|
26764
|
+
const trackerInitializer = getTrackerInitializer(tracker);
|
|
26765
|
+
if (!trackerInitializer || resolveSimpleRenderSource(analysis, trackerInitializer)?.bindingIdentity !== source.bindingIdentity) continue;
|
|
26766
|
+
const guard = {
|
|
26767
|
+
source,
|
|
26768
|
+
tracker,
|
|
26769
|
+
statements: getDirectBranchStatements(statement.consequent)
|
|
26770
|
+
};
|
|
26771
|
+
return isTrackerSynchronized(analysis, guard) ? guard : null;
|
|
26772
|
+
}
|
|
26773
|
+
return null;
|
|
26774
|
+
};
|
|
26775
|
+
const isExclusiveDestinationSetterReference = (setterReference, callExpression) => {
|
|
26776
|
+
if (!setterReference.resolved || !isNodeOfType(callExpression, "CallExpression")) return false;
|
|
26777
|
+
const references = setterReference.resolved.references.filter((reference) => !reference.init);
|
|
26778
|
+
if (references.length !== 1) return false;
|
|
26779
|
+
return references[0].identifier === callExpression.callee;
|
|
26780
|
+
};
|
|
26781
|
+
const getStateInitializer = (stateDeclarator) => {
|
|
26782
|
+
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression")) return null;
|
|
26783
|
+
const initializer = stateDeclarator.init.arguments?.[0];
|
|
26784
|
+
return initializer ? initializer : null;
|
|
26785
|
+
};
|
|
26786
|
+
const collectRenderStateWriteFacts = (analysis, componentBody, currentFilename) => {
|
|
26787
|
+
if (!isNodeOfType(componentBody, "BlockStatement") || !componentBody.parent || !isFunctionLike$1(componentBody.parent)) return [];
|
|
26788
|
+
const componentFunction = componentBody.parent;
|
|
26789
|
+
const facts = [];
|
|
26790
|
+
for (const statement of componentBody.body ?? []) {
|
|
26791
|
+
const guard = parseRenderTrackerGuard(analysis, statement);
|
|
26792
|
+
if (!guard) continue;
|
|
26793
|
+
for (const branchStatement of guard.statements) {
|
|
26794
|
+
const callExpression = getDirectCallExpression(branchStatement);
|
|
26795
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26796
|
+
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26797
|
+
if (!setterReference || (callExpression.arguments ?? []).length !== 1 || !isExclusiveDestinationSetterReference(setterReference, callExpression)) continue;
|
|
26798
|
+
const stateDeclarator = getUseStateDecl(analysis, setterReference);
|
|
26799
|
+
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || guard.tracker.kind === "state" && stateDeclarator === guard.tracker.declarator) continue;
|
|
26800
|
+
const writtenValue = callExpression.arguments?.[0];
|
|
26801
|
+
const initializer = getStateInitializer(stateDeclarator);
|
|
26802
|
+
if (!writtenValue || !initializer || isFunctionLike$1(writtenValue) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, writtenValue, componentFunction, currentFilename), guard.source) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, initializer, componentFunction, currentFilename), guard.source)) continue;
|
|
26803
|
+
facts.push({
|
|
26804
|
+
callExpression,
|
|
26805
|
+
stateDeclarator
|
|
26806
|
+
});
|
|
26807
|
+
}
|
|
26808
|
+
}
|
|
26809
|
+
return facts;
|
|
26810
|
+
};
|
|
26811
|
+
//#endregion
|
|
26812
|
+
//#region src/plugin/rules/state-and-effects/no-derived-state.ts
|
|
26813
|
+
const getStateName$1 = (stateDeclarator) => {
|
|
26814
|
+
if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
|
|
26815
|
+
if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
|
|
26816
|
+
const stateBinding = stateDeclarator.id.elements?.[0];
|
|
26817
|
+
if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
|
|
26818
|
+
const setterBinding = stateDeclarator.id.elements?.[1];
|
|
26819
|
+
if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
|
|
26820
|
+
if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
|
|
26821
|
+
return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
|
|
26822
|
+
};
|
|
26823
|
+
const noDerivedState = defineRule({
|
|
26824
|
+
id: "no-derived-state",
|
|
26825
|
+
title: "Derived value copied into state",
|
|
26826
|
+
severity: "warn",
|
|
26827
|
+
tags: ["test-noise"],
|
|
26828
|
+
recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into state and synchronizing it during render or through an effect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
|
|
26829
|
+
create: (context) => {
|
|
26830
|
+
const reportStateWrite = (callExpression, stateDeclarator) => {
|
|
26831
|
+
const stateName = getStateName$1(stateDeclarator);
|
|
26832
|
+
context.report({
|
|
26833
|
+
node: callExpression,
|
|
26834
|
+
message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
|
|
26835
|
+
});
|
|
26836
|
+
};
|
|
26837
|
+
return {
|
|
26838
|
+
...createComponentPropStackTracker({ onComponentEnter: (componentBody) => {
|
|
26839
|
+
if (!componentBody) return;
|
|
26840
|
+
const analysis = getProgramAnalysis(componentBody);
|
|
26841
|
+
if (!analysis) return;
|
|
26842
|
+
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26843
|
+
} }).visitors,
|
|
26844
|
+
CallExpression(node) {
|
|
26845
|
+
if (!isUseEffect(node)) return;
|
|
26846
|
+
const analysis = getProgramAnalysis(node);
|
|
26847
|
+
if (!analysis) return;
|
|
26848
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
26849
|
+
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
26850
|
+
reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26851
|
+
}
|
|
26852
|
+
}
|
|
26853
|
+
};
|
|
26854
|
+
}
|
|
26855
|
+
});
|
|
26856
|
+
//#endregion
|
|
26857
|
+
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
26858
|
+
const noDerivedStateEffect = defineRule({
|
|
26859
|
+
id: "no-derived-state-effect",
|
|
26860
|
+
title: "Derived state stored in an effect",
|
|
26861
|
+
severity: "warn",
|
|
26862
|
+
tags: ["test-noise"],
|
|
26863
|
+
recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
26864
|
+
create: (context) => ({ CallExpression(node) {
|
|
26865
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
26866
|
+
const analysis = getProgramAnalysis(node);
|
|
26867
|
+
if (!analysis) return;
|
|
26868
|
+
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
26869
|
+
context.report({
|
|
26870
|
+
node,
|
|
26871
|
+
message: "You pay an extra render for state you can derive from other values."
|
|
26872
|
+
});
|
|
26873
|
+
} })
|
|
26874
|
+
});
|
|
26875
|
+
//#endregion
|
|
25966
26876
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
25967
26877
|
const isInitialOnlyPropName = (propName) => {
|
|
25968
26878
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -27468,14 +28378,14 @@ const hasDocumentClassListMutation = (node) => {
|
|
|
27468
28378
|
//#endregion
|
|
27469
28379
|
//#region src/plugin/rules/state-and-effects/no-effect-event-handler.ts
|
|
27470
28380
|
const hasEventLikeNode = (node) => findTriggeredSideEffectCalleeName(node) !== null || hasDocumentClassListMutation(node);
|
|
27471
|
-
const unwrapChainExpression$
|
|
28381
|
+
const unwrapChainExpression$2 = (node) => {
|
|
27472
28382
|
if (!node) return null;
|
|
27473
28383
|
if (isNodeOfType(node, "ChainExpression")) return node.expression;
|
|
27474
28384
|
return node;
|
|
27475
28385
|
};
|
|
27476
28386
|
const collectGuardExpressions = (node, into) => {
|
|
27477
28387
|
if (!node) return;
|
|
27478
|
-
const unwrappedNode = unwrapChainExpression$
|
|
28388
|
+
const unwrappedNode = unwrapChainExpression$2(node);
|
|
27479
28389
|
if (!unwrappedNode) return;
|
|
27480
28390
|
const rootIdentifierName = getRootIdentifierName(unwrappedNode);
|
|
27481
28391
|
if (rootIdentifierName) {
|
|
@@ -27506,7 +28416,7 @@ const isReturnOnlyStatement = (node) => {
|
|
|
27506
28416
|
};
|
|
27507
28417
|
const hasEventLikeRemainingStatements = (statements) => statements.some((statement) => !isNodeOfType(statement, "ReturnStatement") && hasEventLikeNode(statement));
|
|
27508
28418
|
const doesGuardMatchDependency = (guardExpression, dependencyExpression) => {
|
|
27509
|
-
const unwrappedDependencyExpression = unwrapChainExpression$
|
|
28419
|
+
const unwrappedDependencyExpression = unwrapChainExpression$2(dependencyExpression);
|
|
27510
28420
|
if (!unwrappedDependencyExpression) return false;
|
|
27511
28421
|
if (areExpressionsStructurallyEqual(guardExpression.expression, unwrappedDependencyExpression)) return true;
|
|
27512
28422
|
return isNodeOfType(unwrappedDependencyExpression, "Identifier") && unwrappedDependencyExpression.name === guardExpression.rootIdentifierName;
|
|
@@ -29565,6 +30475,405 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
29565
30475
|
} })
|
|
29566
30476
|
});
|
|
29567
30477
|
//#endregion
|
|
30478
|
+
//#region src/plugin/utils/find-enclosing-jsx-opening-element.ts
|
|
30479
|
+
const findEnclosingJsxOpeningElement = (node) => {
|
|
30480
|
+
let cursor = node.parent;
|
|
30481
|
+
while (cursor) {
|
|
30482
|
+
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
30483
|
+
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
30484
|
+
cursor = cursor.parent ?? null;
|
|
30485
|
+
}
|
|
30486
|
+
return null;
|
|
30487
|
+
};
|
|
30488
|
+
//#endregion
|
|
30489
|
+
//#region src/plugin/utils/has-client-render-evidence.ts
|
|
30490
|
+
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
30491
|
+
if (fileHasUseClientDirective) return true;
|
|
30492
|
+
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
30493
|
+
if (displayName && isReactHookName(displayName)) return true;
|
|
30494
|
+
let callsHook = false;
|
|
30495
|
+
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
30496
|
+
if (callsHook) return false;
|
|
30497
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
30498
|
+
callsHook = true;
|
|
30499
|
+
return false;
|
|
30500
|
+
}
|
|
30501
|
+
});
|
|
30502
|
+
return callsHook;
|
|
30503
|
+
};
|
|
30504
|
+
//#endregion
|
|
30505
|
+
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
30506
|
+
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
30507
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
30508
|
+
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
30509
|
+
return false;
|
|
30510
|
+
};
|
|
30511
|
+
//#endregion
|
|
30512
|
+
//#region src/plugin/utils/read-initial-state-boolean.ts
|
|
30513
|
+
const readLogicalResult = (operator, leftResult, rightResult) => {
|
|
30514
|
+
if (operator === "&&") {
|
|
30515
|
+
if (leftResult === false || rightResult === false) return false;
|
|
30516
|
+
if (leftResult === true && rightResult === true) return true;
|
|
30517
|
+
return null;
|
|
30518
|
+
}
|
|
30519
|
+
if (leftResult === true || rightResult === true) return true;
|
|
30520
|
+
if (leftResult === false && rightResult === false) return false;
|
|
30521
|
+
return null;
|
|
30522
|
+
};
|
|
30523
|
+
const readIdentifierInitialStateBoolean = (identifier, scopes, visitedBindings, allowLazyInitializer) => {
|
|
30524
|
+
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
30525
|
+
if (identifier.name === "undefined" && scopes.isGlobalReference(identifier)) return false;
|
|
30526
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
30527
|
+
if (!binding || visitedBindings.has(binding.bindingIdentifier)) return null;
|
|
30528
|
+
visitedBindings.add(binding.bindingIdentifier);
|
|
30529
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
30530
|
+
if (!declarator?.init || scopes.symbolFor(identifier)?.declarationNode !== declarator) return null;
|
|
30531
|
+
const initializer = stripParenExpression(declarator.init);
|
|
30532
|
+
if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements?.[0] === binding.bindingIdentifier && isReactApiCall(initializer, "useState", scopes, { allowGlobalReactNamespace: true }) && isNodeOfType(initializer, "CallExpression")) {
|
|
30533
|
+
const initialState = initializer.arguments?.[0];
|
|
30534
|
+
if (!initialState) return false;
|
|
30535
|
+
if (initialState.type === "SpreadElement") return null;
|
|
30536
|
+
return readInitialStateBooleanInternal(initialState, scopes, visitedBindings, true);
|
|
30537
|
+
}
|
|
30538
|
+
const declaration = declarator.parent;
|
|
30539
|
+
if (!isNodeOfType(declarator.id, "Identifier") || declarator.id !== binding.bindingIdentifier || !isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const") return null;
|
|
30540
|
+
return readInitialStateBooleanInternal(initializer, scopes, visitedBindings, allowLazyInitializer);
|
|
30541
|
+
};
|
|
30542
|
+
const readInitialStateBooleanInternal = (expression, scopes, visitedBindings, allowLazyInitializer) => {
|
|
30543
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
30544
|
+
if (isNodeOfType(unwrappedExpression, "Literal")) return Boolean(unwrappedExpression.value);
|
|
30545
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) return readIdentifierInitialStateBoolean(unwrappedExpression, scopes, visitedBindings, allowLazyInitializer);
|
|
30546
|
+
if (allowLazyInitializer && (isNodeOfType(unwrappedExpression, "ArrowFunctionExpression") || isNodeOfType(unwrappedExpression, "FunctionExpression"))) {
|
|
30547
|
+
if (unwrappedExpression.async || isNodeOfType(unwrappedExpression, "FunctionExpression") && unwrappedExpression.generator) return null;
|
|
30548
|
+
if (!isNodeOfType(unwrappedExpression.body, "BlockStatement")) return readInitialStateBooleanInternal(unwrappedExpression.body, scopes, visitedBindings, false);
|
|
30549
|
+
if (unwrappedExpression.body.body.length !== 1) return null;
|
|
30550
|
+
const returnStatement = unwrappedExpression.body.body[0];
|
|
30551
|
+
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return null;
|
|
30552
|
+
return readInitialStateBooleanInternal(returnStatement.argument, scopes, visitedBindings, false);
|
|
30553
|
+
}
|
|
30554
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
30555
|
+
const argumentResult = readInitialStateBooleanInternal(unwrappedExpression.argument, scopes, visitedBindings, false);
|
|
30556
|
+
return argumentResult === null ? null : !argumentResult;
|
|
30557
|
+
}
|
|
30558
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression") && (unwrappedExpression.operator === "&&" || unwrappedExpression.operator === "||")) return readLogicalResult(unwrappedExpression.operator, readInitialStateBooleanInternal(unwrappedExpression.left, scopes, new Set(visitedBindings), false), readInitialStateBooleanInternal(unwrappedExpression.right, scopes, new Set(visitedBindings), false));
|
|
30559
|
+
return null;
|
|
30560
|
+
};
|
|
30561
|
+
const readInitialStateBoolean = (expression, scopes) => readInitialStateBooleanInternal(expression, scopes, /* @__PURE__ */ new Set(), false);
|
|
30562
|
+
//#endregion
|
|
30563
|
+
//#region src/plugin/utils/is-after-client-only-early-return.ts
|
|
30564
|
+
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode, scopes) => {
|
|
30565
|
+
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
30566
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
30567
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
30568
|
+
let currentNode = node;
|
|
30569
|
+
while (currentNode) {
|
|
30570
|
+
ancestors.add(currentNode);
|
|
30571
|
+
currentNode = currentNode.parent ?? null;
|
|
30572
|
+
}
|
|
30573
|
+
for (const statement of body.body ?? []) {
|
|
30574
|
+
if (ancestors.has(statement)) return false;
|
|
30575
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
30576
|
+
const initialConditionResult = readInitialStateBoolean(statement.test, scopes);
|
|
30577
|
+
if (initialConditionResult === true && statementAlwaysExits(statement.consequent)) return true;
|
|
30578
|
+
if (initialConditionResult === false && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
|
|
30579
|
+
}
|
|
30580
|
+
return false;
|
|
30581
|
+
};
|
|
30582
|
+
//#endregion
|
|
30583
|
+
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
30584
|
+
const isGatedByFalsyInitialState = (node, scopes) => {
|
|
30585
|
+
let cursor = node;
|
|
30586
|
+
let parent = node.parent;
|
|
30587
|
+
while (parent) {
|
|
30588
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === cursor && (parent.operator === "&&" && readInitialStateBoolean(parent.left, scopes) === false || parent.operator === "||" && readInitialStateBoolean(parent.left, scopes) === true)) return true;
|
|
30589
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor && readInitialStateBoolean(parent.test, scopes) === false || parent.alternate === cursor && readInitialStateBoolean(parent.test, scopes) === true)) return true;
|
|
30590
|
+
if (isNodeOfType(parent, "IfStatement") && (parent.consequent === cursor && readInitialStateBoolean(parent.test, scopes) === false || parent.alternate === cursor && readInitialStateBoolean(parent.test, scopes) === true)) return true;
|
|
30591
|
+
cursor = parent;
|
|
30592
|
+
parent = parent.parent ?? null;
|
|
30593
|
+
}
|
|
30594
|
+
return false;
|
|
30595
|
+
};
|
|
30596
|
+
//#endregion
|
|
30597
|
+
//#region src/plugin/rules/performance/no-hydration-branch-on-browser-global.ts
|
|
30598
|
+
const evaluateEquality = (operator, left, right) => {
|
|
30599
|
+
if (operator === "===" || operator === "==") return left === right;
|
|
30600
|
+
if (operator === "!==" || operator === "!=") return left !== right;
|
|
30601
|
+
return null;
|
|
30602
|
+
};
|
|
30603
|
+
const readTypeofBrowserGlobal = (expression, context) => {
|
|
30604
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
30605
|
+
if (!isNodeOfType(unwrappedExpression, "UnaryExpression") || unwrappedExpression.operator !== "typeof") return null;
|
|
30606
|
+
const argument = stripParenExpression(unwrappedExpression.argument);
|
|
30607
|
+
if (isNodeOfType(argument, "Identifier")) return (argument.name === "window" || argument.name === "document") && context.scopes.isGlobalReference(argument) ? argument.name : null;
|
|
30608
|
+
if (!isNodeOfType(argument, "MemberExpression") || argument.computed || !isNodeOfType(argument.object, "Identifier") || argument.object.name !== "globalThis" || !context.scopes.isGlobalReference(argument.object) || !isNodeOfType(argument.property, "Identifier") || argument.property.name !== "window" && argument.property.name !== "document") return null;
|
|
30609
|
+
return argument.property.name;
|
|
30610
|
+
};
|
|
30611
|
+
const matchBrowserPredicate = (expression, context) => {
|
|
30612
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
30613
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
30614
|
+
const innerMatch = matchBrowserPredicate(unwrappedExpression.argument, context);
|
|
30615
|
+
return innerMatch ? {
|
|
30616
|
+
browserGlobalName: innerMatch.browserGlobalName,
|
|
30617
|
+
clientResult: !innerMatch.clientResult,
|
|
30618
|
+
serverResult: !innerMatch.serverResult
|
|
30619
|
+
} : null;
|
|
30620
|
+
}
|
|
30621
|
+
if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
|
|
30622
|
+
const leftGlobalName = readTypeofBrowserGlobal(unwrappedExpression.left, context);
|
|
30623
|
+
const rightGlobalName = readTypeofBrowserGlobal(unwrappedExpression.right, context);
|
|
30624
|
+
const leftString = isNodeOfType(unwrappedExpression.left, "Literal") ? unwrappedExpression.left.value : null;
|
|
30625
|
+
const rightString = isNodeOfType(unwrappedExpression.right, "Literal") ? unwrappedExpression.right.value : null;
|
|
30626
|
+
const browserGlobalName = leftGlobalName && typeof rightString === "string" ? leftGlobalName : rightGlobalName && typeof leftString === "string" ? rightGlobalName : null;
|
|
30627
|
+
const comparedType = leftGlobalName && typeof rightString === "string" ? rightString : rightGlobalName && typeof leftString === "string" ? leftString : null;
|
|
30628
|
+
if (!browserGlobalName || !comparedType) return null;
|
|
30629
|
+
const clientResult = evaluateEquality(unwrappedExpression.operator, "object", comparedType);
|
|
30630
|
+
const serverResult = evaluateEquality(unwrappedExpression.operator, "undefined", comparedType);
|
|
30631
|
+
if (clientResult === null || serverResult === null || clientResult === serverResult) return null;
|
|
30632
|
+
return {
|
|
30633
|
+
browserGlobalName,
|
|
30634
|
+
clientResult,
|
|
30635
|
+
serverResult
|
|
30636
|
+
};
|
|
30637
|
+
};
|
|
30638
|
+
const readLogicalConditionResult = (operator, leftResult, rightResult) => {
|
|
30639
|
+
if (operator === "&&") {
|
|
30640
|
+
if (leftResult === false || rightResult === false) return false;
|
|
30641
|
+
if (leftResult === true && rightResult === true) return true;
|
|
30642
|
+
return null;
|
|
30643
|
+
}
|
|
30644
|
+
if (leftResult === true || rightResult === true) return true;
|
|
30645
|
+
if (leftResult === false && rightResult === false) return false;
|
|
30646
|
+
return null;
|
|
30647
|
+
};
|
|
30648
|
+
const readHydrationConditionResult = (expression, context, runtime) => {
|
|
30649
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
30650
|
+
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
30651
|
+
if (predicateMatch) return predicateMatch[`${runtime}Result`];
|
|
30652
|
+
const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
|
|
30653
|
+
if (staticResult !== null) return staticResult;
|
|
30654
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
30655
|
+
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
|
|
30656
|
+
return argumentResult === null ? null : !argumentResult;
|
|
30657
|
+
}
|
|
30658
|
+
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
30659
|
+
return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime), readHydrationConditionResult(unwrappedExpression.right, context, runtime));
|
|
30660
|
+
};
|
|
30661
|
+
const matchHydrationCondition = (expression, context) => {
|
|
30662
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
30663
|
+
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
30664
|
+
if (predicateMatch) return {
|
|
30665
|
+
predicateMatch,
|
|
30666
|
+
predicateNode: unwrappedExpression
|
|
30667
|
+
};
|
|
30668
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationCondition(unwrappedExpression.argument, context);
|
|
30669
|
+
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
30670
|
+
const leftMatch = matchHydrationCondition(unwrappedExpression.left, context);
|
|
30671
|
+
const rightMatch = matchHydrationCondition(unwrappedExpression.right, context);
|
|
30672
|
+
if (leftMatch && rightMatch) {
|
|
30673
|
+
const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client");
|
|
30674
|
+
const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server");
|
|
30675
|
+
return clientResult !== null && serverResult !== null && clientResult !== serverResult ? leftMatch : null;
|
|
30676
|
+
}
|
|
30677
|
+
const nestedMatch = leftMatch ?? rightMatch;
|
|
30678
|
+
if (!nestedMatch) return null;
|
|
30679
|
+
const otherResult = readInitialStateBoolean(leftMatch ? unwrappedExpression.right : unwrappedExpression.left, context.scopes);
|
|
30680
|
+
if (unwrappedExpression.operator === "&&" && otherResult === false || unwrappedExpression.operator === "||" && otherResult === true) return null;
|
|
30681
|
+
return nestedMatch;
|
|
30682
|
+
};
|
|
30683
|
+
const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
|
|
30684
|
+
const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
|
|
30685
|
+
if (!leftNode || !rightNode) return leftNode === rightNode;
|
|
30686
|
+
const left = stripParenExpression(leftNode);
|
|
30687
|
+
const right = stripParenExpression(rightNode);
|
|
30688
|
+
if (areExpressionsStructurallyEqual(left, right)) return true;
|
|
30689
|
+
if (left.type !== right.type) return false;
|
|
30690
|
+
if (isNodeOfType(left, "JSXText") && isNodeOfType(right, "JSXText")) return left.value === right.value;
|
|
30691
|
+
if (isNodeOfType(left, "JSXExpressionContainer") && isNodeOfType(right, "JSXExpressionContainer")) {
|
|
30692
|
+
if (!isAstNode(left.expression) || !isAstNode(right.expression)) return left.expression.type === right.expression.type;
|
|
30693
|
+
return areRenderedBranchesEquivalent(left.expression, right.expression);
|
|
30694
|
+
}
|
|
30695
|
+
if (isNodeOfType(left, "JSXElement") && isNodeOfType(right, "JSXElement")) {
|
|
30696
|
+
if (flattenJsxName$1(left.openingElement.name) !== flattenJsxName$1(right.openingElement.name)) return false;
|
|
30697
|
+
if (!areNodeArraysEquivalent(left.openingElement.attributes, right.openingElement.attributes)) return false;
|
|
30698
|
+
return areNodeArraysEquivalent(left.children, right.children);
|
|
30699
|
+
}
|
|
30700
|
+
if (isNodeOfType(left, "JSXFragment") && isNodeOfType(right, "JSXFragment")) return areNodeArraysEquivalent(left.children, right.children);
|
|
30701
|
+
if (isNodeOfType(left, "JSXAttribute") && isNodeOfType(right, "JSXAttribute")) {
|
|
30702
|
+
if (flattenJsxName$1(left.name) !== flattenJsxName$1(right.name)) return false;
|
|
30703
|
+
return areRenderedBranchesEquivalent(left.value, right.value);
|
|
30704
|
+
}
|
|
30705
|
+
if (isNodeOfType(left, "JSXSpreadAttribute") && isNodeOfType(right, "JSXSpreadAttribute")) return areRenderedBranchesEquivalent(left.argument, right.argument);
|
|
30706
|
+
if (isNodeOfType(left, "TemplateLiteral") && isNodeOfType(right, "TemplateLiteral")) {
|
|
30707
|
+
if (left.quasis.length !== right.quasis.length) return false;
|
|
30708
|
+
if (!left.quasis.every((quasi, index) => quasi.value.cooked === right.quasis[index]?.value.cooked && quasi.value.raw === right.quasis[index]?.value.raw)) return false;
|
|
30709
|
+
return areNodeArraysEquivalent(left.expressions, right.expressions);
|
|
30710
|
+
}
|
|
30711
|
+
return false;
|
|
30712
|
+
};
|
|
30713
|
+
const isRenderedValue = (node) => {
|
|
30714
|
+
const unwrappedNode = stripParenExpression(node);
|
|
30715
|
+
if (isNodeOfType(unwrappedNode, "Literal")) return unwrappedNode.value !== null && unwrappedNode.value !== true && unwrappedNode.value !== false && unwrappedNode.value !== "";
|
|
30716
|
+
if (isNodeOfType(unwrappedNode, "TemplateLiteral")) return unwrappedNode.expressions.length > 0 || unwrappedNode.quasis[0]?.value.cooked !== "";
|
|
30717
|
+
return isNodeOfType(unwrappedNode, "JSXElement") || isNodeOfType(unwrappedNode, "JSXFragment");
|
|
30718
|
+
};
|
|
30719
|
+
const findRenderedValueInAndBranch = (node) => {
|
|
30720
|
+
const unwrappedNode = stripParenExpression(node);
|
|
30721
|
+
if (isRenderedValue(unwrappedNode)) return unwrappedNode;
|
|
30722
|
+
if (!isNodeOfType(unwrappedNode, "LogicalExpression") || unwrappedNode.operator !== "&&") return null;
|
|
30723
|
+
return findRenderedValueInAndBranch(unwrappedNode.right);
|
|
30724
|
+
};
|
|
30725
|
+
const findEnclosingJsxAttribute = (node) => {
|
|
30726
|
+
let currentNode = node.parent;
|
|
30727
|
+
while (currentNode) {
|
|
30728
|
+
if (isNodeOfType(currentNode, "JSXAttribute")) return currentNode;
|
|
30729
|
+
if (isNodeOfType(currentNode, "JSXElement") || isNodeOfType(currentNode, "JSXFragment") || isFunctionLike$1(currentNode)) return null;
|
|
30730
|
+
currentNode = currentNode.parent;
|
|
30731
|
+
}
|
|
30732
|
+
return null;
|
|
30733
|
+
};
|
|
30734
|
+
const isInRenderedOutput = (node, componentOrHookNode, scopes) => {
|
|
30735
|
+
let currentNode = node;
|
|
30736
|
+
let parentNode = currentNode.parent;
|
|
30737
|
+
while (parentNode) {
|
|
30738
|
+
if (isNodeOfType(parentNode, "JSXExpressionContainer")) {
|
|
30739
|
+
const attribute = findEnclosingJsxAttribute(parentNode);
|
|
30740
|
+
return attribute ? !isEventHandlerAttribute(attribute) : true;
|
|
30741
|
+
}
|
|
30742
|
+
if (isNodeOfType(parentNode, "ReturnStatement")) {
|
|
30743
|
+
if (findEnclosingFunction(parentNode) === componentOrHookNode) return true;
|
|
30744
|
+
}
|
|
30745
|
+
if (parentNode === componentOrHookNode) return isFunctionLike$1(componentOrHookNode) && !isNodeOfType(componentOrHookNode.body, "BlockStatement") && componentOrHookNode.body === currentNode;
|
|
30746
|
+
if (isFunctionLike$1(parentNode) && !executesDuringRender(parentNode, scopes)) return false;
|
|
30747
|
+
currentNode = parentNode;
|
|
30748
|
+
parentNode = currentNode.parent;
|
|
30749
|
+
}
|
|
30750
|
+
return false;
|
|
30751
|
+
};
|
|
30752
|
+
const getReturnedValues = (statement) => {
|
|
30753
|
+
if (!statement) return [];
|
|
30754
|
+
if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? [statement.argument] : [];
|
|
30755
|
+
if (isNodeOfType(statement, "IfStatement")) return [...getReturnedValues(statement.consequent), ...getReturnedValues(statement.alternate)];
|
|
30756
|
+
if (!isNodeOfType(statement, "BlockStatement")) return [];
|
|
30757
|
+
const returnedValues = [];
|
|
30758
|
+
for (const childStatement of statement.body) {
|
|
30759
|
+
returnedValues.push(...getReturnedValues(childStatement));
|
|
30760
|
+
if (statementAlwaysExits(childStatement)) break;
|
|
30761
|
+
}
|
|
30762
|
+
return returnedValues;
|
|
30763
|
+
};
|
|
30764
|
+
const findFollowingReturnedValues = (ifStatement) => {
|
|
30765
|
+
const parentNode = ifStatement.parent;
|
|
30766
|
+
if (!isNodeOfType(parentNode, "BlockStatement")) return [];
|
|
30767
|
+
const statementIndex = parentNode.body.findIndex((statement) => statement === ifStatement);
|
|
30768
|
+
if (statementIndex < 0) return [];
|
|
30769
|
+
const returnedValues = [];
|
|
30770
|
+
for (const statement of parentNode.body.slice(statementIndex + 1)) {
|
|
30771
|
+
returnedValues.push(...getReturnedValues(statement));
|
|
30772
|
+
if (statementAlwaysExits(statement)) break;
|
|
30773
|
+
}
|
|
30774
|
+
return returnedValues;
|
|
30775
|
+
};
|
|
30776
|
+
const areConditionExpressionsEquivalent = (leftExpression, rightExpression) => {
|
|
30777
|
+
const left = stripParenExpression(leftExpression);
|
|
30778
|
+
const right = stripParenExpression(rightExpression);
|
|
30779
|
+
if (areExpressionsStructurallyEqual(left, right)) return true;
|
|
30780
|
+
if (left.type !== right.type) return false;
|
|
30781
|
+
if (isNodeOfType(left, "UnaryExpression") && isNodeOfType(right, "UnaryExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.argument, right.argument);
|
|
30782
|
+
if (isNodeOfType(left, "LogicalExpression") && isNodeOfType(right, "LogicalExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.left, right.left) && areConditionExpressionsEquivalent(left.right, right.right);
|
|
30783
|
+
if (isNodeOfType(left, "BinaryExpression") && isNodeOfType(right, "BinaryExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.left, right.left) && areConditionExpressionsEquivalent(left.right, right.right);
|
|
30784
|
+
return false;
|
|
30785
|
+
};
|
|
30786
|
+
const areReturnTreesEquivalent = (leftStatement, rightStatement) => {
|
|
30787
|
+
if (!leftStatement || !rightStatement) return leftStatement === rightStatement;
|
|
30788
|
+
if (isNodeOfType(leftStatement, "ReturnStatement") && isNodeOfType(rightStatement, "ReturnStatement")) return areRenderedBranchesEquivalent(leftStatement.argument, rightStatement.argument);
|
|
30789
|
+
if (isNodeOfType(leftStatement, "IfStatement") && isNodeOfType(rightStatement, "IfStatement")) return areConditionExpressionsEquivalent(leftStatement.test, rightStatement.test) && areReturnTreesEquivalent(leftStatement.consequent, rightStatement.consequent) && areReturnTreesEquivalent(leftStatement.alternate, rightStatement.alternate);
|
|
30790
|
+
if (!isNodeOfType(leftStatement, "BlockStatement") || !isNodeOfType(rightStatement, "BlockStatement")) return false;
|
|
30791
|
+
const leftReturningStatements = leftStatement.body.filter((statement) => getReturnedValues(statement).length > 0);
|
|
30792
|
+
const rightReturningStatements = rightStatement.body.filter((statement) => getReturnedValues(statement).length > 0);
|
|
30793
|
+
return leftReturningStatements.length === rightReturningStatements.length && leftReturningStatements.every((statement, index) => areReturnTreesEquivalent(statement, rightReturningStatements[index]));
|
|
30794
|
+
};
|
|
30795
|
+
const isStructuralRenderedValue = (node) => {
|
|
30796
|
+
if (!node) return false;
|
|
30797
|
+
const unwrappedNode = stripParenExpression(node);
|
|
30798
|
+
return isNodeOfType(unwrappedNode, "JSXElement") || isNodeOfType(unwrappedNode, "JSXFragment");
|
|
30799
|
+
};
|
|
30800
|
+
const branchRootsSuppressSameElement = (leftBranch, rightBranch) => {
|
|
30801
|
+
if (!rightBranch) return false;
|
|
30802
|
+
const left = stripParenExpression(leftBranch);
|
|
30803
|
+
const right = stripParenExpression(rightBranch);
|
|
30804
|
+
return isNodeOfType(left, "JSXElement") && isNodeOfType(right, "JSXElement") && flattenJsxName$1(left.openingElement.name) === flattenJsxName$1(right.openingElement.name) && hasSuppressHydrationWarningAttribute(left.openingElement) && hasSuppressHydrationWarningAttribute(right.openingElement);
|
|
30805
|
+
};
|
|
30806
|
+
const noHydrationBranchOnBrowserGlobal = defineRule({
|
|
30807
|
+
id: "no-hydration-branch-on-browser-global",
|
|
30808
|
+
title: "Server and client render different branches",
|
|
30809
|
+
severity: "error",
|
|
30810
|
+
category: "Correctness",
|
|
30811
|
+
requires: ["ssr"],
|
|
30812
|
+
recommendation: "Render the same initial output on the server and client, then switch after mount or use useSyncExternalStore with a stable server snapshot.",
|
|
30813
|
+
create: (context) => {
|
|
30814
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
30815
|
+
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
30816
|
+
let fileHasUseClientDirective = false;
|
|
30817
|
+
let fileIsEmailTemplate = false;
|
|
30818
|
+
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30819
|
+
const reportHydrationBranch = (conditionNode, leftBranch, rightBranch, requiresRenderedContext) => {
|
|
30820
|
+
const conditionMatch = matchHydrationCondition(conditionNode, context);
|
|
30821
|
+
if (!conditionMatch) return;
|
|
30822
|
+
const { predicateMatch, predicateNode } = conditionMatch;
|
|
30823
|
+
if (reportedNodes.has(predicateNode)) return;
|
|
30824
|
+
if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
|
|
30825
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(predicateNode, context.scopes);
|
|
30826
|
+
if (!componentOrHookNode) return;
|
|
30827
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30828
|
+
if (requiresRenderedContext && !isInRenderedOutput(predicateNode, componentOrHookNode, context.scopes)) return;
|
|
30829
|
+
if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
|
|
30830
|
+
const attribute = findEnclosingJsxAttribute(predicateNode);
|
|
30831
|
+
if (!attribute || isEventHandlerAttribute(attribute)) return;
|
|
30832
|
+
}
|
|
30833
|
+
if (fileIsEmailTemplate || isGatedByFalsyInitialState(predicateNode, context.scopes)) return;
|
|
30834
|
+
if (isAfterClientOnlyEarlyReturn(predicateNode, componentOrHookNode, context.scopes)) return;
|
|
30835
|
+
const openingElement = findEnclosingJsxOpeningElement(predicateNode);
|
|
30836
|
+
if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
|
|
30837
|
+
if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
|
|
30838
|
+
if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
|
|
30839
|
+
reportedNodes.add(predicateNode);
|
|
30840
|
+
context.report({
|
|
30841
|
+
node: predicateNode,
|
|
30842
|
+
message: `\`typeof ${predicateMatch.browserGlobalName}\` selects different rendered output on the server and during hydration. Render the same initial output, then switch after mount.`
|
|
30843
|
+
});
|
|
30844
|
+
};
|
|
30845
|
+
return {
|
|
30846
|
+
Program(node) {
|
|
30847
|
+
fileHasUseClientDirective = hasDirective(node, "use client");
|
|
30848
|
+
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
30849
|
+
},
|
|
30850
|
+
ConditionalExpression(node) {
|
|
30851
|
+
reportHydrationBranch(node.test, node.consequent, node.alternate, true);
|
|
30852
|
+
},
|
|
30853
|
+
LogicalExpression(node) {
|
|
30854
|
+
if (node.operator !== "&&" && node.operator !== "||") return;
|
|
30855
|
+
const renderedValue = node.operator === "&&" ? findRenderedValueInAndBranch(node.right) : isRenderedValue(node.right) ? node.right : null;
|
|
30856
|
+
if (!renderedValue) return;
|
|
30857
|
+
reportHydrationBranch(node, renderedValue, null, true);
|
|
30858
|
+
},
|
|
30859
|
+
IfStatement(node) {
|
|
30860
|
+
if (node.alternate && areReturnTreesEquivalent(node.consequent, node.alternate)) return;
|
|
30861
|
+
const consequentValues = getReturnedValues(node.consequent);
|
|
30862
|
+
const alternateValues = node.alternate ? getReturnedValues(node.alternate) : findFollowingReturnedValues(node);
|
|
30863
|
+
if (consequentValues.length === 0 || alternateValues.length === 0) return;
|
|
30864
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node.test, context.scopes);
|
|
30865
|
+
if (!componentOrHookNode) return;
|
|
30866
|
+
const enclosingFunction = findEnclosingFunction(node);
|
|
30867
|
+
if (enclosingFunction !== componentOrHookNode && (!enclosingFunction || !isInRenderedOutput(enclosingFunction, componentOrHookNode, context.scopes))) return;
|
|
30868
|
+
for (const consequentValue of consequentValues) for (const alternateValue of alternateValues) {
|
|
30869
|
+
if (!isRenderedValue(consequentValue) && !isRenderedValue(alternateValue)) continue;
|
|
30870
|
+
reportHydrationBranch(node.test, consequentValue, alternateValue, false);
|
|
30871
|
+
}
|
|
30872
|
+
}
|
|
30873
|
+
};
|
|
30874
|
+
}
|
|
30875
|
+
});
|
|
30876
|
+
//#endregion
|
|
29568
30877
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
29569
30878
|
const MESSAGE$22 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
|
|
29570
30879
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
@@ -29585,6 +30894,131 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
29585
30894
|
} })
|
|
29586
30895
|
});
|
|
29587
30896
|
//#endregion
|
|
30897
|
+
//#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
|
|
30898
|
+
const TIMER_FUNCTION_NAMES = new Set([
|
|
30899
|
+
"cancelAnimationFrame",
|
|
30900
|
+
"clearInterval",
|
|
30901
|
+
"clearTimeout",
|
|
30902
|
+
"queueMicrotask",
|
|
30903
|
+
"requestAnimationFrame",
|
|
30904
|
+
"setInterval",
|
|
30905
|
+
"setTimeout"
|
|
30906
|
+
]);
|
|
30907
|
+
const STORAGE_MUTATION_METHOD_NAMES = new Set([
|
|
30908
|
+
"clear",
|
|
30909
|
+
"removeItem",
|
|
30910
|
+
"setItem"
|
|
30911
|
+
]);
|
|
30912
|
+
const STORAGE_RECEIVER_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
30913
|
+
const EXTERNAL_READ_METHOD_NAMES = new Set(["getBoundingClientRect", "getClientRects"]);
|
|
30914
|
+
const NOTIFICATION_RECEIVER_NAMES = new Set([
|
|
30915
|
+
"message",
|
|
30916
|
+
"notification",
|
|
30917
|
+
"toast"
|
|
30918
|
+
]);
|
|
30919
|
+
const NOTIFICATION_METHOD_NAMES = new Set([
|
|
30920
|
+
"error",
|
|
30921
|
+
"info",
|
|
30922
|
+
"loading",
|
|
30923
|
+
"open",
|
|
30924
|
+
"show",
|
|
30925
|
+
"success",
|
|
30926
|
+
"warning"
|
|
30927
|
+
]);
|
|
30928
|
+
const getMemberCall = (node) => {
|
|
30929
|
+
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
30930
|
+
if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
|
|
30931
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
30932
|
+
return {
|
|
30933
|
+
methodName: node.callee.property.name,
|
|
30934
|
+
receiver: stripParenExpression(node.callee.object)
|
|
30935
|
+
};
|
|
30936
|
+
};
|
|
30937
|
+
const isNotificationReceiver = (receiver, scopes) => {
|
|
30938
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
30939
|
+
if (!NOTIFICATION_RECEIVER_NAMES.has(receiver.name)) return false;
|
|
30940
|
+
const symbol = scopes.symbolFor(receiver);
|
|
30941
|
+
if (symbol?.kind === "import") return true;
|
|
30942
|
+
if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
|
|
30943
|
+
const callee = symbol.initializer.callee;
|
|
30944
|
+
return isNodeOfType(callee, "Identifier") && /^use(?:Message|Notification|Toast)$/.test(callee.name);
|
|
30945
|
+
};
|
|
30946
|
+
const getKnownImpureCall = (callExpression, scopes) => {
|
|
30947
|
+
if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
|
|
30948
|
+
const memberCall = getMemberCall(callExpression);
|
|
30949
|
+
if (!memberCall) return null;
|
|
30950
|
+
const { methodName, receiver } = memberCall;
|
|
30951
|
+
if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && STORAGE_RECEIVER_NAMES.has(receiver.name) && scopes.isGlobalReference(receiver)) return `${receiver.name}.${methodName}()`;
|
|
30952
|
+
if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
|
|
30953
|
+
if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && isNotificationReceiver(receiver, scopes)) return `${receiver.name}.${methodName}()`;
|
|
30954
|
+
return null;
|
|
30955
|
+
};
|
|
30956
|
+
const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
|
|
30957
|
+
let rootIdentifier = null;
|
|
30958
|
+
if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
|
|
30959
|
+
else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
|
|
30960
|
+
if (!rootIdentifier) return null;
|
|
30961
|
+
const updaterScope = scopes.ownScopeFor(updater);
|
|
30962
|
+
if (!updaterScope) return null;
|
|
30963
|
+
const symbol = scopes.symbolFor(rootIdentifier);
|
|
30964
|
+
if (!symbol) return `the external value "${rootIdentifier.name}"`;
|
|
30965
|
+
if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
|
|
30966
|
+
return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
|
|
30967
|
+
};
|
|
30968
|
+
const findImpureUpdaterOperation = (updater, scopes) => {
|
|
30969
|
+
const analysis = getProgramAnalysis(updater);
|
|
30970
|
+
let operation = null;
|
|
30971
|
+
walkAst(updater, (child) => {
|
|
30972
|
+
if (operation) return false;
|
|
30973
|
+
if (child !== updater && isFunctionLike$1(child) && !executesDuringRender(child, scopes)) return false;
|
|
30974
|
+
if (isNodeOfType(child, "CallExpression")) {
|
|
30975
|
+
if (isNodeOfType(child.callee, "Identifier") && analysis) {
|
|
30976
|
+
const calleeReference = getRef(analysis, child.callee);
|
|
30977
|
+
if (calleeReference && isStateSetterCall(analysis, calleeReference)) {
|
|
30978
|
+
operation = `the nested state update "${child.callee.name}()"`;
|
|
30979
|
+
return false;
|
|
30980
|
+
}
|
|
30981
|
+
}
|
|
30982
|
+
const impureCall = getKnownImpureCall(child, scopes);
|
|
30983
|
+
if (impureCall) {
|
|
30984
|
+
operation = impureCall;
|
|
30985
|
+
return false;
|
|
30986
|
+
}
|
|
30987
|
+
}
|
|
30988
|
+
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
30989
|
+
operation = getExternalAssignmentDescription(child.left, updater, scopes);
|
|
30990
|
+
if (operation) return false;
|
|
30991
|
+
}
|
|
30992
|
+
if (isNodeOfType(child, "UpdateExpression")) {
|
|
30993
|
+
operation = getExternalAssignmentDescription(child.argument, updater, scopes);
|
|
30994
|
+
if (operation) return false;
|
|
30995
|
+
}
|
|
30996
|
+
});
|
|
30997
|
+
return operation;
|
|
30998
|
+
};
|
|
30999
|
+
const noImpureStateUpdater = defineRule({
|
|
31000
|
+
id: "no-impure-state-updater",
|
|
31001
|
+
title: "State updater has side effects",
|
|
31002
|
+
severity: "error",
|
|
31003
|
+
recommendation: "Keep state updater callbacks pure and return only the next state. Move notifications, storage, timers, ref writes, and other external work into the event or effect that queues the update.",
|
|
31004
|
+
create: (context) => ({ CallExpression(node) {
|
|
31005
|
+
const updater = node.arguments?.[0];
|
|
31006
|
+
if (!updater || !isFunctionLike$1(updater)) return;
|
|
31007
|
+
const analysis = getProgramAnalysis(node);
|
|
31008
|
+
if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
|
|
31009
|
+
const calleeReference = getRef(analysis, node.callee);
|
|
31010
|
+
if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
|
|
31011
|
+
const stateDeclarator = getUseStateDecl(analysis, calleeReference);
|
|
31012
|
+
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
|
|
31013
|
+
const operation = findImpureUpdaterOperation(updater, context.scopes);
|
|
31014
|
+
if (!operation) return;
|
|
31015
|
+
context.report({
|
|
31016
|
+
node: updater,
|
|
31017
|
+
message: `This state updater performs ${operation}. React may run updater functions more than once, so side effects here can repeat or observe inconsistent external state.`
|
|
31018
|
+
});
|
|
31019
|
+
} })
|
|
31020
|
+
});
|
|
31021
|
+
//#endregion
|
|
29588
31022
|
//#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
|
|
29589
31023
|
const MESSAGE$21 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
|
|
29590
31024
|
const REACT_USE_REF_OPTIONS = {
|
|
@@ -29751,7 +31185,7 @@ const noInitializeState = defineRule({
|
|
|
29751
31185
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
29752
31186
|
const analysis = getProgramAnalysis(node);
|
|
29753
31187
|
if (!analysis) return;
|
|
29754
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
31188
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
29755
31189
|
if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
|
|
29756
31190
|
const stateName = getStateName(fact.stateDeclarator);
|
|
29757
31191
|
context.report({
|
|
@@ -30410,111 +31844,6 @@ const noLegacyContextApi = defineRule({
|
|
|
30410
31844
|
}
|
|
30411
31845
|
});
|
|
30412
31846
|
//#endregion
|
|
30413
|
-
//#region src/plugin/utils/executes-during-render.ts
|
|
30414
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
30415
|
-
"map",
|
|
30416
|
-
"filter",
|
|
30417
|
-
"forEach",
|
|
30418
|
-
"flatMap",
|
|
30419
|
-
"reduce",
|
|
30420
|
-
"reduceRight",
|
|
30421
|
-
"some",
|
|
30422
|
-
"every",
|
|
30423
|
-
"find",
|
|
30424
|
-
"findIndex",
|
|
30425
|
-
"findLast",
|
|
30426
|
-
"findLastIndex",
|
|
30427
|
-
"sort",
|
|
30428
|
-
"toSorted"
|
|
30429
|
-
]);
|
|
30430
|
-
const executesDuringRender = (functionNode) => {
|
|
30431
|
-
const parent = functionNode.parent;
|
|
30432
|
-
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
30433
|
-
if (parent.callee === functionNode) return true;
|
|
30434
|
-
if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
|
|
30435
|
-
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
|
|
30436
|
-
};
|
|
30437
|
-
//#endregion
|
|
30438
|
-
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
30439
|
-
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
30440
|
-
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
30441
|
-
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
30442
|
-
return false;
|
|
30443
|
-
};
|
|
30444
|
-
//#endregion
|
|
30445
|
-
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
30446
|
-
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
30447
|
-
let ancestor = bindingIdentifier.parent;
|
|
30448
|
-
while (ancestor) {
|
|
30449
|
-
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
30450
|
-
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
30451
|
-
ancestor = ancestor.parent ?? null;
|
|
30452
|
-
}
|
|
30453
|
-
return null;
|
|
30454
|
-
};
|
|
30455
|
-
//#endregion
|
|
30456
|
-
//#region src/plugin/utils/references-falsy-initial-state.ts
|
|
30457
|
-
const isFalsyLiteral = (node) => {
|
|
30458
|
-
if (!node) return true;
|
|
30459
|
-
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
30460
|
-
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
30461
|
-
};
|
|
30462
|
-
const isFalsyInitialStateBinding = (identifier) => {
|
|
30463
|
-
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
30464
|
-
const binding = findVariableInitializer(identifier, identifier.name);
|
|
30465
|
-
if (!binding) return false;
|
|
30466
|
-
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
30467
|
-
if (!declarator?.init) return false;
|
|
30468
|
-
const init = stripParenExpression(declarator.init);
|
|
30469
|
-
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
30470
|
-
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
30471
|
-
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
30472
|
-
return isFalsyLiteral(init.arguments?.[0]);
|
|
30473
|
-
};
|
|
30474
|
-
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
30475
|
-
//#endregion
|
|
30476
|
-
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
30477
|
-
const isGatedByFalsyInitialState = (node) => {
|
|
30478
|
-
let cursor = node;
|
|
30479
|
-
let parent = node.parent;
|
|
30480
|
-
while (parent) {
|
|
30481
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
30482
|
-
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
30483
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
30484
|
-
cursor = parent;
|
|
30485
|
-
parent = parent.parent ?? null;
|
|
30486
|
-
}
|
|
30487
|
-
return false;
|
|
30488
|
-
};
|
|
30489
|
-
//#endregion
|
|
30490
|
-
//#region src/plugin/utils/references-client-only-flag.ts
|
|
30491
|
-
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
30492
|
-
const referencesClientOnlyFlag = (expression) => {
|
|
30493
|
-
const unwrapped = stripParenExpression(expression);
|
|
30494
|
-
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
30495
|
-
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
30496
|
-
const property = unwrapped.property;
|
|
30497
|
-
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
30498
|
-
}
|
|
30499
|
-
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
30500
|
-
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
30501
|
-
return false;
|
|
30502
|
-
};
|
|
30503
|
-
//#endregion
|
|
30504
|
-
//#region src/plugin/utils/is-inside-client-only-guard.ts
|
|
30505
|
-
const isInsideClientOnlyGuard = (node) => {
|
|
30506
|
-
let cursor = node;
|
|
30507
|
-
let parent = node.parent;
|
|
30508
|
-
while (parent) {
|
|
30509
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
30510
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
30511
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
30512
|
-
cursor = parent;
|
|
30513
|
-
parent = parent.parent ?? null;
|
|
30514
|
-
}
|
|
30515
|
-
return false;
|
|
30516
|
-
};
|
|
30517
|
-
//#endregion
|
|
30518
31847
|
//#region src/plugin/rules/performance/no-locale-format-in-render.ts
|
|
30519
31848
|
const LOCALE_FORMAT_METHOD_NAMES = new Set([
|
|
30520
31849
|
"toLocaleString",
|
|
@@ -30644,75 +31973,12 @@ const matchDateDefaultStringification = (node) => {
|
|
|
30644
31973
|
}
|
|
30645
31974
|
return null;
|
|
30646
31975
|
};
|
|
30647
|
-
const findRenderPhaseComponentOrHook = (node) => {
|
|
30648
|
-
let functionNode = findEnclosingFunction(node);
|
|
30649
|
-
while (functionNode) {
|
|
30650
|
-
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
30651
|
-
if (!executesDuringRender(functionNode)) return null;
|
|
30652
|
-
functionNode = findEnclosingFunction(functionNode);
|
|
30653
|
-
}
|
|
30654
|
-
return null;
|
|
30655
|
-
};
|
|
30656
|
-
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
30657
|
-
if (fileHasUseClientDirective) return true;
|
|
30658
|
-
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
30659
|
-
if (displayName && isReactHookName(displayName)) return true;
|
|
30660
|
-
let callsHook = false;
|
|
30661
|
-
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
30662
|
-
if (callsHook) return false;
|
|
30663
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
30664
|
-
callsHook = true;
|
|
30665
|
-
return false;
|
|
30666
|
-
}
|
|
30667
|
-
});
|
|
30668
|
-
return callsHook;
|
|
30669
|
-
};
|
|
30670
|
-
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
|
|
30671
|
-
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
30672
|
-
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
30673
|
-
const ancestors = /* @__PURE__ */ new Set();
|
|
30674
|
-
let cursor = node;
|
|
30675
|
-
while (cursor) {
|
|
30676
|
-
ancestors.add(cursor);
|
|
30677
|
-
cursor = cursor.parent ?? null;
|
|
30678
|
-
}
|
|
30679
|
-
for (const statement of body.body ?? []) {
|
|
30680
|
-
if (ancestors.has(statement)) return false;
|
|
30681
|
-
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
30682
|
-
if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
|
|
30683
|
-
let returnsEarly = false;
|
|
30684
|
-
walkAst(statement.consequent, (child) => {
|
|
30685
|
-
if (isFunctionLike$1(child)) return false;
|
|
30686
|
-
if (isNodeOfType(child, "ReturnStatement")) {
|
|
30687
|
-
returnsEarly = true;
|
|
30688
|
-
return false;
|
|
30689
|
-
}
|
|
30690
|
-
});
|
|
30691
|
-
if (returnsEarly) return true;
|
|
30692
|
-
}
|
|
30693
|
-
return false;
|
|
30694
|
-
};
|
|
30695
|
-
const findEnclosingJsxOpeningElement = (node) => {
|
|
30696
|
-
let cursor = node.parent;
|
|
30697
|
-
while (cursor) {
|
|
30698
|
-
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
30699
|
-
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
30700
|
-
cursor = cursor.parent ?? null;
|
|
30701
|
-
}
|
|
30702
|
-
return null;
|
|
30703
|
-
};
|
|
30704
31976
|
const noLocaleFormatInRender = defineRule({
|
|
30705
31977
|
id: "no-locale-format-in-render",
|
|
30706
31978
|
title: "Locale/timezone formatting during render",
|
|
30707
31979
|
severity: "warn",
|
|
30708
31980
|
category: "Correctness",
|
|
30709
|
-
|
|
30710
|
-
"vite",
|
|
30711
|
-
"cra",
|
|
30712
|
-
"expo",
|
|
30713
|
-
"react-native",
|
|
30714
|
-
"unknown"
|
|
30715
|
-
],
|
|
31981
|
+
requires: ["ssr"],
|
|
30716
31982
|
recommendation: "Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.",
|
|
30717
31983
|
create: (context) => {
|
|
30718
31984
|
if (isTestlikeFilename(context.filename)) return {};
|
|
@@ -30722,13 +31988,12 @@ const noLocaleFormatInRender = defineRule({
|
|
|
30722
31988
|
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30723
31989
|
const reportIfRenderPhase = (match) => {
|
|
30724
31990
|
if (reportedNodes.has(match.node)) return;
|
|
30725
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
|
|
31991
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node, context.scopes);
|
|
30726
31992
|
if (!componentOrHookNode) return;
|
|
30727
31993
|
if (fileIsEmailTemplate) return;
|
|
30728
31994
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30729
|
-
if (
|
|
30730
|
-
if (
|
|
30731
|
-
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
|
|
31995
|
+
if (isGatedByFalsyInitialState(match.node, context.scopes)) return;
|
|
31996
|
+
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode, context.scopes)) return;
|
|
30732
31997
|
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
|
|
30733
31998
|
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
|
|
30734
31999
|
reportedNodes.add(match.node);
|
|
@@ -30755,7 +32020,7 @@ const noLocaleFormatInRender = defineRule({
|
|
|
30755
32020
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
30756
32021
|
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
30757
32022
|
const helperName = expression.callee.name;
|
|
30758
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(node);
|
|
32023
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node, context.scopes);
|
|
30759
32024
|
if (!componentOrHookNode) return;
|
|
30760
32025
|
const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
|
|
30761
32026
|
if (!helperNode || !isFunctionLike$1(helperNode)) return;
|
|
@@ -30767,9 +32032,8 @@ const noLocaleFormatInRender = defineRule({
|
|
|
30767
32032
|
if (!match || reportedNodes.has(match.node)) return;
|
|
30768
32033
|
if (fileIsEmailTemplate) return;
|
|
30769
32034
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30770
|
-
if (
|
|
30771
|
-
if (
|
|
30772
|
-
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
|
|
32035
|
+
if (isGatedByFalsyInitialState(node, context.scopes)) return;
|
|
32036
|
+
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode, context.scopes)) return;
|
|
30773
32037
|
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
|
|
30774
32038
|
reportedNodes.add(match.node);
|
|
30775
32039
|
context.report({
|
|
@@ -30871,9 +32135,6 @@ const noLongTransitionDuration = defineRule({
|
|
|
30871
32135
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
30872
32136
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
30873
32137
|
//#endregion
|
|
30874
|
-
//#region src/plugin/utils/is-event-handler-attribute.ts
|
|
30875
|
-
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
|
|
30876
|
-
//#endregion
|
|
30877
32138
|
//#region src/plugin/rules/architecture/no-many-boolean-props.ts
|
|
30878
32139
|
const IMPERATIVE_CALLBACK_PREFIX_PATTERN = /^(?:show|hide|enable|disable)[A-Z]/;
|
|
30879
32140
|
const collectCallbackUsedNames = (componentBody, propsParam, scopes) => {
|
|
@@ -31005,7 +32266,7 @@ const noMatchMediaInStateInitializer = defineRule({
|
|
|
31005
32266
|
title: "matchMedia in state initializer",
|
|
31006
32267
|
severity: "warn",
|
|
31007
32268
|
category: "Correctness",
|
|
31008
|
-
|
|
32269
|
+
requires: ["ssr"],
|
|
31009
32270
|
recommendation: "Prefer CSS media queries for layout, or subscribe with `useSyncExternalStore` and provide a stable server snapshot.",
|
|
31010
32271
|
create: (context) => {
|
|
31011
32272
|
if (isTestlikeFilename(context.filename)) return {};
|
|
@@ -31556,6 +32817,41 @@ const noMutableInDeps = defineRule({
|
|
|
31556
32817
|
}
|
|
31557
32818
|
});
|
|
31558
32819
|
//#endregion
|
|
32820
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
32821
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
32822
|
+
let node = callExpression;
|
|
32823
|
+
let parent = node.parent;
|
|
32824
|
+
while (parent) {
|
|
32825
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
32826
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
32827
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
32828
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
32829
|
+
node = parent;
|
|
32830
|
+
parent = node.parent;
|
|
32831
|
+
continue;
|
|
32832
|
+
}
|
|
32833
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
32834
|
+
node = parent;
|
|
32835
|
+
parent = node.parent;
|
|
32836
|
+
continue;
|
|
32837
|
+
}
|
|
32838
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
32839
|
+
node = parent;
|
|
32840
|
+
parent = node.parent;
|
|
32841
|
+
continue;
|
|
32842
|
+
}
|
|
32843
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
32844
|
+
const expressions = parent.expressions ?? [];
|
|
32845
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
32846
|
+
node = parent;
|
|
32847
|
+
parent = node.parent;
|
|
32848
|
+
continue;
|
|
32849
|
+
}
|
|
32850
|
+
return false;
|
|
32851
|
+
}
|
|
32852
|
+
return false;
|
|
32853
|
+
};
|
|
32854
|
+
//#endregion
|
|
31559
32855
|
//#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
|
|
31560
32856
|
const LODASH_MUTATOR_NAMES = new Set([
|
|
31561
32857
|
"set",
|
|
@@ -32846,7 +34142,7 @@ const isUseRefIdentifier = (identifier) => {
|
|
|
32846
34142
|
};
|
|
32847
34143
|
const COMMAND_PROP_NAME_PATTERN = /^(fetch|load|refetch|dispatch|register|render)([A-Z_]|$)/;
|
|
32848
34144
|
const SETTER_NAMED_PROP_PATTERN = /^set[A-Z]/;
|
|
32849
|
-
const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
34145
|
+
const unwrapChainExpression$1 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
32850
34146
|
const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
|
|
32851
34147
|
"useCallback",
|
|
32852
34148
|
"useMemo",
|
|
@@ -32881,7 +34177,7 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
32881
34177
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
32882
34178
|
const node = def.node;
|
|
32883
34179
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
32884
|
-
const initializer = unwrapChainExpression(node.init);
|
|
34180
|
+
const initializer = unwrapChainExpression$1(node.init);
|
|
32885
34181
|
const wrappedFunction = getWrapperHookWrappedFunction(initializer);
|
|
32886
34182
|
if (wrappedFunction) {
|
|
32887
34183
|
if (wrappedFunction.async) return false;
|
|
@@ -32891,10 +34187,170 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
32891
34187
|
return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
|
|
32892
34188
|
}));
|
|
32893
34189
|
};
|
|
34190
|
+
const getDeclarationKind = (declarator) => {
|
|
34191
|
+
const declaration = declarator.parent;
|
|
34192
|
+
return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
|
|
34193
|
+
};
|
|
34194
|
+
const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
34195
|
+
const getDestructuredPropertyName = (bindingIdentifier) => {
|
|
34196
|
+
const property = bindingIdentifier.parent;
|
|
34197
|
+
if (!property || !isNodeOfType(property, "Property")) return null;
|
|
34198
|
+
if (property.computed) return isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" ? String(property.key.value) : null;
|
|
34199
|
+
return isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
34200
|
+
};
|
|
34201
|
+
const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
|
|
34202
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
34203
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
34204
|
+
const callbackReference = getRef(analysis, unwrappedExpression);
|
|
34205
|
+
const callbackVariable = callbackReference?.resolved;
|
|
34206
|
+
if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable)) return null;
|
|
34207
|
+
if (isProp(analysis, callbackReference)) {
|
|
34208
|
+
if (isWholePropsObjectReference(analysis, callbackReference)) return null;
|
|
34209
|
+
const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
|
|
34210
|
+
return (bindingIdentifier && getDestructuredPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
|
|
34211
|
+
}
|
|
34212
|
+
if (hasMutableBindingWrite(callbackReference)) return null;
|
|
34213
|
+
const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
34214
|
+
if (definitions.length !== 1) return null;
|
|
34215
|
+
const declarator = definitions[0];
|
|
34216
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || getDeclarationKind(declarator) !== "const" || !declarator.init) return null;
|
|
34217
|
+
visitedVariables.add(callbackVariable);
|
|
34218
|
+
if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
34219
|
+
const bindingIdentifier = callbackVariable.defs[0]?.name;
|
|
34220
|
+
const propertyName = bindingIdentifier ? getDestructuredPropertyName(bindingIdentifier) : null;
|
|
34221
|
+
const propsReference = getDownstreamRefs(analysis, declarator.init).find((candidateReference) => isWholePropsObjectReference(analysis, candidateReference));
|
|
34222
|
+
return propertyName && propsReference ? propertyName : null;
|
|
34223
|
+
}
|
|
34224
|
+
if (!isNodeOfType(declarator.id, "Identifier")) return null;
|
|
34225
|
+
return getParentCallbackPropName(analysis, declarator.init, visitedVariables);
|
|
34226
|
+
}
|
|
34227
|
+
if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
|
|
34228
|
+
const callbackName = getStaticMemberPropertyName(unwrappedExpression);
|
|
34229
|
+
if (!callbackName) return null;
|
|
34230
|
+
const receiver = stripParenExpression(unwrappedExpression.object);
|
|
34231
|
+
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
34232
|
+
const receiverReference = getRef(analysis, receiver);
|
|
34233
|
+
if (!receiverReference || !isWholePropsObjectReference(analysis, receiverReference)) return null;
|
|
34234
|
+
return callbackName;
|
|
34235
|
+
};
|
|
34236
|
+
const isNullishRefInitializer = (initializer) => {
|
|
34237
|
+
if (!initializer) return true;
|
|
34238
|
+
const unwrappedInitializer = stripParenExpression(initializer);
|
|
34239
|
+
if (isNodeOfType(unwrappedInitializer, "Literal")) return unwrappedInitializer.value === null;
|
|
34240
|
+
if (!isNodeOfType(unwrappedInitializer, "Identifier")) return false;
|
|
34241
|
+
return unwrappedInitializer.name === "undefined";
|
|
34242
|
+
};
|
|
34243
|
+
const getDirectComponentBodyStatement = (node, componentBody) => {
|
|
34244
|
+
let current = node;
|
|
34245
|
+
while (current?.parent && current.parent !== componentBody) current = current.parent;
|
|
34246
|
+
return current?.parent === componentBody ? current : null;
|
|
34247
|
+
};
|
|
34248
|
+
const getVariableForDeclarator = (analysis, declarator) => {
|
|
34249
|
+
for (const scope of analysis.scopeManager.scopes) {
|
|
34250
|
+
const variable = scope.variables.find((candidateVariable) => candidateVariable.defs.some((definition) => definition.node === declarator));
|
|
34251
|
+
if (variable) return variable;
|
|
34252
|
+
}
|
|
34253
|
+
return null;
|
|
34254
|
+
};
|
|
34255
|
+
const getRefMember = (identifier) => {
|
|
34256
|
+
const receiver = findTransparentExpressionRoot(identifier);
|
|
34257
|
+
const member = receiver.parent;
|
|
34258
|
+
if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== receiver) return null;
|
|
34259
|
+
return member;
|
|
34260
|
+
};
|
|
34261
|
+
const getRefMemberAssignment = (identifier) => {
|
|
34262
|
+
const member = getRefMember(identifier);
|
|
34263
|
+
if (!member) return null;
|
|
34264
|
+
const memberRoot = findTransparentExpressionRoot(member);
|
|
34265
|
+
const assignment = memberRoot.parent;
|
|
34266
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== memberRoot) return null;
|
|
34267
|
+
return assignment;
|
|
34268
|
+
};
|
|
34269
|
+
const getRefAliasDeclarator = (identifier) => {
|
|
34270
|
+
const initializer = findTransparentExpressionRoot(identifier);
|
|
34271
|
+
const declarator = initializer.parent;
|
|
34272
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== initializer || !isNodeOfType(declarator.id, "Identifier") || getDeclarationKind(declarator) !== "const") return null;
|
|
34273
|
+
return declarator;
|
|
34274
|
+
};
|
|
34275
|
+
const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
|
|
34276
|
+
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
34277
|
+
const receiverReference = getRef(analysis, receiver);
|
|
34278
|
+
if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
|
|
34279
|
+
const variables = /* @__PURE__ */ new Set();
|
|
34280
|
+
let currentVariable = receiverReference.resolved;
|
|
34281
|
+
let refCall = null;
|
|
34282
|
+
while (currentVariable && !variables.has(currentVariable)) {
|
|
34283
|
+
variables.add(currentVariable);
|
|
34284
|
+
const definitions = currentVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
34285
|
+
if (definitions.length !== 1) return null;
|
|
34286
|
+
const declarator = definitions[0];
|
|
34287
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) return null;
|
|
34288
|
+
if (isNodeOfType(declarator.init, "CallExpression") && isReactUseRefCall(declarator.init)) {
|
|
34289
|
+
refCall = declarator.init;
|
|
34290
|
+
break;
|
|
34291
|
+
}
|
|
34292
|
+
if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
|
|
34293
|
+
const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
|
|
34294
|
+
if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
|
|
34295
|
+
currentVariable = upstreamReference.resolved;
|
|
34296
|
+
}
|
|
34297
|
+
if (!refCall) return null;
|
|
34298
|
+
const pendingVariables = [...variables];
|
|
34299
|
+
while (pendingVariables.length > 0) {
|
|
34300
|
+
const variable = pendingVariables.pop();
|
|
34301
|
+
if (!variable) continue;
|
|
34302
|
+
for (const candidateReference of variable.references) {
|
|
34303
|
+
const aliasDeclarator = getRefAliasDeclarator(candidateReference.identifier);
|
|
34304
|
+
if (!aliasDeclarator) continue;
|
|
34305
|
+
const aliasVariable = getVariableForDeclarator(analysis, aliasDeclarator);
|
|
34306
|
+
if (!aliasVariable || variables.has(aliasVariable)) continue;
|
|
34307
|
+
if (aliasVariable.references.some((aliasReference) => aliasReference.isWrite() && !aliasReference.init)) return null;
|
|
34308
|
+
variables.add(aliasVariable);
|
|
34309
|
+
pendingVariables.push(aliasVariable);
|
|
34310
|
+
}
|
|
34311
|
+
}
|
|
34312
|
+
return {
|
|
34313
|
+
refCall,
|
|
34314
|
+
variables
|
|
34315
|
+
};
|
|
34316
|
+
};
|
|
34317
|
+
const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactUseRefCall) => {
|
|
34318
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
34319
|
+
if (!isNodeOfType(callee, "MemberExpression") || getStaticMemberPropertyName(callee) !== "current") return null;
|
|
34320
|
+
const bindingProvenance = getRefBindingProvenance(analysis, stripParenExpression(callee.object), isReactUseRefCall);
|
|
34321
|
+
if (!bindingProvenance) return null;
|
|
34322
|
+
const { refCall, variables } = bindingProvenance;
|
|
34323
|
+
const componentFunction = findEnclosingFunction(effectCall);
|
|
34324
|
+
if (!componentFunction || !isFunctionLike$1(componentFunction) || !isComponentFunction$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
|
|
34325
|
+
if (!getDirectComponentBodyStatement(effectCall, componentFunction.body)) return null;
|
|
34326
|
+
const callbackPropNames = /* @__PURE__ */ new Set();
|
|
34327
|
+
const initializer = refCall.arguments?.[0];
|
|
34328
|
+
const initializerCallbackName = initializer ? getParentCallbackPropName(analysis, initializer) : null;
|
|
34329
|
+
if (initializerCallbackName) callbackPropNames.add(initializerCallbackName);
|
|
34330
|
+
else if (!isNullishRefInitializer(initializer)) return null;
|
|
34331
|
+
for (const variable of variables) for (const candidateReference of variable.references) {
|
|
34332
|
+
if (candidateReference.init) continue;
|
|
34333
|
+
if (candidateReference.isWrite()) return null;
|
|
34334
|
+
const identifier = candidateReference.identifier;
|
|
34335
|
+
if (getRefAliasDeclarator(identifier)) continue;
|
|
34336
|
+
const member = getRefMember(identifier);
|
|
34337
|
+
if (!member || getStaticMemberPropertyName(member) !== "current") return null;
|
|
34338
|
+
const memberParent = findTransparentExpressionRoot(member).parent;
|
|
34339
|
+
if (memberParent && (isNodeOfType(memberParent, "UpdateExpression") || isNodeOfType(memberParent, "UnaryExpression") && memberParent.operator === "delete")) return null;
|
|
34340
|
+
const assignment = getRefMemberAssignment(identifier);
|
|
34341
|
+
if (!assignment) continue;
|
|
34342
|
+
const assignmentStatement = findTransparentExpressionRoot(assignment).parent;
|
|
34343
|
+
if (assignment.operator !== "=" || !assignmentStatement || !isNodeOfType(assignmentStatement, "ExpressionStatement") || assignmentStatement.parent !== componentFunction.body) return null;
|
|
34344
|
+
const assignedCallbackName = getParentCallbackPropName(analysis, assignment.right);
|
|
34345
|
+
if (!assignedCallbackName) return null;
|
|
34346
|
+
callbackPropNames.add(assignedCallbackName);
|
|
34347
|
+
}
|
|
34348
|
+
return callbackPropNames.size > 0 ? { callbackPropNames } : null;
|
|
34349
|
+
};
|
|
32894
34350
|
const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
32895
34351
|
const node = def.node;
|
|
32896
34352
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
32897
|
-
return getWrapperHookWrappedFunction(unwrapChainExpression(node.init)) !== null;
|
|
34353
|
+
return getWrapperHookWrappedFunction(unwrapChainExpression$1(node.init)) !== null;
|
|
32898
34354
|
}));
|
|
32899
34355
|
const isHandlerBagArgument = (analysis, argument) => {
|
|
32900
34356
|
if (!isNodeOfType(argument, "ObjectExpression")) return false;
|
|
@@ -32916,7 +34372,7 @@ const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
|
|
|
32916
34372
|
const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
32917
34373
|
const node = def.node;
|
|
32918
34374
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
32919
|
-
const init = unwrapChainExpression(node.init);
|
|
34375
|
+
const init = unwrapChainExpression$1(node.init);
|
|
32920
34376
|
if (!isNodeOfType(init, "CallExpression")) return false;
|
|
32921
34377
|
const callee = init.callee;
|
|
32922
34378
|
if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
|
|
@@ -32946,71 +34402,79 @@ const noPassDataToParent = defineRule({
|
|
|
32946
34402
|
severity: "warn",
|
|
32947
34403
|
tags: ["test-noise"],
|
|
32948
34404
|
recommendation: "Fetch the data in the parent and pass it down as a prop (or return it from the hook), instead of handing it back up through a prop callback in a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#passing-data-to-the-parent",
|
|
32949
|
-
create: (context) =>
|
|
32950
|
-
|
|
32951
|
-
|
|
32952
|
-
|
|
32953
|
-
|
|
32954
|
-
|
|
32955
|
-
|
|
32956
|
-
|
|
32957
|
-
|
|
32958
|
-
|
|
32959
|
-
const
|
|
32960
|
-
if (!
|
|
32961
|
-
|
|
32962
|
-
if (!
|
|
32963
|
-
const
|
|
32964
|
-
|
|
32965
|
-
|
|
32966
|
-
|
|
32967
|
-
if (
|
|
32968
|
-
|
|
32969
|
-
|
|
32970
|
-
|
|
32971
|
-
|
|
32972
|
-
|
|
32973
|
-
|
|
32974
|
-
|
|
32975
|
-
|
|
32976
|
-
|
|
32977
|
-
|
|
32978
|
-
|
|
32979
|
-
|
|
32980
|
-
|
|
32981
|
-
|
|
32982
|
-
|
|
32983
|
-
|
|
32984
|
-
|
|
32985
|
-
|
|
32986
|
-
|
|
32987
|
-
|
|
32988
|
-
|
|
32989
|
-
|
|
32990
|
-
|
|
32991
|
-
|
|
32992
|
-
|
|
32993
|
-
|
|
32994
|
-
|
|
32995
|
-
|
|
32996
|
-
|
|
32997
|
-
|
|
32998
|
-
|
|
32999
|
-
|
|
33000
|
-
if (
|
|
33001
|
-
if (
|
|
33002
|
-
|
|
33003
|
-
|
|
33004
|
-
|
|
33005
|
-
|
|
33006
|
-
|
|
33007
|
-
|
|
33008
|
-
|
|
33009
|
-
|
|
33010
|
-
|
|
33011
|
-
|
|
33012
|
-
|
|
33013
|
-
|
|
34405
|
+
create: (context) => {
|
|
34406
|
+
const isReactUseRefCall = (node) => isReactApiCall(node, "useRef", context.scopes, {
|
|
34407
|
+
allowGlobalReactNamespace: true,
|
|
34408
|
+
allowUnboundBareCalls: true
|
|
34409
|
+
});
|
|
34410
|
+
return { CallExpression(node) {
|
|
34411
|
+
if (!isUseEffect(node)) return;
|
|
34412
|
+
const analysis = getProgramAnalysis(node);
|
|
34413
|
+
if (!analysis) return;
|
|
34414
|
+
if (hasCleanup(analysis, node)) return;
|
|
34415
|
+
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
34416
|
+
if (!effectFnRefs) return;
|
|
34417
|
+
const effectFn = getEffectFn(analysis, node);
|
|
34418
|
+
if (!effectFn) return;
|
|
34419
|
+
for (const ref of effectFnRefs) {
|
|
34420
|
+
const callExpr = getCallExpr(ref);
|
|
34421
|
+
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
34422
|
+
const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall);
|
|
34423
|
+
if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
|
|
34424
|
+
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
34425
|
+
const calleeNode = unwrapChainExpression$1(callExpr.callee);
|
|
34426
|
+
const identifier = ref.identifier;
|
|
34427
|
+
if (callbackRefProvenance) {
|
|
34428
|
+
if ([...callbackRefProvenance.callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
|
|
34429
|
+
} else if (calleeNode === identifier) {
|
|
34430
|
+
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
34431
|
+
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
34432
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
34433
|
+
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
34434
|
+
if (isCustomHookParameter(ref)) continue;
|
|
34435
|
+
} else continue;
|
|
34436
|
+
const methodName = getCallMethodName(calleeNode);
|
|
34437
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
34438
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
34439
|
+
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
34440
|
+
if (!callbackRefProvenance && isNamespacedApiCallee(calleeNode)) continue;
|
|
34441
|
+
const isSetterNamedCallee = callbackRefProvenance ? [...callbackRefProvenance.callbackPropNames].every((callbackPropName) => SETTER_NAMED_PROP_PATTERN.test(callbackPropName)) : Boolean((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) && SETTER_NAMED_PROP_PATTERN.test((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) ?? ""));
|
|
34442
|
+
const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
|
|
34443
|
+
const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
|
|
34444
|
+
if (isFunctionLike$1(argument)) {
|
|
34445
|
+
if (!isSetterNamedCallee) return [];
|
|
34446
|
+
return getFunctionalUpdaterDataRefs(analysis, argument);
|
|
34447
|
+
}
|
|
34448
|
+
if (isHandlerBagArgument(analysis, argument)) return [];
|
|
34449
|
+
if (isParentWiredHookResultArgument(analysis, argument)) return [];
|
|
34450
|
+
if (isNodeOfType(argument, "Identifier")) {
|
|
34451
|
+
const argumentRef = getRef(analysis, argument);
|
|
34452
|
+
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
34453
|
+
}
|
|
34454
|
+
return getDownstreamRefs(analysis, argument);
|
|
34455
|
+
}).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
34456
|
+
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
34457
|
+
if (!argsUpstreamRefs.some((argRef) => {
|
|
34458
|
+
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
34459
|
+
if (isProp(analysis, argRef)) return false;
|
|
34460
|
+
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
34461
|
+
if (isRefCurrent(argRef)) return false;
|
|
34462
|
+
if (isConstant(argRef)) return false;
|
|
34463
|
+
if (isParentWiredHookResultRef(analysis, argRef)) return false;
|
|
34464
|
+
if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
|
|
34465
|
+
if (resolvesToFunctionBinding(argRef)) return false;
|
|
34466
|
+
const argIdentifier = argRef.identifier;
|
|
34467
|
+
if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
|
|
34468
|
+
if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
|
|
34469
|
+
return true;
|
|
34470
|
+
})) continue;
|
|
34471
|
+
context.report({
|
|
34472
|
+
node: callExpr,
|
|
34473
|
+
message: "Handing data back to a parent from a useEffect costs your users an extra render."
|
|
34474
|
+
});
|
|
34475
|
+
}
|
|
34476
|
+
} };
|
|
34477
|
+
}
|
|
33014
34478
|
});
|
|
33015
34479
|
//#endregion
|
|
33016
34480
|
//#region src/plugin/utils/is-call-result-consumed-as-argument.ts
|
|
@@ -34258,6 +35722,63 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
34258
35722
|
}
|
|
34259
35723
|
});
|
|
34260
35724
|
//#endregion
|
|
35725
|
+
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
35726
|
+
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
35727
|
+
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
35728
|
+
if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
35729
|
+
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
35730
|
+
if (!symbol?.initializer) return null;
|
|
35731
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
35732
|
+
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
35733
|
+
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
35734
|
+
};
|
|
35735
|
+
const isSameRefCurrentMember = (node, refSymbol, scopes) => {
|
|
35736
|
+
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
|
|
35737
|
+
const receiver = stripParenExpression(node.object);
|
|
35738
|
+
return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
|
|
35739
|
+
};
|
|
35740
|
+
const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
|
|
35741
|
+
if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
|
|
35742
|
+
let descendant = assignmentExpression;
|
|
35743
|
+
let ancestor = descendant.parent;
|
|
35744
|
+
while (ancestor) {
|
|
35745
|
+
if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
|
|
35746
|
+
const { left, right } = ancestor.test;
|
|
35747
|
+
if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
|
|
35748
|
+
}
|
|
35749
|
+
descendant = ancestor;
|
|
35750
|
+
ancestor = descendant.parent;
|
|
35751
|
+
}
|
|
35752
|
+
return false;
|
|
35753
|
+
};
|
|
35754
|
+
const noRefCurrentInRender = defineRule({
|
|
35755
|
+
id: "no-ref-current-in-render",
|
|
35756
|
+
title: "Ref mutated during render",
|
|
35757
|
+
severity: "error",
|
|
35758
|
+
recommendation: "Move ref writes into an event handler or effect. Render must stay pure because React can replay or discard it. The predictable null-guarded lazy initialization pattern remains supported.",
|
|
35759
|
+
create: (context) => {
|
|
35760
|
+
const report = (memberExpression) => {
|
|
35761
|
+
if (!resolveReactRefSymbol(memberExpression, context.scopes)) return;
|
|
35762
|
+
if (!findRenderPhaseComponentOrHook(memberExpression, context.scopes)) return;
|
|
35763
|
+
context.report({
|
|
35764
|
+
node: memberExpression,
|
|
35765
|
+
message: "This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits."
|
|
35766
|
+
});
|
|
35767
|
+
};
|
|
35768
|
+
return {
|
|
35769
|
+
AssignmentExpression(node) {
|
|
35770
|
+
const refSymbol = resolveReactRefSymbol(node.left, context.scopes);
|
|
35771
|
+
if (!refSymbol) return;
|
|
35772
|
+
if (isDocumentedLazyInitialization(node, refSymbol, context.scopes)) return;
|
|
35773
|
+
report(node.left);
|
|
35774
|
+
},
|
|
35775
|
+
UpdateExpression(node) {
|
|
35776
|
+
report(node.argument);
|
|
35777
|
+
}
|
|
35778
|
+
};
|
|
35779
|
+
}
|
|
35780
|
+
});
|
|
35781
|
+
//#endregion
|
|
34261
35782
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
34262
35783
|
const isInsideComponentContext = (node) => {
|
|
34263
35784
|
let cursor = node.parent;
|
|
@@ -36272,6 +37793,140 @@ const noUnescapedEntities = defineRule({
|
|
|
36272
37793
|
} })
|
|
36273
37794
|
});
|
|
36274
37795
|
//#endregion
|
|
37796
|
+
//#region src/plugin/rules/performance/no-unguarded-browser-global-in-render-or-hook-init.ts
|
|
37797
|
+
const BROWSER_GLOBAL_NAMES = new Set([
|
|
37798
|
+
"window",
|
|
37799
|
+
"document",
|
|
37800
|
+
"localStorage",
|
|
37801
|
+
"sessionStorage",
|
|
37802
|
+
"navigator",
|
|
37803
|
+
"matchMedia"
|
|
37804
|
+
]);
|
|
37805
|
+
const getTypeofBrowserGlobalName = (expression, context) => {
|
|
37806
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
37807
|
+
if (!isNodeOfType(unwrappedExpression, "UnaryExpression") || unwrappedExpression.operator !== "typeof") return null;
|
|
37808
|
+
const argument = stripParenExpression(unwrappedExpression.argument);
|
|
37809
|
+
if (isNodeOfType(argument, "Identifier")) return BROWSER_GLOBAL_NAMES.has(argument.name) && context.scopes.isGlobalReference(argument) ? argument.name : null;
|
|
37810
|
+
if (!isNodeOfType(argument, "MemberExpression") || argument.computed || !isNodeOfType(argument.object, "Identifier") || argument.object.name !== "globalThis" || !context.scopes.isGlobalReference(argument.object) || !isNodeOfType(argument.property, "Identifier") || !BROWSER_GLOBAL_NAMES.has(argument.property.name)) return null;
|
|
37811
|
+
return argument.property.name;
|
|
37812
|
+
};
|
|
37813
|
+
const browserGuardCoversGlobal = (guardName, browserGlobalName) => guardName === browserGlobalName || guardName === "window" || guardName === "document";
|
|
37814
|
+
const mergeAvailability = (leftAvailability, rightAvailability) => {
|
|
37815
|
+
if (leftAvailability === null) return rightAvailability;
|
|
37816
|
+
if (rightAvailability === null) return leftAvailability;
|
|
37817
|
+
return leftAvailability === rightAvailability ? leftAvailability : null;
|
|
37818
|
+
};
|
|
37819
|
+
const readAvailabilityWhenPredicate = (expression, browserGlobalName, context, predicateResult) => {
|
|
37820
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
37821
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return readAvailabilityWhenPredicate(unwrappedExpression.argument, browserGlobalName, context, !predicateResult);
|
|
37822
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
|
|
37823
|
+
if (unwrappedExpression.operator === "&&" && predicateResult) return mergeAvailability(readAvailabilityWhenPredicate(unwrappedExpression.left, browserGlobalName, context, true), readAvailabilityWhenPredicate(unwrappedExpression.right, browserGlobalName, context, true));
|
|
37824
|
+
if (unwrappedExpression.operator === "||" && !predicateResult) return mergeAvailability(readAvailabilityWhenPredicate(unwrappedExpression.left, browserGlobalName, context, false), readAvailabilityWhenPredicate(unwrappedExpression.right, browserGlobalName, context, false));
|
|
37825
|
+
return null;
|
|
37826
|
+
}
|
|
37827
|
+
if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
|
|
37828
|
+
const leftTypeofName = getTypeofBrowserGlobalName(unwrappedExpression.left, context);
|
|
37829
|
+
const rightTypeofName = getTypeofBrowserGlobalName(unwrappedExpression.right, context);
|
|
37830
|
+
const leftComparedType = isNodeOfType(unwrappedExpression.left, "Literal") && typeof unwrappedExpression.left.value === "string" ? unwrappedExpression.left.value : null;
|
|
37831
|
+
const rightComparedType = isNodeOfType(unwrappedExpression.right, "Literal") && typeof unwrappedExpression.right.value === "string" ? unwrappedExpression.right.value : null;
|
|
37832
|
+
const guardName = leftTypeofName && rightComparedType ? leftTypeofName : rightTypeofName && leftComparedType ? rightTypeofName : null;
|
|
37833
|
+
const comparedType = leftTypeofName && rightComparedType ? rightComparedType : rightTypeofName && leftComparedType ? leftComparedType : null;
|
|
37834
|
+
if (!guardName || !browserGuardCoversGlobal(guardName, browserGlobalName)) return null;
|
|
37835
|
+
if (!comparedType) return null;
|
|
37836
|
+
const isEquality = unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==";
|
|
37837
|
+
const isInequality = unwrappedExpression.operator === "!==" || unwrappedExpression.operator === "!=";
|
|
37838
|
+
if (!isEquality && !isInequality) return null;
|
|
37839
|
+
const browserType = guardName === "matchMedia" ? "function" : "object";
|
|
37840
|
+
const browserResult = isEquality ? browserType === comparedType : browserType !== comparedType;
|
|
37841
|
+
if (browserResult === (isEquality ? comparedType === "undefined" : comparedType !== "undefined")) return null;
|
|
37842
|
+
return predicateResult === browserResult;
|
|
37843
|
+
};
|
|
37844
|
+
const isInsideAvailabilityGuard = (node, browserGlobalName, context) => {
|
|
37845
|
+
let currentNode = node;
|
|
37846
|
+
let parentNode = currentNode.parent;
|
|
37847
|
+
while (parentNode) {
|
|
37848
|
+
if (isFunctionLike$1(parentNode) && !executesDuringRender(parentNode, context.scopes)) break;
|
|
37849
|
+
if (isNodeOfType(parentNode, "LogicalExpression") && (parentNode.operator === "&&" || parentNode.operator === "||") && parentNode.right === currentNode && readAvailabilityWhenPredicate(parentNode.left, browserGlobalName, context, parentNode.operator === "&&") === true) return true;
|
|
37850
|
+
if (isNodeOfType(parentNode, "ConditionalExpression")) {
|
|
37851
|
+
if (parentNode.consequent === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, true) === true || parentNode.alternate === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, false) === true) return true;
|
|
37852
|
+
}
|
|
37853
|
+
if (isNodeOfType(parentNode, "IfStatement")) {
|
|
37854
|
+
if (parentNode.consequent === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, true) === true || parentNode.alternate === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, false) === true) return true;
|
|
37855
|
+
}
|
|
37856
|
+
currentNode = parentNode;
|
|
37857
|
+
parentNode = currentNode.parent;
|
|
37858
|
+
}
|
|
37859
|
+
return false;
|
|
37860
|
+
};
|
|
37861
|
+
const isAfterAvailabilityEarlyExit = (node, componentOrHookNode, browserGlobalName, context) => {
|
|
37862
|
+
const enclosingFunction = findEnclosingFunction(node);
|
|
37863
|
+
if (!enclosingFunction || enclosingFunction !== componentOrHookNode && !executesDuringRender(enclosingFunction, context.scopes) || !isFunctionLike$1(enclosingFunction) || !isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
|
|
37864
|
+
let currentNode = node;
|
|
37865
|
+
while (currentNode !== enclosingFunction) {
|
|
37866
|
+
const parentNode = currentNode.parent;
|
|
37867
|
+
if (!parentNode) return false;
|
|
37868
|
+
if (isNodeOfType(parentNode, "BlockStatement")) for (const statement of parentNode.body) {
|
|
37869
|
+
if (statement === currentNode) break;
|
|
37870
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
37871
|
+
if (readAvailabilityWhenPredicate(statement.test, browserGlobalName, context, false) === true && statementAlwaysExits(statement.consequent)) return true;
|
|
37872
|
+
if (readAvailabilityWhenPredicate(statement.test, browserGlobalName, context, true) === true && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
|
|
37873
|
+
}
|
|
37874
|
+
currentNode = parentNode;
|
|
37875
|
+
}
|
|
37876
|
+
return false;
|
|
37877
|
+
};
|
|
37878
|
+
const isTypeofProbe = (node) => {
|
|
37879
|
+
const expressionRoot = findTransparentExpressionRoot(node);
|
|
37880
|
+
const parentNode = expressionRoot.parent;
|
|
37881
|
+
return isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "typeof" && parentNode.argument === expressionRoot;
|
|
37882
|
+
};
|
|
37883
|
+
const noUnguardedBrowserGlobalInRenderOrHookInit = defineRule({
|
|
37884
|
+
id: "no-unguarded-browser-global-in-render-or-hook-init",
|
|
37885
|
+
title: "Browser global read during server render",
|
|
37886
|
+
severity: "error",
|
|
37887
|
+
category: "Correctness",
|
|
37888
|
+
requires: ["ssr"],
|
|
37889
|
+
recommendation: "Move browser-only reads into an effect or event, guard them behind a client-only render path, or use useSyncExternalStore with a stable server snapshot.",
|
|
37890
|
+
create: (context) => {
|
|
37891
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
37892
|
+
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
37893
|
+
let fileIsEmailTemplate = false;
|
|
37894
|
+
const reportedNodes = /* @__PURE__ */ new Set();
|
|
37895
|
+
const reportBrowserRead = (node, browserGlobalName) => {
|
|
37896
|
+
if (reportedNodes.has(node) || isTypeofProbe(node)) return;
|
|
37897
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node, context.scopes);
|
|
37898
|
+
if (!componentOrHookNode) return;
|
|
37899
|
+
if (fileIsEmailTemplate) return;
|
|
37900
|
+
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(node) ?? node)) return;
|
|
37901
|
+
if (isGatedByFalsyInitialState(node, context.scopes)) return;
|
|
37902
|
+
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode, context.scopes)) return;
|
|
37903
|
+
if (isInsideAvailabilityGuard(node, browserGlobalName, context)) return;
|
|
37904
|
+
if (isAfterAvailabilityEarlyExit(node, componentOrHookNode, browserGlobalName, context)) return;
|
|
37905
|
+
reportedNodes.add(node);
|
|
37906
|
+
context.report({
|
|
37907
|
+
node,
|
|
37908
|
+
message: `\`${browserGlobalName}\` is read while React is rendering on the server, where browser globals are unavailable. Move the read into an effect or event, or provide a stable server snapshot.`
|
|
37909
|
+
});
|
|
37910
|
+
};
|
|
37911
|
+
return {
|
|
37912
|
+
Program(node) {
|
|
37913
|
+
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
37914
|
+
},
|
|
37915
|
+
Identifier(node) {
|
|
37916
|
+
if (!BROWSER_GLOBAL_NAMES.has(node.name)) return;
|
|
37917
|
+
if (!context.scopes.isGlobalReference(node)) return;
|
|
37918
|
+
reportBrowserRead(node, node.name);
|
|
37919
|
+
},
|
|
37920
|
+
MemberExpression(node) {
|
|
37921
|
+
if (node.computed) return;
|
|
37922
|
+
const objectNode = stripParenExpression(node.object);
|
|
37923
|
+
if (!isNodeOfType(objectNode, "Identifier") || objectNode.name !== "globalThis" || !context.scopes.isGlobalReference(objectNode) || !isNodeOfType(node.property, "Identifier") || !BROWSER_GLOBAL_NAMES.has(node.property.name)) return;
|
|
37924
|
+
reportBrowserRead(node, node.property.name);
|
|
37925
|
+
}
|
|
37926
|
+
};
|
|
37927
|
+
}
|
|
37928
|
+
});
|
|
37929
|
+
//#endregion
|
|
36275
37930
|
//#region src/plugin/constants/dom-aria-properties.ts
|
|
36276
37931
|
const ARIA_PROPERTY_NAMES = new Set([
|
|
36277
37932
|
"activedescendant",
|
|
@@ -40114,6 +41769,123 @@ const preferUseEffectEvent = defineRule({
|
|
|
40114
41769
|
}
|
|
40115
41770
|
});
|
|
40116
41771
|
//#endregion
|
|
41772
|
+
//#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
|
|
41773
|
+
const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
41774
|
+
"each",
|
|
41775
|
+
"every",
|
|
41776
|
+
"filter",
|
|
41777
|
+
"find",
|
|
41778
|
+
"findIndex",
|
|
41779
|
+
"findLast",
|
|
41780
|
+
"findLastIndex",
|
|
41781
|
+
"flatMap",
|
|
41782
|
+
"forEach",
|
|
41783
|
+
"map",
|
|
41784
|
+
"reduce",
|
|
41785
|
+
"reduceRight",
|
|
41786
|
+
"some",
|
|
41787
|
+
"sort",
|
|
41788
|
+
"toSorted"
|
|
41789
|
+
]);
|
|
41790
|
+
const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
41791
|
+
"from",
|
|
41792
|
+
"fromAsync",
|
|
41793
|
+
"groupBy"
|
|
41794
|
+
]);
|
|
41795
|
+
const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
41796
|
+
const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
|
|
41797
|
+
const isListenerRemovalViaNullHandler = (callNode) => {
|
|
41798
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
41799
|
+
const callee = unwrapChainExpression(callNode.callee);
|
|
41800
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
|
|
41801
|
+
};
|
|
41802
|
+
const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
|
|
41803
|
+
"off",
|
|
41804
|
+
"removeEventListener",
|
|
41805
|
+
"removeListener"
|
|
41806
|
+
]);
|
|
41807
|
+
const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
|
|
41808
|
+
if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
|
|
41809
|
+
const handlerArgument = callNode.arguments?.[1];
|
|
41810
|
+
return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
|
|
41811
|
+
};
|
|
41812
|
+
const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
41813
|
+
const callNode = unwrapChainExpression(node);
|
|
41814
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
41815
|
+
if (isListenerRemovalViaNullHandler(callNode)) return true;
|
|
41816
|
+
const callee = unwrapChainExpression(callNode.callee);
|
|
41817
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
41818
|
+
if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
|
|
41819
|
+
if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
|
|
41820
|
+
if (knownCleanupFunctionNames.has(callee.name)) return true;
|
|
41821
|
+
return false;
|
|
41822
|
+
}
|
|
41823
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
41824
|
+
if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
|
|
41825
|
+
if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
|
|
41826
|
+
return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
|
|
41827
|
+
}
|
|
41828
|
+
return false;
|
|
41829
|
+
};
|
|
41830
|
+
const isStaticIteratorCallbackCallee = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && isNodeOfType(callee.property, "Identifier") && (callee.object.name === "Array" || callee.object.name === "Map" || callee.object.name === "Object") && STATIC_ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
|
|
41831
|
+
const isIteratorCallbackArgument = (node) => {
|
|
41832
|
+
const parentNode = node.parent;
|
|
41833
|
+
if (!isNodeOfType(parentNode, "CallExpression")) return false;
|
|
41834
|
+
if (!parentNode.arguments?.some((argument) => argument === node)) return false;
|
|
41835
|
+
const callee = unwrapChainExpression(parentNode.callee);
|
|
41836
|
+
if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
|
|
41837
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
|
|
41838
|
+
};
|
|
41839
|
+
const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
41840
|
+
let didFindRelease = false;
|
|
41841
|
+
walkAst(node, (child) => {
|
|
41842
|
+
if (didFindRelease) return false;
|
|
41843
|
+
if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
|
|
41844
|
+
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
41845
|
+
didFindRelease = true;
|
|
41846
|
+
return false;
|
|
41847
|
+
}
|
|
41848
|
+
});
|
|
41849
|
+
return didFindRelease;
|
|
41850
|
+
};
|
|
41851
|
+
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
41852
|
+
if (!isFunctionLike$1(node)) return false;
|
|
41853
|
+
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
41854
|
+
};
|
|
41855
|
+
const isProvablyNoOpCleanupFunction = (node) => {
|
|
41856
|
+
if (!isFunctionLike$1(node)) return false;
|
|
41857
|
+
let sawNoOpRemoval = false;
|
|
41858
|
+
let sawOtherCall = false;
|
|
41859
|
+
walkAst(node.body, (child) => {
|
|
41860
|
+
if (sawOtherCall) return false;
|
|
41861
|
+
if (isFunctionLike$1(child)) return false;
|
|
41862
|
+
const callNode = unwrapChainExpression(child);
|
|
41863
|
+
if (!isNodeOfType(callNode, "CallExpression")) return;
|
|
41864
|
+
const callee = unwrapChainExpression(callNode.callee);
|
|
41865
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
|
|
41866
|
+
sawNoOpRemoval = true;
|
|
41867
|
+
return;
|
|
41868
|
+
}
|
|
41869
|
+
sawOtherCall = true;
|
|
41870
|
+
});
|
|
41871
|
+
return sawNoOpRemoval && !sawOtherCall;
|
|
41872
|
+
};
|
|
41873
|
+
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
|
|
41874
|
+
if (!returnedValue) return false;
|
|
41875
|
+
const unwrappedValue = unwrapChainExpression(returnedValue);
|
|
41876
|
+
if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
|
|
41877
|
+
if (isNodeOfType(unwrappedValue, "Identifier")) {
|
|
41878
|
+
if (unwrappedValue.name === "undefined") return false;
|
|
41879
|
+
if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
|
|
41880
|
+
return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
|
|
41881
|
+
}
|
|
41882
|
+
if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
41883
|
+
if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
|
|
41884
|
+
if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
41885
|
+
if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
|
|
41886
|
+
return false;
|
|
41887
|
+
};
|
|
41888
|
+
//#endregion
|
|
40117
41889
|
//#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
|
|
40118
41890
|
const findUseEffectsInComponent = (componentBody) => {
|
|
40119
41891
|
const effectCalls = [];
|
|
@@ -41582,7 +43354,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
41582
43354
|
title: "Time or random value in JSX",
|
|
41583
43355
|
severity: "warn",
|
|
41584
43356
|
category: "Correctness",
|
|
41585
|
-
|
|
43357
|
+
requires: ["ssr"],
|
|
41586
43358
|
recommendation: "Move time or random values into useEffect+useState so they only run in the browser, or add suppressHydrationWarning to the parent if it's intentional",
|
|
41587
43359
|
create: (context) => {
|
|
41588
43360
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
@@ -41596,8 +43368,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
41596
43368
|
const matched = NONDETERMINISTIC_RENDER_PATTERNS.find((pattern) => pattern.matches(node.expression));
|
|
41597
43369
|
if (matched) {
|
|
41598
43370
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
41599
|
-
if (
|
|
41600
|
-
if (isGatedByFalsyInitialState(node)) return;
|
|
43371
|
+
if (isGatedByFalsyInitialState(node, context.scopes)) return;
|
|
41601
43372
|
if (isInsideMotionTransitionAttribute(node)) return;
|
|
41602
43373
|
context.report({
|
|
41603
43374
|
node,
|
|
@@ -41606,11 +43377,10 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
41606
43377
|
return;
|
|
41607
43378
|
}
|
|
41608
43379
|
walkAst(node.expression, (child) => {
|
|
41609
|
-
if (isFunctionLike$1(child) && !executesDuringRender(child)) return false;
|
|
43380
|
+
if (isFunctionLike$1(child) && !executesDuringRender(child, context.scopes)) return false;
|
|
41610
43381
|
for (const pattern of NONDETERMINISTIC_RENDER_PATTERNS) if (pattern.matches(child)) {
|
|
41611
43382
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
41612
|
-
if (
|
|
41613
|
-
if (isGatedByFalsyInitialState(child)) return;
|
|
43383
|
+
if (isGatedByFalsyInitialState(child, context.scopes)) return;
|
|
41614
43384
|
if (isInsideMotionTransitionAttribute(child)) return;
|
|
41615
43385
|
if (pattern.display === "new Date()" && isYearOnlyDateRead(child)) return;
|
|
41616
43386
|
context.report({
|
|
@@ -54947,6 +56717,18 @@ const reactDoctorRules = [
|
|
|
54947
56717
|
category: "Accessibility"
|
|
54948
56718
|
}
|
|
54949
56719
|
},
|
|
56720
|
+
{
|
|
56721
|
+
key: "react-doctor/no-hydration-branch-on-browser-global",
|
|
56722
|
+
id: "no-hydration-branch-on-browser-global",
|
|
56723
|
+
source: "react-doctor",
|
|
56724
|
+
originallyExternal: false,
|
|
56725
|
+
rule: {
|
|
56726
|
+
...noHydrationBranchOnBrowserGlobal,
|
|
56727
|
+
framework: "global",
|
|
56728
|
+
category: "Bugs",
|
|
56729
|
+
requires: [...new Set(["react", ...noHydrationBranchOnBrowserGlobal.requires ?? []])]
|
|
56730
|
+
}
|
|
56731
|
+
},
|
|
54950
56732
|
{
|
|
54951
56733
|
key: "react-doctor/no-img-lazy-with-high-fetchpriority",
|
|
54952
56734
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
@@ -54959,6 +56741,18 @@ const reactDoctorRules = [
|
|
|
54959
56741
|
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
54960
56742
|
}
|
|
54961
56743
|
},
|
|
56744
|
+
{
|
|
56745
|
+
key: "react-doctor/no-impure-state-updater",
|
|
56746
|
+
id: "no-impure-state-updater",
|
|
56747
|
+
source: "react-doctor",
|
|
56748
|
+
originallyExternal: false,
|
|
56749
|
+
rule: {
|
|
56750
|
+
...noImpureStateUpdater,
|
|
56751
|
+
framework: "global",
|
|
56752
|
+
category: "Bugs",
|
|
56753
|
+
requires: [...new Set(["react", ...noImpureStateUpdater.requires ?? []])]
|
|
56754
|
+
}
|
|
56755
|
+
},
|
|
54962
56756
|
{
|
|
54963
56757
|
key: "react-doctor/no-indeterminate-attribute",
|
|
54964
56758
|
id: "no-indeterminate-attribute",
|
|
@@ -55466,6 +57260,18 @@ const reactDoctorRules = [
|
|
|
55466
57260
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
55467
57261
|
}
|
|
55468
57262
|
},
|
|
57263
|
+
{
|
|
57264
|
+
key: "react-doctor/no-ref-current-in-render",
|
|
57265
|
+
id: "no-ref-current-in-render",
|
|
57266
|
+
source: "react-doctor",
|
|
57267
|
+
originallyExternal: false,
|
|
57268
|
+
rule: {
|
|
57269
|
+
...noRefCurrentInRender,
|
|
57270
|
+
framework: "global",
|
|
57271
|
+
category: "Bugs",
|
|
57272
|
+
requires: [...new Set(["react", ...noRefCurrentInRender.requires ?? []])]
|
|
57273
|
+
}
|
|
57274
|
+
},
|
|
55469
57275
|
{
|
|
55470
57276
|
key: "react-doctor/no-render-in-render",
|
|
55471
57277
|
id: "no-render-in-render",
|
|
@@ -55710,6 +57516,18 @@ const reactDoctorRules = [
|
|
|
55710
57516
|
requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
|
|
55711
57517
|
}
|
|
55712
57518
|
},
|
|
57519
|
+
{
|
|
57520
|
+
key: "react-doctor/no-unguarded-browser-global-in-render-or-hook-init",
|
|
57521
|
+
id: "no-unguarded-browser-global-in-render-or-hook-init",
|
|
57522
|
+
source: "react-doctor",
|
|
57523
|
+
originallyExternal: false,
|
|
57524
|
+
rule: {
|
|
57525
|
+
...noUnguardedBrowserGlobalInRenderOrHookInit,
|
|
57526
|
+
framework: "global",
|
|
57527
|
+
category: "Bugs",
|
|
57528
|
+
requires: [...new Set(["react", ...noUnguardedBrowserGlobalInRenderOrHookInit.requires ?? []])]
|
|
57529
|
+
}
|
|
57530
|
+
},
|
|
55713
57531
|
{
|
|
55714
57532
|
key: "react-doctor/no-unknown-property",
|
|
55715
57533
|
id: "no-unknown-property",
|
|
@@ -58042,10 +59860,17 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
58042
59860
|
"nextjs-no-use-search-params-without-suspense",
|
|
58043
59861
|
"no-dynamic-import-path",
|
|
58044
59862
|
"no-full-lodash-import",
|
|
59863
|
+
"no-hydration-branch-on-browser-global",
|
|
58045
59864
|
"no-indeterminate-attribute",
|
|
58046
59865
|
"no-locale-format-in-render",
|
|
58047
59866
|
"no-match-media-in-state-initializer",
|
|
59867
|
+
"no-adjust-state-on-prop-change",
|
|
59868
|
+
"no-derived-state",
|
|
59869
|
+
"no-derived-state-effect",
|
|
59870
|
+
"no-event-handler",
|
|
59871
|
+
"no-initialize-state",
|
|
58048
59872
|
"no-mutating-reducer-state",
|
|
59873
|
+
"no-unguarded-browser-global-in-render-or-hook-init",
|
|
58049
59874
|
"prefer-dynamic-import",
|
|
58050
59875
|
"rendering-hydration-mismatch-time",
|
|
58051
59876
|
"rn-no-legacy-shadow-styles",
|
|
@@ -58097,6 +59922,9 @@ const collectNextjsSearchParamsDependencies = ({ absoluteFilePath, staticImports
|
|
|
58097
59922
|
if (hasAncestorSuspenseLayout(absoluteFilePath)) return;
|
|
58098
59923
|
for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
|
|
58099
59924
|
};
|
|
59925
|
+
const collectEffectValueHelperDependencies = ({ absoluteFilePath, staticImports }) => {
|
|
59926
|
+
for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
|
|
59927
|
+
};
|
|
58100
59928
|
const collectMutatingReducerDependencies = ({ absoluteFilePath, sourceText, staticImports, program }) => {
|
|
58101
59929
|
const namedUseReducerLocals = /* @__PURE__ */ new Set();
|
|
58102
59930
|
const reactObjectLocals = /* @__PURE__ */ new Set();
|
|
@@ -58179,10 +60007,17 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
58179
60007
|
["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
|
|
58180
60008
|
["no-dynamic-import-path", collectNearestManifestDependencies],
|
|
58181
60009
|
["no-full-lodash-import", collectNearestManifestDependencies],
|
|
60010
|
+
["no-hydration-branch-on-browser-global", collectNearestManifestDependencies],
|
|
58182
60011
|
["no-indeterminate-attribute", collectNearestManifestDependencies],
|
|
58183
60012
|
["no-locale-format-in-render", collectNearestManifestDependencies],
|
|
58184
60013
|
["no-match-media-in-state-initializer", collectNearestManifestDependencies],
|
|
60014
|
+
["no-adjust-state-on-prop-change", collectEffectValueHelperDependencies],
|
|
60015
|
+
["no-derived-state", collectEffectValueHelperDependencies],
|
|
60016
|
+
["no-derived-state-effect", collectEffectValueHelperDependencies],
|
|
60017
|
+
["no-event-handler", collectEffectValueHelperDependencies],
|
|
60018
|
+
["no-initialize-state", collectEffectValueHelperDependencies],
|
|
58185
60019
|
["no-mutating-reducer-state", collectMutatingReducerDependencies],
|
|
60020
|
+
["no-unguarded-browser-global-in-render-or-hook-init", collectNearestManifestDependencies],
|
|
58186
60021
|
["prefer-dynamic-import", collectNearestManifestDependencies],
|
|
58187
60022
|
["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
|
|
58188
60023
|
["rn-no-legacy-shadow-styles", collectLegacyArchDependencies],
|