oxlint-plugin-react-doctor 0.7.4-dev.b47d053 → 0.7.4-dev.d4f2209
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 +138 -0
- package/dist/index.js +2326 -809
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -553,7 +553,7 @@ const EXTERNAL_SYNC_OBSERVER_CONSTRUCTORS = new Set([
|
|
|
553
553
|
"ResizeObserver",
|
|
554
554
|
"PerformanceObserver"
|
|
555
555
|
]);
|
|
556
|
-
const STORAGE_OBJECTS
|
|
556
|
+
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
557
557
|
//#endregion
|
|
558
558
|
//#region src/plugin/constants/react.ts
|
|
559
559
|
const INDEX_PARAMETER_NAMES = new Set([
|
|
@@ -5139,8 +5139,10 @@ const STORAGE_GLOBALS = new Set([
|
|
|
5139
5139
|
const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
|
|
5140
5140
|
const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
|
|
5141
5141
|
const STRONG_AUTH_KEY_PATTERN = /jwt|secret|password|passwd|credential|private[-_]?key|api[-_]?key|bearer|access[-_]?token|refresh[-_]?token|auth[-_]?token|id[-_]?token|session/i;
|
|
5142
|
+
const PRODUCT_API_KEY_COLLECTION_PATTERN = /[._:-](?:created|generated|saved)[-_]?api[-_]?keys$/i;
|
|
5142
5143
|
const isAuthCredentialKey = (key) => {
|
|
5143
5144
|
if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
|
|
5145
|
+
if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
|
|
5144
5146
|
if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
|
|
5145
5147
|
return true;
|
|
5146
5148
|
};
|
|
@@ -6135,7 +6137,6 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
|
6135
6137
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6136
6138
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6137
6139
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
6138
|
-
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
6139
6140
|
const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
|
|
6140
6141
|
const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
|
|
6141
6142
|
const resolveStringKey = (keyArg, context) => {
|
|
@@ -6158,7 +6159,7 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6158
6159
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6159
6160
|
const receiver = stripParenExpression(node.callee.object);
|
|
6160
6161
|
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6161
|
-
if (
|
|
6162
|
+
if (receiver.name !== "localStorage") return;
|
|
6162
6163
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6163
6164
|
if (node.callee.property.name !== "setItem") return;
|
|
6164
6165
|
const keyArg = node.arguments?.[0];
|
|
@@ -8406,6 +8407,54 @@ const effectListenerCleanupMismatch = defineRule({
|
|
|
8406
8407
|
} })
|
|
8407
8408
|
});
|
|
8408
8409
|
//#endregion
|
|
8410
|
+
//#region src/plugin/utils/collect-effect-invoked-functions.ts
|
|
8411
|
+
const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
|
|
8412
|
+
"then",
|
|
8413
|
+
"catch",
|
|
8414
|
+
"finally"
|
|
8415
|
+
]);
|
|
8416
|
+
const collectEffectInvokedFunctions = (effectCallback) => {
|
|
8417
|
+
const invokedFunctions = new Set([effectCallback]);
|
|
8418
|
+
const localFunctionBindings = /* @__PURE__ */ new Map();
|
|
8419
|
+
const calledBindingNames = /* @__PURE__ */ new Set();
|
|
8420
|
+
const pendingFunctions = [effectCallback];
|
|
8421
|
+
const enqueue = (candidate) => {
|
|
8422
|
+
const strippedCandidate = candidate ? stripParenExpression(candidate) : candidate;
|
|
8423
|
+
if (!isFunctionLike$1(strippedCandidate) || invokedFunctions.has(strippedCandidate)) return;
|
|
8424
|
+
invokedFunctions.add(strippedCandidate);
|
|
8425
|
+
pendingFunctions.push(strippedCandidate);
|
|
8426
|
+
};
|
|
8427
|
+
const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
|
|
8428
|
+
while (pendingFunctions.length > 0) {
|
|
8429
|
+
const currentFunction = pendingFunctions.pop();
|
|
8430
|
+
if (!currentFunction) break;
|
|
8431
|
+
walkAst(currentFunction, (child) => {
|
|
8432
|
+
if (child !== currentFunction && isFunctionLike$1(child)) {
|
|
8433
|
+
if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) localFunctionBindings.set(child.id.name, child);
|
|
8434
|
+
return false;
|
|
8435
|
+
}
|
|
8436
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
8437
|
+
const initializer = child.init ? stripParenExpression(child.init) : null;
|
|
8438
|
+
if (isFunctionLike$1(initializer)) localFunctionBindings.set(child.id.name, initializer);
|
|
8439
|
+
return;
|
|
8440
|
+
}
|
|
8441
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8442
|
+
const callee = stripParenExpression(child.callee);
|
|
8443
|
+
if (isFunctionLike$1(callee)) {
|
|
8444
|
+
enqueue(callee);
|
|
8445
|
+
return;
|
|
8446
|
+
}
|
|
8447
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
8448
|
+
calledBindingNames.add(callee.name);
|
|
8449
|
+
return;
|
|
8450
|
+
}
|
|
8451
|
+
if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
8452
|
+
});
|
|
8453
|
+
for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
|
|
8454
|
+
}
|
|
8455
|
+
return invokedFunctions;
|
|
8456
|
+
};
|
|
8457
|
+
//#endregion
|
|
8409
8458
|
//#region src/plugin/utils/is-react-hook-name.ts
|
|
8410
8459
|
const isReactHookName = (name) => {
|
|
8411
8460
|
if (!name.startsWith("use")) return false;
|
|
@@ -8461,46 +8510,90 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
8461
8510
|
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
8462
8511
|
};
|
|
8463
8512
|
//#endregion
|
|
8513
|
+
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
8514
|
+
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
8515
|
+
let ancestor = bindingIdentifier.parent;
|
|
8516
|
+
while (ancestor) {
|
|
8517
|
+
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
8518
|
+
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
8519
|
+
ancestor = ancestor.parent ?? null;
|
|
8520
|
+
}
|
|
8521
|
+
return null;
|
|
8522
|
+
};
|
|
8523
|
+
//#endregion
|
|
8524
|
+
//#region src/plugin/utils/executes-during-render.ts
|
|
8525
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
|
|
8526
|
+
"map",
|
|
8527
|
+
"filter",
|
|
8528
|
+
"forEach",
|
|
8529
|
+
"flatMap",
|
|
8530
|
+
"reduce",
|
|
8531
|
+
"reduceRight",
|
|
8532
|
+
"some",
|
|
8533
|
+
"every",
|
|
8534
|
+
"find",
|
|
8535
|
+
"findIndex",
|
|
8536
|
+
"findLast",
|
|
8537
|
+
"findLastIndex",
|
|
8538
|
+
"sort",
|
|
8539
|
+
"toSorted"
|
|
8540
|
+
]);
|
|
8541
|
+
const REACT_RENDER_PHASE_HOOK_NAMES = new Set(["useMemo", "useState"]);
|
|
8542
|
+
const isGlobalArrayFromMember = (node, scopes) => {
|
|
8543
|
+
const unwrappedNode = stripParenExpression(node);
|
|
8544
|
+
if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed || !isNodeOfType(unwrappedNode.object, "Identifier") || unwrappedNode.object.name !== "Array" || !scopes.isGlobalReference(unwrappedNode.object) || !isNodeOfType(unwrappedNode.property, "Identifier")) return false;
|
|
8545
|
+
return unwrappedNode.property.name === "from";
|
|
8546
|
+
};
|
|
8547
|
+
const isConstAliasOfGlobalArrayFrom = (node, scopes) => {
|
|
8548
|
+
const unwrappedNode = stripParenExpression(node);
|
|
8549
|
+
if (!isNodeOfType(unwrappedNode, "Identifier")) return false;
|
|
8550
|
+
const binding = findVariableInitializer(unwrappedNode, unwrappedNode.name);
|
|
8551
|
+
if (!binding?.initializer) return false;
|
|
8552
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
8553
|
+
return Boolean(declarator && scopes.symbolFor(unwrappedNode)?.declarationNode === declarator && declarator.init && declarator.parent && isNodeOfType(declarator.parent, "VariableDeclaration") && declarator.parent.kind === "const" && isGlobalArrayFromMember(declarator.init, scopes));
|
|
8554
|
+
};
|
|
8555
|
+
const executesDuringRender = (functionNode, scopes) => {
|
|
8556
|
+
const parent = functionNode.parent;
|
|
8557
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
8558
|
+
if (parent.callee === functionNode) return true;
|
|
8559
|
+
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;
|
|
8560
|
+
if (scopes && parent.arguments?.[1] === functionNode && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
|
|
8561
|
+
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;
|
|
8562
|
+
};
|
|
8563
|
+
//#endregion
|
|
8564
|
+
//#region src/plugin/utils/find-render-phase-component-or-hook.ts
|
|
8565
|
+
const findRenderPhaseComponentOrHook = (node, scopes) => {
|
|
8566
|
+
let functionNode = findEnclosingFunction(node);
|
|
8567
|
+
while (functionNode) {
|
|
8568
|
+
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
8569
|
+
if (!executesDuringRender(functionNode, scopes)) return null;
|
|
8570
|
+
functionNode = findEnclosingFunction(functionNode);
|
|
8571
|
+
}
|
|
8572
|
+
return null;
|
|
8573
|
+
};
|
|
8574
|
+
//#endregion
|
|
8575
|
+
//#region src/plugin/utils/get-function-binding-name.ts
|
|
8576
|
+
const getFunctionBindingIdentifier = (functionNode) => {
|
|
8577
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
8578
|
+
const parent = functionNode.parent;
|
|
8579
|
+
if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
|
|
8580
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionNode && isNodeOfType(parent.left, "Identifier")) return parent.left;
|
|
8581
|
+
if (isNodeOfType(parent, "CallExpression")) {
|
|
8582
|
+
const callParent = parent.parent;
|
|
8583
|
+
if (isNodeOfType(callParent, "VariableDeclarator") && isNodeOfType(callParent.id, "Identifier")) return callParent.id;
|
|
8584
|
+
}
|
|
8585
|
+
return null;
|
|
8586
|
+
};
|
|
8587
|
+
const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(functionNode)?.name ?? null;
|
|
8588
|
+
//#endregion
|
|
8464
8589
|
//#region src/plugin/utils/get-range-start.ts
|
|
8465
8590
|
const getRangeStart = (node) => {
|
|
8466
8591
|
const rangeStart = node.range?.[0];
|
|
8467
8592
|
return typeof rangeStart === "number" ? rangeStart : null;
|
|
8468
8593
|
};
|
|
8469
8594
|
//#endregion
|
|
8470
|
-
//#region src/plugin/utils/is-
|
|
8471
|
-
const
|
|
8472
|
-
let node = callExpression;
|
|
8473
|
-
let parent = node.parent;
|
|
8474
|
-
while (parent) {
|
|
8475
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
8476
|
-
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
8477
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
8478
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
8479
|
-
node = parent;
|
|
8480
|
-
parent = node.parent;
|
|
8481
|
-
continue;
|
|
8482
|
-
}
|
|
8483
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
8484
|
-
node = parent;
|
|
8485
|
-
parent = node.parent;
|
|
8486
|
-
continue;
|
|
8487
|
-
}
|
|
8488
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
8489
|
-
node = parent;
|
|
8490
|
-
parent = node.parent;
|
|
8491
|
-
continue;
|
|
8492
|
-
}
|
|
8493
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
8494
|
-
const expressions = parent.expressions ?? [];
|
|
8495
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
8496
|
-
node = parent;
|
|
8497
|
-
parent = node.parent;
|
|
8498
|
-
continue;
|
|
8499
|
-
}
|
|
8500
|
-
return false;
|
|
8501
|
-
}
|
|
8502
|
-
return false;
|
|
8503
|
-
};
|
|
8595
|
+
//#region src/plugin/utils/is-event-handler-attribute.ts
|
|
8596
|
+
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
|
|
8504
8597
|
//#endregion
|
|
8505
8598
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
8506
8599
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
@@ -8535,125 +8628,9 @@ const isCleanupReturningSubscribeLikeCallExpression = (node) => {
|
|
|
8535
8628
|
return true;
|
|
8536
8629
|
};
|
|
8537
8630
|
//#endregion
|
|
8538
|
-
//#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
|
|
8539
|
-
const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
|
|
8540
|
-
"each",
|
|
8541
|
-
"every",
|
|
8542
|
-
"filter",
|
|
8543
|
-
"find",
|
|
8544
|
-
"findIndex",
|
|
8545
|
-
"findLast",
|
|
8546
|
-
"findLastIndex",
|
|
8547
|
-
"flatMap",
|
|
8548
|
-
"forEach",
|
|
8549
|
-
"map",
|
|
8550
|
-
"reduce",
|
|
8551
|
-
"reduceRight",
|
|
8552
|
-
"some",
|
|
8553
|
-
"sort",
|
|
8554
|
-
"toSorted"
|
|
8555
|
-
]);
|
|
8556
|
-
const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
8557
|
-
"from",
|
|
8558
|
-
"fromAsync",
|
|
8559
|
-
"groupBy"
|
|
8560
|
-
]);
|
|
8561
|
-
const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8562
|
-
const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
|
|
8563
|
-
const isListenerRemovalViaNullHandler = (callNode) => {
|
|
8564
|
-
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
8565
|
-
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8566
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
|
|
8567
|
-
};
|
|
8568
|
-
const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
|
|
8569
|
-
"off",
|
|
8570
|
-
"removeEventListener",
|
|
8571
|
-
"removeListener"
|
|
8572
|
-
]);
|
|
8573
|
-
const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
|
|
8574
|
-
if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
|
|
8575
|
-
const handlerArgument = callNode.arguments?.[1];
|
|
8576
|
-
return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
|
|
8577
|
-
};
|
|
8578
|
-
const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8579
|
-
const callNode = unwrapChainExpression$3(node);
|
|
8580
|
-
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
8581
|
-
if (isListenerRemovalViaNullHandler(callNode)) return true;
|
|
8582
|
-
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8583
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
8584
|
-
if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
|
|
8585
|
-
if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
|
|
8586
|
-
if (knownCleanupFunctionNames.has(callee.name)) return true;
|
|
8587
|
-
return false;
|
|
8588
|
-
}
|
|
8589
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
8590
|
-
if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
|
|
8591
|
-
if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
|
|
8592
|
-
return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
|
|
8593
|
-
}
|
|
8594
|
-
return false;
|
|
8595
|
-
};
|
|
8596
|
-
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);
|
|
8597
|
-
const isIteratorCallbackArgument = (node) => {
|
|
8598
|
-
const parentNode = node.parent;
|
|
8599
|
-
if (!isNodeOfType(parentNode, "CallExpression")) return false;
|
|
8600
|
-
if (!parentNode.arguments?.some((argument) => argument === node)) return false;
|
|
8601
|
-
const callee = unwrapChainExpression$3(parentNode.callee);
|
|
8602
|
-
if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
|
|
8603
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(callee.property.name);
|
|
8604
|
-
};
|
|
8605
|
-
const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8606
|
-
let didFindRelease = false;
|
|
8607
|
-
walkAst(node, (child) => {
|
|
8608
|
-
if (didFindRelease) return false;
|
|
8609
|
-
if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
|
|
8610
|
-
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
8611
|
-
didFindRelease = true;
|
|
8612
|
-
return false;
|
|
8613
|
-
}
|
|
8614
|
-
});
|
|
8615
|
-
return didFindRelease;
|
|
8616
|
-
};
|
|
8617
|
-
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8618
|
-
if (!isFunctionLike$1(node)) return false;
|
|
8619
|
-
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
8620
|
-
};
|
|
8621
|
-
const isProvablyNoOpCleanupFunction = (node) => {
|
|
8622
|
-
if (!isFunctionLike$1(node)) return false;
|
|
8623
|
-
let sawNoOpRemoval = false;
|
|
8624
|
-
let sawOtherCall = false;
|
|
8625
|
-
walkAst(node.body, (child) => {
|
|
8626
|
-
if (sawOtherCall) return false;
|
|
8627
|
-
if (isFunctionLike$1(child)) return false;
|
|
8628
|
-
const callNode = unwrapChainExpression$3(child);
|
|
8629
|
-
if (!isNodeOfType(callNode, "CallExpression")) return;
|
|
8630
|
-
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8631
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
|
|
8632
|
-
sawNoOpRemoval = true;
|
|
8633
|
-
return;
|
|
8634
|
-
}
|
|
8635
|
-
sawOtherCall = true;
|
|
8636
|
-
});
|
|
8637
|
-
return sawNoOpRemoval && !sawOtherCall;
|
|
8638
|
-
};
|
|
8639
|
-
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
|
|
8640
|
-
if (!returnedValue) return false;
|
|
8641
|
-
const unwrappedValue = unwrapChainExpression$3(returnedValue);
|
|
8642
|
-
if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
|
|
8643
|
-
if (isNodeOfType(unwrappedValue, "Identifier")) {
|
|
8644
|
-
if (unwrappedValue.name === "undefined") return false;
|
|
8645
|
-
if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
|
|
8646
|
-
return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
|
|
8647
|
-
}
|
|
8648
|
-
if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
8649
|
-
if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
|
|
8650
|
-
if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
8651
|
-
if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
|
|
8652
|
-
return false;
|
|
8653
|
-
};
|
|
8654
|
-
//#endregion
|
|
8655
8631
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
8656
8632
|
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
8633
|
+
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
8657
8634
|
const RESOURCE_NOUN_BY_KIND = {
|
|
8658
8635
|
subscribe: "subscription",
|
|
8659
8636
|
timer: "timer",
|
|
@@ -8664,7 +8641,66 @@ const isSubscribeOrObserveCall = (node) => {
|
|
|
8664
8641
|
if (isSubscribeLikeCallExpression(node)) return true;
|
|
8665
8642
|
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
8666
8643
|
};
|
|
8667
|
-
const
|
|
8644
|
+
const resolveExpressionKey$1 = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
8645
|
+
if (!expression) return null;
|
|
8646
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
8647
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
8648
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
8649
|
+
if (!symbol) return context.scopes.isGlobalReference(unwrappedExpression) ? `global:${unwrappedExpression.name}` : null;
|
|
8650
|
+
if (visitedSymbolIds.has(symbol.id)) return `symbol:${symbol.id}`;
|
|
8651
|
+
visitedSymbolIds.add(symbol.id);
|
|
8652
|
+
const bindingProperty = symbol.bindingIdentifier.parent;
|
|
8653
|
+
const bindingPattern = bindingProperty?.parent;
|
|
8654
|
+
const variableDeclarator = bindingPattern?.parent;
|
|
8655
|
+
const bindingPropertyName = isNodeOfType(bindingProperty, "Property") ? getStaticPropertyKeyName(bindingProperty) : null;
|
|
8656
|
+
if (bindingPropertyName && isNodeOfType(bindingPattern, "ObjectPattern") && isNodeOfType(variableDeclarator, "VariableDeclarator") && variableDeclarator.id === bindingPattern) {
|
|
8657
|
+
const objectKey = resolveExpressionKey$1(variableDeclarator.init, context, visitedSymbolIds);
|
|
8658
|
+
return objectKey ? `${objectKey}.${bindingPropertyName}` : `symbol:${symbol.id}`;
|
|
8659
|
+
}
|
|
8660
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
8661
|
+
if (symbol.kind === "const" && initializer && (isNodeOfType(initializer, "Identifier") || isNodeOfType(initializer, "MemberExpression"))) return resolveExpressionKey$1(initializer, context, visitedSymbolIds) ?? `symbol:${symbol.id}`;
|
|
8662
|
+
return `symbol:${symbol.id}`;
|
|
8663
|
+
}
|
|
8664
|
+
if (isNodeOfType(unwrappedExpression, "MemberExpression") && !unwrappedExpression.computed) {
|
|
8665
|
+
if (!isNodeOfType(unwrappedExpression.property, "Identifier")) return null;
|
|
8666
|
+
const objectKey = resolveExpressionKey$1(unwrappedExpression.object, context, visitedSymbolIds);
|
|
8667
|
+
return objectKey ? `${objectKey}.${unwrappedExpression.property.name}` : null;
|
|
8668
|
+
}
|
|
8669
|
+
if (isNodeOfType(unwrappedExpression, "ThisExpression")) return "this";
|
|
8670
|
+
if (isNodeOfType(unwrappedExpression, "Literal") && (typeof unwrappedExpression.value === "string" || typeof unwrappedExpression.value === "number")) return `literal:${String(unwrappedExpression.value)}`;
|
|
8671
|
+
if (isFunctionLike$1(unwrappedExpression)) {
|
|
8672
|
+
const rangeStart = getRangeStart(unwrappedExpression);
|
|
8673
|
+
return rangeStart === null ? null : `function:${rangeStart}`;
|
|
8674
|
+
}
|
|
8675
|
+
return null;
|
|
8676
|
+
};
|
|
8677
|
+
const findAssignedResourceKey = (resourceNode, context) => {
|
|
8678
|
+
let currentNode = resourceNode;
|
|
8679
|
+
let parentNode = currentNode.parent;
|
|
8680
|
+
while (isNodeOfType(parentNode, "ChainExpression")) {
|
|
8681
|
+
currentNode = parentNode;
|
|
8682
|
+
parentNode = currentNode.parent;
|
|
8683
|
+
}
|
|
8684
|
+
if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode) return resolveExpressionKey$1(parentNode.id, context);
|
|
8685
|
+
if (isNodeOfType(parentNode, "AssignmentExpression") && parentNode.right === currentNode) return resolveExpressionKey$1(parentNode.left, context);
|
|
8686
|
+
return null;
|
|
8687
|
+
};
|
|
8688
|
+
const getCallRegistrationDetails = (callNode, context) => {
|
|
8689
|
+
const callee = stripParenExpression(callNode.callee);
|
|
8690
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return {
|
|
8691
|
+
receiverKey: null,
|
|
8692
|
+
registrationVerbName: null,
|
|
8693
|
+
eventKey: null,
|
|
8694
|
+
handlerKey: null
|
|
8695
|
+
};
|
|
8696
|
+
return {
|
|
8697
|
+
receiverKey: resolveExpressionKey$1(callee.object, context),
|
|
8698
|
+
registrationVerbName: callee.property.name,
|
|
8699
|
+
eventKey: resolveExpressionKey$1(callNode.arguments?.[0], context),
|
|
8700
|
+
handlerKey: resolveExpressionKey$1(callNode.arguments?.[1], context)
|
|
8701
|
+
};
|
|
8702
|
+
};
|
|
8703
|
+
const findSubscribeLikeUsages = (callback, context) => {
|
|
8668
8704
|
const usages = [];
|
|
8669
8705
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8670
8706
|
let cleanupArgument = null;
|
|
@@ -8673,13 +8709,22 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
8673
8709
|
const lastCallbackStatement = callbackStatements[callbackStatements.length - 1];
|
|
8674
8710
|
if (isNodeOfType(lastCallbackStatement, "ReturnStatement") && lastCallbackStatement.argument) cleanupArgument = lastCallbackStatement.argument;
|
|
8675
8711
|
}
|
|
8712
|
+
const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
|
|
8676
8713
|
walkAst(callback, (child) => {
|
|
8677
|
-
if (child
|
|
8714
|
+
if (child !== callback && isFunctionLike$1(child)) {
|
|
8715
|
+
if (child === cleanupArgument) return false;
|
|
8716
|
+
if (!effectInvokedFunctions.has(child) && !isSynchronousIteratorCallback(child)) return false;
|
|
8717
|
+
}
|
|
8678
8718
|
if (isSocketConstruction(child)) {
|
|
8679
8719
|
usages.push({
|
|
8680
8720
|
kind: "socket",
|
|
8681
8721
|
node: child,
|
|
8682
|
-
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8722
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket",
|
|
8723
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
8724
|
+
receiverKey: null,
|
|
8725
|
+
registrationVerbName: null,
|
|
8726
|
+
eventKey: null,
|
|
8727
|
+
handlerKey: null
|
|
8683
8728
|
});
|
|
8684
8729
|
return;
|
|
8685
8730
|
}
|
|
@@ -8688,118 +8733,348 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
8688
8733
|
usages.push({
|
|
8689
8734
|
kind: "timer",
|
|
8690
8735
|
node: child,
|
|
8691
|
-
resourceName: child.callee.name
|
|
8736
|
+
resourceName: child.callee.name,
|
|
8737
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
8738
|
+
receiverKey: null,
|
|
8739
|
+
registrationVerbName: child.callee.name,
|
|
8740
|
+
eventKey: null,
|
|
8741
|
+
handlerKey: null
|
|
8692
8742
|
});
|
|
8693
8743
|
return;
|
|
8694
8744
|
}
|
|
8695
|
-
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))
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
const bindings = {
|
|
8705
|
-
cleanupFunctionNames: /* @__PURE__ */ new Set(),
|
|
8706
|
-
subscriptionNames: /* @__PURE__ */ new Set(),
|
|
8707
|
-
effectScopeVariableNames: /* @__PURE__ */ new Set()
|
|
8708
|
-
};
|
|
8709
|
-
if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
|
|
8710
|
-
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
|
|
8711
|
-
walkAst(effectCallback.body, (child) => {
|
|
8712
|
-
if (!isSubscribeOrObserveCall(child)) return;
|
|
8713
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8714
|
-
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
8715
|
-
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
8716
|
-
bindings.subscriptionNames.add(child.callee.object.name);
|
|
8717
|
-
});
|
|
8718
|
-
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
8719
|
-
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
8720
|
-
for (const declarator of child.declarations ?? []) {
|
|
8721
|
-
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
8722
|
-
const bindingName = declarator.id.name;
|
|
8723
|
-
bindings.effectScopeVariableNames.add(bindingName);
|
|
8724
|
-
const init = declarator.init;
|
|
8725
|
-
if (!init) continue;
|
|
8726
|
-
if (isSocketConstruction(init)) {
|
|
8727
|
-
bindings.subscriptionNames.add(bindingName);
|
|
8728
|
-
continue;
|
|
8729
|
-
}
|
|
8730
|
-
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
8731
|
-
if (isSubscribeLikeCallExpression(init)) {
|
|
8732
|
-
bindings.subscriptionNames.add(bindingName);
|
|
8733
|
-
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
8734
|
-
}
|
|
8735
|
-
}
|
|
8736
|
-
});
|
|
8737
|
-
walkAst(effectCallback.body, (child) => {
|
|
8738
|
-
if (child !== effectCallback.body && (isNodeOfType(child, "ArrowFunctionExpression") || isNodeOfType(child, "FunctionExpression"))) return false;
|
|
8739
|
-
if (isNodeOfType(child, "FunctionDeclaration") && child.id && isCleanupFunctionLike(child, bindings.cleanupFunctionNames, bindings.subscriptionNames)) {
|
|
8740
|
-
bindings.cleanupFunctionNames.add(child.id.name);
|
|
8741
|
-
return false;
|
|
8742
|
-
}
|
|
8743
|
-
});
|
|
8744
|
-
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
8745
|
-
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
8746
|
-
for (const declarator of child.declarations ?? []) {
|
|
8747
|
-
if (!isNodeOfType(declarator.id, "Identifier") || !declarator.init) continue;
|
|
8748
|
-
if (isCleanupFunctionLike(declarator.init, bindings.cleanupFunctionNames, bindings.subscriptionNames)) bindings.cleanupFunctionNames.add(declarator.id.name);
|
|
8745
|
+
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)) {
|
|
8746
|
+
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
8747
|
+
usages.push({
|
|
8748
|
+
kind: "subscribe",
|
|
8749
|
+
node: child,
|
|
8750
|
+
resourceName: child.callee.property.name,
|
|
8751
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
8752
|
+
...registrationDetails
|
|
8753
|
+
});
|
|
8749
8754
|
}
|
|
8750
8755
|
});
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
const
|
|
8756
|
+
return usages.filter((usage) => isNodeReachableWithinFunction(usage.node, context));
|
|
8757
|
+
};
|
|
8758
|
+
const isNodeReachableWithinFunction = (node, context) => {
|
|
8759
|
+
const owner = context.cfg.enclosingFunction(node);
|
|
8760
|
+
if (!owner) return true;
|
|
8761
|
+
const functionCfg = context.cfg.cfgFor(owner);
|
|
8762
|
+
if (!functionCfg) return true;
|
|
8763
|
+
const targetBlock = functionCfg.blockOf(node);
|
|
8764
|
+
if (!targetBlock) return true;
|
|
8765
|
+
const visitedBlocks = new Set([functionCfg.entry]);
|
|
8766
|
+
const pendingBlocks = [functionCfg.entry];
|
|
8767
|
+
while (pendingBlocks.length > 0) {
|
|
8768
|
+
const currentBlock = pendingBlocks.pop();
|
|
8769
|
+
if (!currentBlock) break;
|
|
8770
|
+
if (currentBlock === targetBlock) return true;
|
|
8771
|
+
for (const edge of currentBlock.successors) {
|
|
8772
|
+
if (visitedBlocks.has(edge.to)) continue;
|
|
8773
|
+
visitedBlocks.add(edge.to);
|
|
8774
|
+
pendingBlocks.push(edge.to);
|
|
8775
|
+
}
|
|
8776
|
+
}
|
|
8777
|
+
return false;
|
|
8778
|
+
};
|
|
8779
|
+
const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, context) => {
|
|
8780
|
+
let pathAnchor = usageNode;
|
|
8781
|
+
let pathOwner = findEnclosingFunction(pathAnchor);
|
|
8782
|
+
while (pathOwner && isSynchronousIteratorCallback(pathOwner)) {
|
|
8783
|
+
if (matchingNodes.length > 0 && matchingNodes.every((matchingNode) => context.cfg.enclosingFunction(matchingNode) === pathOwner)) break;
|
|
8784
|
+
const iteratorCall = pathOwner.parent;
|
|
8785
|
+
if (!isNodeOfType(iteratorCall, "CallExpression")) break;
|
|
8786
|
+
pathAnchor = iteratorCall;
|
|
8787
|
+
pathOwner = findEnclosingFunction(pathAnchor);
|
|
8788
|
+
}
|
|
8789
|
+
const owner = context.cfg.enclosingFunction(pathAnchor);
|
|
8790
|
+
if (!owner) return false;
|
|
8791
|
+
const functionCfg = context.cfg.cfgFor(owner);
|
|
8792
|
+
if (!functionCfg) return false;
|
|
8793
|
+
const usageBlock = functionCfg.blockOf(pathAnchor);
|
|
8794
|
+
if (!usageBlock) return false;
|
|
8795
|
+
const usageStart = getRangeStart(usageNode);
|
|
8796
|
+
const matchingBlocks = new Set(matchingNodes.flatMap((matchingNode) => {
|
|
8797
|
+
if (context.cfg.enclosingFunction(matchingNode) !== owner) return [];
|
|
8798
|
+
const matchingBlock = functionCfg.blockOf(matchingNode);
|
|
8799
|
+
if (!matchingBlock) return [];
|
|
8800
|
+
const matchingStart = getRangeStart(matchingNode);
|
|
8801
|
+
if (matchingBlock === usageBlock && usageStart !== null && matchingStart !== null && matchingStart < usageStart) return [];
|
|
8802
|
+
return [matchingBlock];
|
|
8803
|
+
}));
|
|
8804
|
+
if (matchingBlocks.has(usageBlock)) return true;
|
|
8805
|
+
const visitedBlocks = new Set([usageBlock]);
|
|
8806
|
+
const pendingBlocks = [usageBlock];
|
|
8807
|
+
while (pendingBlocks.length > 0) {
|
|
8808
|
+
const currentBlock = pendingBlocks.pop();
|
|
8809
|
+
if (!currentBlock) break;
|
|
8810
|
+
for (const edge of currentBlock.successors) {
|
|
8811
|
+
if (matchingBlocks.has(edge.to)) continue;
|
|
8812
|
+
if (edge.to === functionCfg.exit) return false;
|
|
8813
|
+
if (visitedBlocks.has(edge.to)) continue;
|
|
8814
|
+
visitedBlocks.add(edge.to);
|
|
8815
|
+
pendingBlocks.push(edge.to);
|
|
8816
|
+
}
|
|
8817
|
+
}
|
|
8818
|
+
return matchingBlocks.size > 0;
|
|
8819
|
+
};
|
|
8820
|
+
const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
|
|
8757
8821
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8758
8822
|
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
8759
|
-
const
|
|
8823
|
+
const releaseCalls = [];
|
|
8760
8824
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8761
|
-
if (!
|
|
8762
|
-
|
|
8763
|
-
if (releaseStart !== null) releaseStarts.push(releaseStart);
|
|
8825
|
+
if (!isNodeOfType(isNodeOfType(child, "ChainExpression") ? child.expression : child, "CallExpression")) return;
|
|
8826
|
+
releaseCalls.push(child);
|
|
8764
8827
|
});
|
|
8765
|
-
if (
|
|
8828
|
+
if (releaseCalls.length === 0) return usages;
|
|
8766
8829
|
return usages.filter((usage) => {
|
|
8767
8830
|
const usageStart = getRangeStart(usage.node);
|
|
8768
8831
|
if (usageStart === null) return true;
|
|
8769
|
-
|
|
8832
|
+
const matchingReleaseCalls = releaseCalls.filter((releaseCall) => {
|
|
8833
|
+
const releaseStart = getRangeStart(releaseCall);
|
|
8834
|
+
return releaseStart !== null && releaseStart > usageStart && doesReleaseCallMatchUsage(releaseCall, usage, context);
|
|
8835
|
+
});
|
|
8836
|
+
return !doMatchingNodesCoverEveryPathAfterUsage(usage.node, matchingReleaseCalls, context);
|
|
8770
8837
|
});
|
|
8771
8838
|
};
|
|
8772
|
-
const
|
|
8773
|
-
if (
|
|
8774
|
-
const
|
|
8775
|
-
if (
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8839
|
+
const resolveIteratorCollectionKey = (expression, context) => {
|
|
8840
|
+
if (!expression || !isNodeOfType(expression, "Identifier")) return null;
|
|
8841
|
+
const symbol = context.scopes.symbolFor(expression);
|
|
8842
|
+
if (!symbol || symbol.kind !== "parameter") return null;
|
|
8843
|
+
let callbackNode = symbol.bindingIdentifier.parent;
|
|
8844
|
+
while (callbackNode && !isFunctionLike$1(callbackNode)) callbackNode = callbackNode.parent;
|
|
8845
|
+
if (!callbackNode || !isFunctionLike$1(callbackNode)) return null;
|
|
8846
|
+
const callNode = callbackNode.parent;
|
|
8847
|
+
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8848
|
+
const callee = stripParenExpression(callNode.callee);
|
|
8849
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
|
|
8850
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from" && callNode.arguments?.[1] === callbackNode) return resolveExpressionKey$1(callNode.arguments[0], context);
|
|
8851
|
+
if (callNode.arguments?.[0] === callbackNode) return resolveExpressionKey$1(callee.object, context);
|
|
8852
|
+
}
|
|
8853
|
+
return null;
|
|
8854
|
+
};
|
|
8855
|
+
const findCollectionMappingCall = (callbackNode) => {
|
|
8856
|
+
if (!isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression") || callbackNode.async || callbackNode.generator) return null;
|
|
8857
|
+
const callNode = callbackNode.parent;
|
|
8858
|
+
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8859
|
+
const callee = stripParenExpression(callNode.callee);
|
|
8860
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return null;
|
|
8861
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from" && callNode.arguments?.[1] === callbackNode) return callNode;
|
|
8862
|
+
return callee.property.name === "map" && callNode.arguments?.[0] === callbackNode ? callNode : null;
|
|
8863
|
+
};
|
|
8864
|
+
const findMappedResourceCollectionKey = (resourceNode, context) => {
|
|
8865
|
+
const callbackNode = findEnclosingFunction(resourceNode);
|
|
8866
|
+
if (!callbackNode || !isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression")) return null;
|
|
8867
|
+
const mappingCall = findCollectionMappingCall(callbackNode);
|
|
8868
|
+
if (!mappingCall) return null;
|
|
8869
|
+
if (isNodeOfType(callbackNode.body, "BlockStatement")) {
|
|
8870
|
+
const resourceRoot = findTransparentExpressionRoot(resourceNode);
|
|
8871
|
+
const resourceDeclarator = resourceRoot.parent;
|
|
8872
|
+
const resourceDeclaration = resourceDeclarator?.parent;
|
|
8873
|
+
if (!isNodeOfType(resourceDeclarator, "VariableDeclarator") || resourceDeclarator.init !== resourceRoot || !isNodeOfType(resourceDeclarator.id, "Identifier") || !isNodeOfType(resourceDeclaration, "VariableDeclaration") || resourceDeclaration.kind !== "const" || resourceDeclaration.parent !== callbackNode.body) return null;
|
|
8874
|
+
const returnStatements = [];
|
|
8875
|
+
walkAst(callbackNode.body, (child) => {
|
|
8876
|
+
if (child !== callbackNode.body && isFunctionLike$1(child)) return false;
|
|
8877
|
+
if (isNodeOfType(child, "ReturnStatement")) returnStatements.push(child);
|
|
8878
|
+
});
|
|
8879
|
+
const returnStatement = returnStatements[0];
|
|
8880
|
+
const callbackStatements = callbackNode.body.body ?? [];
|
|
8881
|
+
const returnedIdentifier = isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument ? stripParenExpression(returnStatement.argument) : null;
|
|
8882
|
+
const resourceSymbol = context.scopes.symbolFor(resourceDeclarator.id);
|
|
8883
|
+
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;
|
|
8884
|
+
} else if (findTransparentExpressionRoot(resourceNode) !== callbackNode.body) return null;
|
|
8885
|
+
const mappingRoot = findTransparentExpressionRoot(mappingCall);
|
|
8886
|
+
const collectionDeclarator = mappingRoot.parent;
|
|
8887
|
+
return isNodeOfType(collectionDeclarator, "VariableDeclarator") && collectionDeclarator.init === mappingRoot ? resolveExpressionKey$1(collectionDeclarator.id, context) : null;
|
|
8888
|
+
};
|
|
8889
|
+
const findContainingCollectionKey = (resourceNode, context) => {
|
|
8890
|
+
const mappedCollectionKey = findMappedResourceCollectionKey(resourceNode, context);
|
|
8891
|
+
if (mappedCollectionKey !== null) return mappedCollectionKey;
|
|
8892
|
+
let currentNode = resourceNode;
|
|
8893
|
+
let parentNode = currentNode.parent;
|
|
8894
|
+
while (parentNode) {
|
|
8895
|
+
if (isFunctionLike$1(parentNode)) return null;
|
|
8896
|
+
if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode) return resolveExpressionKey$1(parentNode.id, context);
|
|
8897
|
+
currentNode = parentNode;
|
|
8898
|
+
parentNode = currentNode.parent;
|
|
8899
|
+
}
|
|
8900
|
+
return null;
|
|
8901
|
+
};
|
|
8902
|
+
const isWithinAssignmentTarget = (identifier) => {
|
|
8903
|
+
let currentNode = identifier;
|
|
8904
|
+
let parentNode = currentNode.parent;
|
|
8905
|
+
while (parentNode) {
|
|
8906
|
+
if (isNodeOfType(parentNode, "AssignmentExpression")) return parentNode.left === currentNode;
|
|
8907
|
+
if (isNodeOfType(parentNode, "UpdateExpression") || isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "delete") return parentNode.argument === currentNode;
|
|
8908
|
+
if (isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return parentNode.left === currentNode;
|
|
8909
|
+
currentNode = parentNode;
|
|
8910
|
+
parentNode = currentNode.parent;
|
|
8911
|
+
}
|
|
8912
|
+
return false;
|
|
8913
|
+
};
|
|
8914
|
+
const resolveStableValue = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
8915
|
+
if (!expression) return null;
|
|
8916
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
8917
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return unwrappedExpression;
|
|
8918
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
8919
|
+
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;
|
|
8920
|
+
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;
|
|
8921
|
+
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;
|
|
8922
|
+
if (!symbol || symbol.kind !== "const" && !isUnreassignedMutableBinding && !isUnreassignedFunctionBinding || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return unwrappedExpression;
|
|
8923
|
+
visitedSymbolIds.add(symbol.id);
|
|
8924
|
+
return resolveStableValue(symbol.initializer, context, visitedSymbolIds);
|
|
8925
|
+
};
|
|
8926
|
+
const resolveObjectExpression = (expression, context) => {
|
|
8927
|
+
const resolvedExpression = resolveStableValue(expression, context);
|
|
8928
|
+
return isNodeOfType(resolvedExpression, "ObjectExpression") ? resolvedExpression : null;
|
|
8929
|
+
};
|
|
8930
|
+
const getListenerAbortControllerKey = (usage, context) => {
|
|
8931
|
+
if (usage.registrationVerbName !== "addEventListener" || !isNodeOfType(usage.node, "CallExpression")) return null;
|
|
8932
|
+
const optionsArgument = usage.node.arguments?.[2];
|
|
8933
|
+
const optionsObject = resolveObjectExpression(optionsArgument, context);
|
|
8934
|
+
if (!optionsObject) return null;
|
|
8935
|
+
for (const property of optionsObject.properties ?? []) {
|
|
8936
|
+
if (!isNodeOfType(property, "Property") || getStaticPropertyKeyName(property) !== "signal") continue;
|
|
8937
|
+
const signalKey = resolveExpressionKey$1(property.value, context);
|
|
8938
|
+
return signalKey?.endsWith(".signal") ? signalKey.slice(0, -7) : null;
|
|
8939
|
+
}
|
|
8940
|
+
return null;
|
|
8941
|
+
};
|
|
8942
|
+
const SYNCHRONOUS_ITERATOR_METHOD_NAMES$1 = new Set([
|
|
8943
|
+
"every",
|
|
8944
|
+
"filter",
|
|
8945
|
+
"flatMap",
|
|
8946
|
+
"forEach",
|
|
8947
|
+
"map",
|
|
8948
|
+
"reduce",
|
|
8949
|
+
"reduceRight",
|
|
8950
|
+
"some"
|
|
8951
|
+
]);
|
|
8952
|
+
const isSynchronousIteratorCallback = (functionNode) => {
|
|
8953
|
+
const callNode = functionNode.parent;
|
|
8954
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
8955
|
+
const callee = stripParenExpression(callNode.callee);
|
|
8956
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
8957
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from") return callNode.arguments?.[1] === functionNode;
|
|
8958
|
+
return SYNCHRONOUS_ITERATOR_METHOD_NAMES$1.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
|
|
8959
|
+
};
|
|
8960
|
+
const findDirectCallForReference = (identifier) => {
|
|
8961
|
+
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
8962
|
+
const callNode = expressionRoot.parent;
|
|
8963
|
+
return isNodeOfType(callNode, "CallExpression") && callNode.callee === expressionRoot ? callNode : null;
|
|
8964
|
+
};
|
|
8965
|
+
const findSingleDirectInvocation = (functionNode, caller, context) => {
|
|
8966
|
+
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
8967
|
+
if (!bindingIdentifier || resolveStableValue(bindingIdentifier, context) !== functionNode) return null;
|
|
8968
|
+
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
8969
|
+
if (!symbol) return null;
|
|
8970
|
+
const invocationCalls = symbol.references.flatMap((reference) => {
|
|
8971
|
+
const callNode = findDirectCallForReference(reference.identifier);
|
|
8972
|
+
return callNode ? [callNode] : [];
|
|
8973
|
+
});
|
|
8974
|
+
if (invocationCalls.length !== 1) return null;
|
|
8975
|
+
const invocationCall = invocationCalls[0];
|
|
8976
|
+
return findEnclosingFunction(invocationCall) === caller && isNodeReachableWithinFunction(invocationCall, context) ? invocationCall : null;
|
|
8977
|
+
};
|
|
8978
|
+
const resolveCleanupPathAnchor = (usageNode, effectCallback, context) => {
|
|
8979
|
+
const usageFunction = findEnclosingFunction(usageNode);
|
|
8980
|
+
if (!usageFunction || usageFunction === effectCallback) return usageNode;
|
|
8981
|
+
return findSingleDirectInvocation(usageFunction, effectCallback, context) ?? usageNode;
|
|
8982
|
+
};
|
|
8983
|
+
const resolveSingleAssignedCleanupFunction = (expression, usage, context) => {
|
|
8984
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
8985
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
|
|
8986
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
8987
|
+
const initializer = symbol?.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
8988
|
+
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;
|
|
8989
|
+
const assignmentReferences = symbol.references.filter((reference) => isWithinAssignmentTarget(reference.identifier));
|
|
8990
|
+
if (assignmentReferences.length !== 1) return null;
|
|
8991
|
+
const assignmentReference = assignmentReferences[0];
|
|
8992
|
+
const assignmentTarget = findTransparentExpressionRoot(assignmentReference.identifier);
|
|
8993
|
+
const assignmentNode = assignmentTarget.parent;
|
|
8994
|
+
if (!isNodeOfType(assignmentNode, "AssignmentExpression") || assignmentNode.operator !== "=" || assignmentNode.left !== assignmentTarget || findEnclosingFunction(assignmentNode) !== findEnclosingFunction(usage.node) || !doMatchingNodesCoverEveryPathAfterUsage(usage.node, [assignmentNode], context)) return null;
|
|
8995
|
+
const assignedValue = stripParenExpression(assignmentNode.right);
|
|
8996
|
+
return isFunctionLike$1(assignedValue) ? assignedValue : null;
|
|
8997
|
+
};
|
|
8998
|
+
const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visitedFunctions = /* @__PURE__ */ new Set()) => {
|
|
8999
|
+
if (!isFunctionLike$1(cleanupFunction) || visitedFunctions.has(cleanupFunction)) return false;
|
|
9000
|
+
visitedFunctions.add(cleanupFunction);
|
|
9001
|
+
let didCleanupFunctionMatch = false;
|
|
9002
|
+
walkAst(cleanupFunction.body, (cleanupChild) => {
|
|
9003
|
+
if (didCleanupFunctionMatch) return false;
|
|
9004
|
+
if (cleanupChild !== cleanupFunction.body && isFunctionLike$1(cleanupChild) && !isSynchronousIteratorCallback(cleanupChild)) return false;
|
|
9005
|
+
if (doesReleaseCallMatchUsage(cleanupChild, usage, context)) {
|
|
9006
|
+
didCleanupFunctionMatch = true;
|
|
9007
|
+
return false;
|
|
9008
|
+
}
|
|
9009
|
+
const helperCall = isNodeOfType(cleanupChild, "ChainExpression") ? cleanupChild.expression : cleanupChild;
|
|
9010
|
+
if (!isNodeOfType(helperCall, "CallExpression")) return;
|
|
9011
|
+
const stableHelperFunction = resolveStableValue(helperCall.callee, context);
|
|
9012
|
+
const helperFunction = isNodeOfType(stableHelperFunction, "Identifier") ? resolveSingleAssignedCleanupFunction(stableHelperFunction, usage, context) : stableHelperFunction;
|
|
9013
|
+
if (helperFunction && isFunctionLike$1(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context, visitedFunctions)) {
|
|
9014
|
+
didCleanupFunctionMatch = true;
|
|
9015
|
+
return false;
|
|
9016
|
+
}
|
|
8779
9017
|
});
|
|
9018
|
+
return didCleanupFunctionMatch;
|
|
8780
9019
|
};
|
|
8781
|
-
const
|
|
9020
|
+
const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
8782
9021
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
8783
|
-
if (
|
|
8784
|
-
|
|
8785
|
-
|
|
9022
|
+
if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
9023
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return callback.body === usage.node && isCleanupReturningSubscribeLikeCallExpression(callback.body);
|
|
9024
|
+
const matchingCleanupReturns = [];
|
|
8786
9025
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8787
|
-
if (didFindCleanupReturn) return;
|
|
8788
9026
|
if (!isNodeOfType(child, "ReturnStatement")) return;
|
|
8789
|
-
|
|
8790
|
-
|
|
9027
|
+
const returnStart = getRangeStart(child);
|
|
9028
|
+
const usageStart = getRangeStart(usage.node);
|
|
9029
|
+
if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
|
|
9030
|
+
const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
|
|
9031
|
+
if (!returnedValue) return;
|
|
9032
|
+
if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
|
|
9033
|
+
matchingCleanupReturns.push(child);
|
|
9034
|
+
return;
|
|
9035
|
+
}
|
|
9036
|
+
if (usage.kind === "subscribe" && isNodeOfType(returnedValue, "Identifier") && usage.handleKey !== null && resolveExpressionKey$1(returnedValue, context) === usage.handleKey && isCleanupReturningSubscribeLikeCallExpression(usage.node)) {
|
|
9037
|
+
matchingCleanupReturns.push(child);
|
|
9038
|
+
return;
|
|
9039
|
+
}
|
|
9040
|
+
if (isNodeOfType(returnedValue, "Identifier")) {
|
|
9041
|
+
if (returnedValue.name === "undefined" && context.scopes.isGlobalReference(returnedValue)) return;
|
|
9042
|
+
const returnedKey = resolveExpressionKey$1(returnedValue, context);
|
|
9043
|
+
if (usage.handleKey !== null && returnedKey === usage.handleKey) return;
|
|
9044
|
+
if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
|
|
9045
|
+
}
|
|
9046
|
+
const cleanupFunction = resolveStableValue(returnedValue, context);
|
|
9047
|
+
if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
|
|
9048
|
+
if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
|
|
8791
9049
|
});
|
|
8792
|
-
return
|
|
9050
|
+
return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
|
|
9051
|
+
};
|
|
9052
|
+
const findFirstUsageWithoutCleanup = (callback, usages, context) => {
|
|
9053
|
+
for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context)) return usage;
|
|
9054
|
+
return null;
|
|
9055
|
+
};
|
|
9056
|
+
const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
9057
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
9058
|
+
if (isNodeOfType(unwrappedExpression, "NewExpression") && isNodeOfType(unwrappedExpression.callee, "Identifier") && unwrappedExpression.callee.name === "AbortController") return true;
|
|
9059
|
+
if (isNodeOfType(unwrappedExpression, "MemberExpression")) return isLocalAbortControllerExpression(unwrappedExpression.object, context, visitedSymbolIds);
|
|
9060
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return false;
|
|
9061
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
9062
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
9063
|
+
visitedSymbolIds.add(symbol.id);
|
|
9064
|
+
if (symbol.initializer) return isLocalAbortControllerExpression(symbol.initializer, context, visitedSymbolIds);
|
|
9065
|
+
const bindingProperty = symbol.bindingIdentifier.parent;
|
|
9066
|
+
const bindingPattern = bindingProperty?.parent;
|
|
9067
|
+
const variableDeclarator = bindingPattern?.parent;
|
|
9068
|
+
return Boolean(isNodeOfType(bindingProperty, "Property") && isNodeOfType(bindingPattern, "ObjectPattern") && isNodeOfType(variableDeclarator, "VariableDeclarator") && variableDeclarator.init && isLocalAbortControllerExpression(variableDeclarator.init, context, visitedSymbolIds));
|
|
8793
9069
|
};
|
|
8794
|
-
const
|
|
8795
|
-
const isSelfReleasingListenerOptionProperty = (property) => {
|
|
9070
|
+
const isSelfReleasingListenerOptionProperty = (property, context) => {
|
|
8796
9071
|
if (!isNodeOfType(property, "Property")) return false;
|
|
8797
9072
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
|
|
8798
|
-
if (keyName === "signal") return
|
|
9073
|
+
if (keyName === "signal") return !isLocalAbortControllerExpression(property.value, context);
|
|
8799
9074
|
if (keyName !== "once") return false;
|
|
8800
9075
|
return isNodeOfType(property.value, "Literal") && property.value.value === true;
|
|
8801
9076
|
};
|
|
8802
|
-
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
|
|
9077
|
+
const hasSelfReleasingListenerOptions = (node, context) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some((property) => isSelfReleasingListenerOptionProperty(property, context)));
|
|
8803
9078
|
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
8804
9079
|
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
8805
9080
|
["addListener", new Set([
|
|
@@ -8825,85 +9100,185 @@ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
|
8825
9100
|
"teardown"
|
|
8826
9101
|
]);
|
|
8827
9102
|
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8828
|
-
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
8829
9103
|
const getReleaseVerbName = (node) => {
|
|
8830
|
-
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
8831
9104
|
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8832
9105
|
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8833
9106
|
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
8834
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8835
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier"))
|
|
9107
|
+
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;
|
|
9108
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
9109
|
+
const methodName = callee.property.name;
|
|
9110
|
+
return GLOBAL_RELEASE_METHOD_NAMES.has(methodName) || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(methodName) || methodName === "on" ? methodName : null;
|
|
9111
|
+
}
|
|
8836
9112
|
return null;
|
|
8837
9113
|
};
|
|
9114
|
+
const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
9115
|
+
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
9116
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
9117
|
+
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
9118
|
+
if (usage.kind === "timer") {
|
|
9119
|
+
const expectedCleanupName = usage.registrationVerbName === "setInterval" ? "clearInterval" : "clearTimeout";
|
|
9120
|
+
if (!isNodeOfType(callee, "Identifier") || !TIMER_CLEANUP_CALLEE_NAMES.has(callee.name) || callee.name !== expectedCleanupName) return false;
|
|
9121
|
+
if (usage.handleKey !== null && resolveExpressionKey$1(callNode.arguments?.[0], context) === usage.handleKey) return true;
|
|
9122
|
+
const collectionKey = findContainingCollectionKey(usage.node, context);
|
|
9123
|
+
return collectionKey !== null && collectionKey === resolveIteratorCollectionKey(callNode.arguments?.[0], context);
|
|
9124
|
+
}
|
|
9125
|
+
if (isNodeOfType(callee, "Identifier") && usage.kind === "subscribe" && usage.handleKey !== null && resolveExpressionKey$1(callee, context) === usage.handleKey && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
9126
|
+
const releaseVerbName = getReleaseVerbName(callNode);
|
|
9127
|
+
if (!releaseVerbName) return false;
|
|
9128
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
9129
|
+
const releaseReceiverKey = resolveExpressionKey$1(callee.object, context);
|
|
9130
|
+
if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
|
|
9131
|
+
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;
|
|
9132
|
+
if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
|
|
9133
|
+
if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
|
|
9134
|
+
const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
9135
|
+
if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
|
|
9136
|
+
const releaseEventKey = resolveExpressionKey$1(callNode.arguments?.[0], context);
|
|
9137
|
+
if (usage.eventKey !== null && releaseEventKey !== null && usage.eventKey !== releaseEventKey) {
|
|
9138
|
+
const usageIteratorCollectionKey = isNodeOfType(usage.node, "CallExpression") ? resolveIteratorCollectionKey(usage.node.arguments?.[0], context) : null;
|
|
9139
|
+
const releaseIteratorCollectionKey = resolveIteratorCollectionKey(callNode.arguments?.[0], context);
|
|
9140
|
+
if (usageIteratorCollectionKey === null || usageIteratorCollectionKey !== releaseIteratorCollectionKey) return false;
|
|
9141
|
+
}
|
|
9142
|
+
if (releaseVerbName === "on") {
|
|
9143
|
+
const handlerArgument = callNode.arguments?.[1];
|
|
9144
|
+
return isNodeOfType(handlerArgument, "Literal") && handlerArgument.value === null;
|
|
9145
|
+
}
|
|
9146
|
+
if (releaseVerbName === "removeEventListener" || releaseVerbName === "removeListener" || releaseVerbName === "off") {
|
|
9147
|
+
const releaseHandler = callNode.arguments?.[1];
|
|
9148
|
+
if (!releaseHandler) return releaseVerbName === "off";
|
|
9149
|
+
return usage.handlerKey !== null && resolveExpressionKey$1(releaseHandler, context) === usage.handlerKey;
|
|
9150
|
+
}
|
|
9151
|
+
if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
|
|
9152
|
+
return true;
|
|
9153
|
+
};
|
|
8838
9154
|
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
8839
|
-
const
|
|
8840
|
-
let
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
9155
|
+
const isReturnedEffectCleanupFunction = (functionNode) => {
|
|
9156
|
+
let currentNode = functionNode;
|
|
9157
|
+
let parentNode = currentNode.parent;
|
|
9158
|
+
while (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
|
|
9159
|
+
currentNode = parentNode;
|
|
9160
|
+
parentNode = currentNode.parent;
|
|
9161
|
+
}
|
|
9162
|
+
if (!isNodeOfType(parentNode, "ReturnStatement") || parentNode.argument !== currentNode) return false;
|
|
9163
|
+
const effectCallback = findEnclosingFunction(parentNode);
|
|
9164
|
+
const effectCall = effectCallback?.parent;
|
|
9165
|
+
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isHookCall$2(effectCall, CLEANUP_EFFECT_HOOK_NAMES));
|
|
9166
|
+
};
|
|
9167
|
+
const isPotentiallyReachableFunction = (functionNode, context) => {
|
|
9168
|
+
if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode)) return true;
|
|
9169
|
+
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
9170
|
+
if (!bindingIdentifier) return false;
|
|
9171
|
+
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
9172
|
+
if (!symbol) return false;
|
|
9173
|
+
return symbol.references.some((reference) => findEnclosingFunction(reference.identifier) !== functionNode);
|
|
9174
|
+
};
|
|
9175
|
+
const isReleaseReachableForUsage = (releaseNode, usage, context) => {
|
|
9176
|
+
if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
|
|
9177
|
+
const releaseFunction = findEnclosingFunction(releaseNode);
|
|
9178
|
+
if (!releaseFunction) return true;
|
|
9179
|
+
if (releaseFunction === findEnclosingFunction(usage.node)) return true;
|
|
9180
|
+
return isPotentiallyReachableFunction(releaseFunction, context);
|
|
8851
9181
|
};
|
|
8852
|
-
const
|
|
8853
|
-
|
|
8854
|
-
let programNode = anyNode;
|
|
9182
|
+
const fileContainsReleaseForUsage = (usage, context) => {
|
|
9183
|
+
let programNode = usage.node;
|
|
8855
9184
|
while (programNode.parent) programNode = programNode.parent;
|
|
8856
|
-
|
|
8857
|
-
if (cached) return cached;
|
|
8858
|
-
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
9185
|
+
let didFindRelease = false;
|
|
8859
9186
|
walkAst(programNode, (child) => {
|
|
8860
|
-
|
|
8861
|
-
if (
|
|
9187
|
+
if (didFindRelease) return false;
|
|
9188
|
+
if (doesReleaseCallMatchUsage(child, usage, context) && isReleaseReachableForUsage(child, usage, context)) {
|
|
9189
|
+
didFindRelease = true;
|
|
9190
|
+
return false;
|
|
9191
|
+
}
|
|
8862
9192
|
});
|
|
8863
|
-
|
|
8864
|
-
return releaseVerbNames;
|
|
9193
|
+
return didFindRelease;
|
|
8865
9194
|
};
|
|
8866
|
-
const
|
|
8867
|
-
const
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
9195
|
+
const isUseSyncExternalStoreSubscribeFunction = (functionNode, context) => {
|
|
9196
|
+
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
9197
|
+
if (!bindingIdentifier) return false;
|
|
9198
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
9199
|
+
const isSubscribeBinding = (candidateBinding) => {
|
|
9200
|
+
const symbol = context.scopes.symbolFor(candidateBinding);
|
|
9201
|
+
if (!symbol || visitedSymbolIds.has(symbol.id) || symbol.references.length === 0) return false;
|
|
9202
|
+
visitedSymbolIds.add(symbol.id);
|
|
9203
|
+
return symbol.references.every((reference) => {
|
|
9204
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
9205
|
+
const referenceParent = referenceRoot.parent;
|
|
9206
|
+
if (isNodeOfType(referenceParent, "CallExpression") && referenceParent.arguments?.[0] === referenceRoot) return isReactApiCall(referenceParent, "useSyncExternalStore", context.scopes);
|
|
9207
|
+
const aliasDeclaration = referenceParent?.parent;
|
|
9208
|
+
return Boolean(isNodeOfType(referenceParent, "VariableDeclarator") && referenceParent.init === referenceRoot && isNodeOfType(referenceParent.id, "Identifier") && isNodeOfType(aliasDeclaration, "VariableDeclaration") && aliasDeclaration.kind === "const" && isSubscribeBinding(referenceParent.id));
|
|
9209
|
+
});
|
|
9210
|
+
};
|
|
9211
|
+
return isSubscribeBinding(bindingIdentifier);
|
|
9212
|
+
};
|
|
9213
|
+
const doesResourceResultEscape = (resourceNode, allowConciseReturnEscape) => {
|
|
9214
|
+
let currentNode = resourceNode;
|
|
9215
|
+
let parentNode = currentNode.parent;
|
|
9216
|
+
while (parentNode) {
|
|
9217
|
+
if (isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode) return true;
|
|
9218
|
+
if (isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode && allowConciseReturnEscape) return true;
|
|
9219
|
+
if (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
|
|
9220
|
+
currentNode = parentNode;
|
|
9221
|
+
parentNode = currentNode.parent;
|
|
9222
|
+
continue;
|
|
9223
|
+
}
|
|
9224
|
+
return false;
|
|
9225
|
+
}
|
|
8871
9226
|
return false;
|
|
8872
9227
|
};
|
|
8873
|
-
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
9228
|
+
const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
8874
9229
|
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
8875
9230
|
const body = retainedFunction.body;
|
|
8876
9231
|
if (!body) return null;
|
|
8877
|
-
const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
|
|
8878
9232
|
let leak = null;
|
|
9233
|
+
const allowConciseReturnEscape = !isInlineRetainedHandlerFunction(retainedFunction, context);
|
|
9234
|
+
const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
|
|
9235
|
+
const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context);
|
|
8879
9236
|
walkAst(body, (child) => {
|
|
8880
9237
|
if (leak !== null) return false;
|
|
8881
9238
|
if (isFunctionLike$1(child)) return false;
|
|
8882
|
-
if (child
|
|
8883
|
-
|
|
8884
|
-
leak = {
|
|
9239
|
+
if (isSocketConstruction(child) && !doesResourceResultEscape(child, false)) {
|
|
9240
|
+
const socketUsage = {
|
|
8885
9241
|
kind: "socket",
|
|
8886
9242
|
node: child,
|
|
8887
|
-
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
9243
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket",
|
|
9244
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
9245
|
+
receiverKey: null,
|
|
9246
|
+
registrationVerbName: null,
|
|
9247
|
+
eventKey: null,
|
|
9248
|
+
handlerKey: null
|
|
8888
9249
|
};
|
|
8889
|
-
|
|
9250
|
+
if (!hasReleaseForUsage(socketUsage)) {
|
|
9251
|
+
leak = socketUsage;
|
|
9252
|
+
return false;
|
|
9253
|
+
}
|
|
8890
9254
|
}
|
|
8891
9255
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8892
|
-
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" &&
|
|
8893
|
-
|
|
9256
|
+
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && !doesResourceResultEscape(child, allowConciseReturnEscape)) {
|
|
9257
|
+
const timerUsage = {
|
|
8894
9258
|
kind: "timer",
|
|
8895
9259
|
node: child,
|
|
8896
|
-
resourceName: "setInterval"
|
|
9260
|
+
resourceName: "setInterval",
|
|
9261
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
9262
|
+
receiverKey: null,
|
|
9263
|
+
registrationVerbName: "setInterval",
|
|
9264
|
+
eventKey: null,
|
|
9265
|
+
handlerKey: null
|
|
8897
9266
|
};
|
|
8898
|
-
|
|
9267
|
+
if (!hasReleaseForUsage(timerUsage)) {
|
|
9268
|
+
leak = timerUsage;
|
|
9269
|
+
return false;
|
|
9270
|
+
}
|
|
8899
9271
|
}
|
|
8900
|
-
if (isSubscribeOrObserveCall(child) &&
|
|
8901
|
-
const
|
|
8902
|
-
|
|
9272
|
+
if (isSubscribeOrObserveCall(child) && !doesResourceResultEscape(child, allowConciseReturnEscape)) {
|
|
9273
|
+
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
9274
|
+
const subscriptionUsage = {
|
|
8903
9275
|
kind: "subscribe",
|
|
8904
9276
|
node: child,
|
|
8905
|
-
resourceName: registrationVerbName
|
|
9277
|
+
resourceName: registrationDetails.registrationVerbName ?? "subscribe",
|
|
9278
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
9279
|
+
...registrationDetails
|
|
8906
9280
|
};
|
|
9281
|
+
if (!hasSelfReleasingListenerOptions(child, context) && !hasReleaseForUsage(subscriptionUsage)) leak = subscriptionUsage;
|
|
8907
9282
|
return false;
|
|
8908
9283
|
}
|
|
8909
9284
|
});
|
|
@@ -8915,6 +9290,26 @@ const isRetainedComponentScopeFunction = (functionNode) => {
|
|
|
8915
9290
|
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
8916
9291
|
return enclosingComponentOrHookName(functionNode) !== null;
|
|
8917
9292
|
};
|
|
9293
|
+
const isDirectJsxEventHandlerValue = (expression) => {
|
|
9294
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
9295
|
+
const expressionContainer = expressionRoot.parent;
|
|
9296
|
+
return isNodeOfType(expressionContainer, "JSXExpressionContainer") && expressionContainer.expression === expressionRoot && isEventHandlerAttribute(expressionContainer.parent);
|
|
9297
|
+
};
|
|
9298
|
+
const isInlineRetainedHandlerFunction = (functionNode, context) => {
|
|
9299
|
+
if (!isFunctionLike$1(functionNode)) return false;
|
|
9300
|
+
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
9301
|
+
const callbackCall = functionRoot.parent;
|
|
9302
|
+
if (isNodeOfType(callbackCall, "CallExpression") && callbackCall.arguments?.[0] === functionRoot && isHookCall$2(callbackCall, "useCallback") && isDirectJsxEventHandlerValue(callbackCall)) return true;
|
|
9303
|
+
const parentNode = functionNode.parent;
|
|
9304
|
+
if (isDirectJsxEventHandlerValue(functionNode)) return true;
|
|
9305
|
+
if (!isNodeOfType(parentNode, "Property") || parentNode.value !== functionNode || parentNode.computed) return false;
|
|
9306
|
+
const propertyName = getStaticPropertyKeyName(parentNode);
|
|
9307
|
+
if (!propertyName || !/^on[A-Z]/.test(propertyName)) return false;
|
|
9308
|
+
const objectExpression = parentNode.parent;
|
|
9309
|
+
if (!isNodeOfType(objectExpression, "ObjectExpression")) return false;
|
|
9310
|
+
const objectParent = objectExpression.parent;
|
|
9311
|
+
return (isNodeOfType(objectParent, "CallExpression") && objectParent.arguments.some((argument) => argument === objectExpression) || isNodeOfType(objectParent, "JSXExpressionContainer")) && findRenderPhaseComponentOrHook(parentNode, context.scopes) !== null;
|
|
9312
|
+
};
|
|
8918
9313
|
const effectNeedsCleanup = defineRule({
|
|
8919
9314
|
id: "effect-needs-cleanup",
|
|
8920
9315
|
title: "Effect subscription or timer never cleaned up",
|
|
@@ -8923,7 +9318,8 @@ const effectNeedsCleanup = defineRule({
|
|
|
8923
9318
|
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.",
|
|
8924
9319
|
create: (context) => {
|
|
8925
9320
|
const reportRetainedLeak = (retainedFunction) => {
|
|
8926
|
-
|
|
9321
|
+
if (!isPotentiallyReachableFunction(retainedFunction, context)) return;
|
|
9322
|
+
const leak = findRetainedFunctionLeak(retainedFunction, context);
|
|
8927
9323
|
if (!leak) return;
|
|
8928
9324
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
8929
9325
|
context.report({
|
|
@@ -8935,30 +9331,31 @@ const effectNeedsCleanup = defineRule({
|
|
|
8935
9331
|
CallExpression(node) {
|
|
8936
9332
|
if (isHookCall$2(node, "useCallback")) {
|
|
8937
9333
|
const retainedCallback = getEffectCallback(node);
|
|
8938
|
-
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
9334
|
+
if (retainedCallback && !isInlineRetainedHandlerFunction(retainedCallback, context)) reportRetainedLeak(retainedCallback);
|
|
8939
9335
|
return;
|
|
8940
9336
|
}
|
|
8941
|
-
if (!isHookCall$2(node,
|
|
9337
|
+
if (!isHookCall$2(node, CLEANUP_EFFECT_HOOK_NAMES)) return;
|
|
8942
9338
|
const callback = getEffectCallback(node);
|
|
8943
9339
|
if (!callback) return;
|
|
8944
|
-
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
|
|
9340
|
+
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback, context), context);
|
|
8945
9341
|
if (usages.length === 0) return;
|
|
8946
|
-
|
|
8947
|
-
|
|
9342
|
+
const firstUsage = findFirstUsageWithoutCleanup(callback, usages, context);
|
|
9343
|
+
if (!firstUsage) return;
|
|
8948
9344
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
9345
|
+
const hookName = getCalleeName$2(node) ?? "effect";
|
|
8949
9346
|
context.report({
|
|
8950
9347
|
node,
|
|
8951
|
-
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in
|
|
9348
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in ${hookName} without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
8952
9349
|
});
|
|
8953
9350
|
},
|
|
8954
9351
|
FunctionDeclaration(node) {
|
|
8955
9352
|
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8956
9353
|
},
|
|
8957
9354
|
ArrowFunctionExpression(node) {
|
|
8958
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9355
|
+
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
|
|
8959
9356
|
},
|
|
8960
9357
|
FunctionExpression(node) {
|
|
8961
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9358
|
+
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
|
|
8962
9359
|
}
|
|
8963
9360
|
};
|
|
8964
9361
|
}
|
|
@@ -9572,13 +9969,18 @@ const unwrapExpression$3 = (node) => {
|
|
|
9572
9969
|
return current;
|
|
9573
9970
|
};
|
|
9574
9971
|
/**
|
|
9575
|
-
* Get the hook name from a
|
|
9576
|
-
*
|
|
9577
|
-
* `React.useFoo()` (MemberExpression).
|
|
9972
|
+
* Get the hook name from a direct, wrapped, namespaced, or immutable
|
|
9973
|
+
* React import alias call.
|
|
9578
9974
|
*/
|
|
9579
|
-
const getHookName = (callee) => {
|
|
9580
|
-
|
|
9581
|
-
if (isNodeOfType(
|
|
9975
|
+
const getHookName = (callee, scopes) => {
|
|
9976
|
+
const strippedCallee = unwrapExpression$3(callee);
|
|
9977
|
+
if (isNodeOfType(strippedCallee, "Identifier")) {
|
|
9978
|
+
const resolvedSymbol = scopes ? resolveConstIdentifierAlias(strippedCallee, scopes) : null;
|
|
9979
|
+
const importDeclaration = resolvedSymbol?.declarationNode.parent;
|
|
9980
|
+
if (resolvedSymbol?.kind === "import" && importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "react") return getImportedName(resolvedSymbol.declarationNode) ?? strippedCallee.name;
|
|
9981
|
+
return strippedCallee.name;
|
|
9982
|
+
}
|
|
9983
|
+
if (isNodeOfType(strippedCallee, "MemberExpression") && !strippedCallee.computed && isNodeOfType(strippedCallee.property, "Identifier")) return strippedCallee.property.name;
|
|
9582
9984
|
return null;
|
|
9583
9985
|
};
|
|
9584
9986
|
const FUNCTION_SCOPE_KINDS = new Set([
|
|
@@ -9763,7 +10165,7 @@ const STABLE_IDENTITY_WRAPPER_HOOK_NAMES = new Set([
|
|
|
9763
10165
|
* - primitive-literal local consts (the value never changes
|
|
9764
10166
|
* between renders unless the literal does)
|
|
9765
10167
|
*/
|
|
9766
|
-
const symbolHasStableHookOrigin = (symbol) => {
|
|
10168
|
+
const symbolHasStableHookOrigin = (symbol, scopes) => {
|
|
9767
10169
|
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
9768
10170
|
let declarator = symbol.declarationNode;
|
|
9769
10171
|
while (declarator && declarator.type !== "VariableDeclarator") declarator = declarator.parent ?? null;
|
|
@@ -9776,7 +10178,7 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
9776
10178
|
if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
|
|
9777
10179
|
}
|
|
9778
10180
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
9779
|
-
const initializerHookName = getHookName(initializer.callee);
|
|
10181
|
+
const initializerHookName = getHookName(initializer.callee, scopes);
|
|
9780
10182
|
if (!initializerHookName) return false;
|
|
9781
10183
|
if (initializerHookName === "useRef") return true;
|
|
9782
10184
|
if (initializerHookName === "useEffectEvent") return true;
|
|
@@ -9811,7 +10213,7 @@ const symbolHasStableMemoizedOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
9811
10213
|
if (!declarator.init) return false;
|
|
9812
10214
|
const initializer = unwrapExpression$3(declarator.init);
|
|
9813
10215
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
9814
|
-
const initializerHookName = getHookName(initializer.callee);
|
|
10216
|
+
const initializerHookName = getHookName(initializer.callee, scopes);
|
|
9815
10217
|
if (!initializerHookName || !MEMOIZING_HOOK_NAMES.has(initializerHookName)) return false;
|
|
9816
10218
|
const depsArgument = initializer.arguments[1];
|
|
9817
10219
|
if (!depsArgument || !isAstNode(depsArgument)) return false;
|
|
@@ -9841,7 +10243,7 @@ const getObjectPropertyValue = (objectExpression, propertyName) => {
|
|
|
9841
10243
|
}
|
|
9842
10244
|
return null;
|
|
9843
10245
|
};
|
|
9844
|
-
const isStableRefContainerCapture = (symbol, depKey) => {
|
|
10246
|
+
const isStableRefContainerCapture = (symbol, depKey, scopes) => {
|
|
9845
10247
|
if (symbol.kind !== "const") return false;
|
|
9846
10248
|
if (!depKey.startsWith(`${symbol.name}.`)) return false;
|
|
9847
10249
|
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
@@ -9855,7 +10257,7 @@ const isStableRefContainerCapture = (symbol, depKey) => {
|
|
|
9855
10257
|
if (!propertyValue) return false;
|
|
9856
10258
|
currentValue = propertyValue;
|
|
9857
10259
|
}
|
|
9858
|
-
return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee) === "useRef";
|
|
10260
|
+
return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee, scopes) === "useRef";
|
|
9859
10261
|
};
|
|
9860
10262
|
const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
9861
10263
|
if (visitedSymbolIds.has(symbol.id)) return true;
|
|
@@ -9873,7 +10275,13 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
9873
10275
|
}
|
|
9874
10276
|
return true;
|
|
9875
10277
|
};
|
|
9876
|
-
const
|
|
10278
|
+
const symbolHasStableImportedAlias = (symbol, scopes) => {
|
|
10279
|
+
if (symbol.kind !== "const") return false;
|
|
10280
|
+
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
10281
|
+
const resolvedSymbol = resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes);
|
|
10282
|
+
return resolvedSymbol !== null && resolvedSymbol !== symbol && resolvedSymbol.kind === "import";
|
|
10283
|
+
};
|
|
10284
|
+
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol, scopes) || symbolHasStableImportedAlias(symbol, scopes) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
9877
10285
|
//#endregion
|
|
9878
10286
|
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
9879
10287
|
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
@@ -10061,10 +10469,18 @@ const collectCaptureDepKeys = (callback, scopes) => {
|
|
|
10061
10469
|
}
|
|
10062
10470
|
const depKey = computeDepKey(reference);
|
|
10063
10471
|
if (!depKey) continue;
|
|
10064
|
-
if (isStableRefContainerCapture(symbol, depKey)) {
|
|
10472
|
+
if (isStableRefContainerCapture(symbol, depKey, scopes)) {
|
|
10065
10473
|
stableCapturedNames.add(depKey);
|
|
10066
10474
|
continue;
|
|
10067
10475
|
}
|
|
10476
|
+
if (depKey === symbol.name) {
|
|
10477
|
+
const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
10478
|
+
if (identitySourceKeys) {
|
|
10479
|
+
if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
|
|
10480
|
+
for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
|
|
10481
|
+
continue;
|
|
10482
|
+
}
|
|
10483
|
+
}
|
|
10068
10484
|
keys.add(depKey);
|
|
10069
10485
|
}
|
|
10070
10486
|
return {
|
|
@@ -10093,12 +10509,60 @@ const hasComputedMemberExpression = (node) => {
|
|
|
10093
10509
|
if (stripped.computed) return true;
|
|
10094
10510
|
return hasComputedMemberExpression(stripped.object);
|
|
10095
10511
|
};
|
|
10512
|
+
const mergeIdentitySourceKeys = (expressions, scopes, visitedSymbolIds) => {
|
|
10513
|
+
const identitySourceKeys = /* @__PURE__ */ new Set();
|
|
10514
|
+
for (const expression of expressions) {
|
|
10515
|
+
const expressionSourceKeys = resolveIdentitySourceKeysFromExpression(expression, scopes, visitedSymbolIds);
|
|
10516
|
+
if (!expressionSourceKeys) return null;
|
|
10517
|
+
for (const expressionSourceKey of expressionSourceKeys) identitySourceKeys.add(expressionSourceKey);
|
|
10518
|
+
}
|
|
10519
|
+
return identitySourceKeys;
|
|
10520
|
+
};
|
|
10521
|
+
const resolveIdentitySourceKeysFromExpression = (expression, scopes, visitedSymbolIds) => {
|
|
10522
|
+
const stripped = unwrapExpression$3(expression);
|
|
10523
|
+
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();
|
|
10524
|
+
if (isNodeOfType(stripped, "Identifier")) {
|
|
10525
|
+
const sourceSymbol = scopes.symbolFor(stripped);
|
|
10526
|
+
if (!sourceSymbol) return null;
|
|
10527
|
+
if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
10528
|
+
if (sourceSymbol.kind === "const" && sourceSymbol.initializer && isNodeOfType(sourceSymbol.declarationNode, "VariableDeclarator") && sourceSymbol.declarationNode.id === sourceSymbol.bindingIdentifier && sourceSymbol.references.every((reference) => reference.flag === "read")) {
|
|
10529
|
+
if (visitedSymbolIds.has(sourceSymbol.id)) return null;
|
|
10530
|
+
visitedSymbolIds.add(sourceSymbol.id);
|
|
10531
|
+
const sourceKeys = resolveIdentitySourceKeysFromExpression(sourceSymbol.initializer, scopes, visitedSymbolIds);
|
|
10532
|
+
visitedSymbolIds.delete(sourceSymbol.id);
|
|
10533
|
+
if (sourceKeys) return sourceKeys;
|
|
10534
|
+
}
|
|
10535
|
+
return new Set([sourceSymbol.name]);
|
|
10536
|
+
}
|
|
10537
|
+
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
10538
|
+
if (hasComputedMemberExpression(stripped)) return null;
|
|
10539
|
+
const sourceKey = stringifyMemberChain(stripped);
|
|
10540
|
+
const rootIdentifier = getMemberRootIdentifier(stripped);
|
|
10541
|
+
const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
10542
|
+
if (!sourceKey || !rootSymbol) return null;
|
|
10543
|
+
if (isOutsideAllFunctions(rootSymbol)) return /* @__PURE__ */ new Set();
|
|
10544
|
+
if (isStableRefContainerCapture(rootSymbol, sourceKey, scopes)) return /* @__PURE__ */ new Set();
|
|
10545
|
+
if (symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
10546
|
+
return new Set([sourceKey]);
|
|
10547
|
+
}
|
|
10548
|
+
if (isNodeOfType(stripped, "LogicalExpression")) return mergeIdentitySourceKeys([stripped.left, stripped.right], scopes, visitedSymbolIds);
|
|
10549
|
+
if (isNodeOfType(stripped, "ConditionalExpression")) return mergeIdentitySourceKeys([
|
|
10550
|
+
stripped.test,
|
|
10551
|
+
stripped.consequent,
|
|
10552
|
+
stripped.alternate
|
|
10553
|
+
], scopes, visitedSymbolIds);
|
|
10554
|
+
return null;
|
|
10555
|
+
};
|
|
10556
|
+
const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
|
|
10557
|
+
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
|
|
10558
|
+
return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
|
|
10559
|
+
};
|
|
10096
10560
|
const isUseCallbackResultDep = (node, scopes) => {
|
|
10097
10561
|
const rootSymbol = getRootSymbol(node, scopes);
|
|
10098
10562
|
const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
|
|
10099
|
-
return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee) === "useCallback");
|
|
10563
|
+
return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee, scopes) === "useCallback");
|
|
10100
10564
|
};
|
|
10101
|
-
const
|
|
10565
|
+
const isExtraReactiveDepAllowed = (node, scopes) => {
|
|
10102
10566
|
const rootIdentifier = getMemberRootIdentifier(node);
|
|
10103
10567
|
if (!rootIdentifier) return false;
|
|
10104
10568
|
const symbol = scopes.symbolFor(rootIdentifier);
|
|
@@ -10126,12 +10590,78 @@ const isUnstableInitializer = (node) => {
|
|
|
10126
10590
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
10127
10591
|
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");
|
|
10128
10592
|
};
|
|
10593
|
+
const isPotentiallyFreshComparedValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
10594
|
+
const candidate = unwrapExpression$3(node);
|
|
10595
|
+
if (isUnstableInitializer(candidate)) return true;
|
|
10596
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) return isPotentiallyFreshComparedValue(candidate.consequent, scopes, visitedSymbolIds) || isPotentiallyFreshComparedValue(candidate.alternate, scopes, visitedSymbolIds);
|
|
10597
|
+
if (isNodeOfType(candidate, "LogicalExpression")) return isPotentiallyFreshComparedValue(candidate.left, scopes, visitedSymbolIds) || isPotentiallyFreshComparedValue(candidate.right, scopes, visitedSymbolIds);
|
|
10598
|
+
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
10599
|
+
const symbol = scopes.symbolFor(candidate);
|
|
10600
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
10601
|
+
if (symbol.kind === "let" || symbol.kind === "var") return true;
|
|
10602
|
+
if (symbol.kind !== "const" || !symbol.initializer) return false;
|
|
10603
|
+
visitedSymbolIds.add(symbol.id);
|
|
10604
|
+
return isPotentiallyFreshComparedValue(symbol.initializer, scopes, visitedSymbolIds);
|
|
10605
|
+
};
|
|
10606
|
+
const isExtraDepAllowedForHook = (hookName, node, scopes) => {
|
|
10607
|
+
if (!isExtraReactiveDepAllowed(node, scopes)) return false;
|
|
10608
|
+
if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) return true;
|
|
10609
|
+
if (hookName !== "useMemo") return false;
|
|
10610
|
+
const rootSymbol = getRootSymbol(node, scopes);
|
|
10611
|
+
return Boolean(rootSymbol && !symbolHasStableValue(rootSymbol, scopes) && !isUnstableInitializer(rootSymbol.initializer));
|
|
10612
|
+
};
|
|
10129
10613
|
const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
|
|
10130
10614
|
const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
|
|
10131
|
-
const isStableSetterLikeSymbol = (symbol) => {
|
|
10132
|
-
if (!symbolHasStableHookOrigin(symbol)) return false;
|
|
10615
|
+
const isStableSetterLikeSymbol = (symbol, scopes) => {
|
|
10616
|
+
if (!symbolHasStableHookOrigin(symbol, scopes)) return false;
|
|
10133
10617
|
return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
|
|
10134
10618
|
};
|
|
10619
|
+
const isConvergingFunctionalUpdater = (node, scopes) => {
|
|
10620
|
+
const updater = unwrapExpression$3(node);
|
|
10621
|
+
if (!isNodeOfType(updater, "ArrowFunctionExpression") && !isNodeOfType(updater, "FunctionExpression")) return false;
|
|
10622
|
+
const previousValueParameter = updater.params?.[0];
|
|
10623
|
+
if (!previousValueParameter || !isNodeOfType(previousValueParameter, "Identifier")) return false;
|
|
10624
|
+
let returnedExpression = updater.body;
|
|
10625
|
+
if (isNodeOfType(updater.body, "BlockStatement")) {
|
|
10626
|
+
returnedExpression = null;
|
|
10627
|
+
for (const statement of updater.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument) {
|
|
10628
|
+
returnedExpression = statement.argument;
|
|
10629
|
+
break;
|
|
10630
|
+
}
|
|
10631
|
+
}
|
|
10632
|
+
const conditional = returnedExpression ? unwrapExpression$3(returnedExpression) : null;
|
|
10633
|
+
if (!conditional || !isNodeOfType(conditional, "ConditionalExpression")) return false;
|
|
10634
|
+
const test = unwrapExpression$3(conditional.test);
|
|
10635
|
+
if (!isNodeOfType(test, "BinaryExpression") || ![
|
|
10636
|
+
"===",
|
|
10637
|
+
"!==",
|
|
10638
|
+
"==",
|
|
10639
|
+
"!="
|
|
10640
|
+
].includes(test.operator)) return false;
|
|
10641
|
+
const isPreviousValue = (expression) => {
|
|
10642
|
+
const candidate = unwrapExpression$3(expression);
|
|
10643
|
+
return isNodeOfType(candidate, "Identifier") && candidate.name === previousValueParameter.name;
|
|
10644
|
+
};
|
|
10645
|
+
let comparedValue = null;
|
|
10646
|
+
if (isPreviousValue(test.left)) comparedValue = test.right;
|
|
10647
|
+
else if (isPreviousValue(test.right)) comparedValue = test.left;
|
|
10648
|
+
if (!comparedValue) return false;
|
|
10649
|
+
if (isPotentiallyFreshComparedValue(comparedValue, scopes)) return false;
|
|
10650
|
+
const isSameComparedValue = (expression) => {
|
|
10651
|
+
const candidate = unwrapExpression$3(expression);
|
|
10652
|
+
const compared = unwrapExpression$3(comparedValue);
|
|
10653
|
+
if (isNodeOfType(candidate, "Identifier") && isNodeOfType(compared, "Identifier")) return candidate.name === compared.name;
|
|
10654
|
+
if (isNodeOfType(candidate, "Literal") && isNodeOfType(compared, "Literal")) return candidate.value === compared.value;
|
|
10655
|
+
return false;
|
|
10656
|
+
};
|
|
10657
|
+
return test.operator === "===" || test.operator === "==" ? isPreviousValue(conditional.consequent) && isSameComparedValue(conditional.alternate) : isSameComparedValue(conditional.consequent) && isPreviousValue(conditional.alternate);
|
|
10658
|
+
};
|
|
10659
|
+
const isGuardedStableSetterCall = (identifier, symbol, scopes) => {
|
|
10660
|
+
const parent = identifier.parent;
|
|
10661
|
+
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier || !isStableSetterLikeSymbol(symbol, scopes)) return false;
|
|
10662
|
+
const writtenValue = parent.arguments?.[0];
|
|
10663
|
+
return Boolean(writtenValue && isConvergingFunctionalUpdater(writtenValue, scopes));
|
|
10664
|
+
};
|
|
10135
10665
|
const findStableSetterReference = (node, scopes) => {
|
|
10136
10666
|
let setterName = null;
|
|
10137
10667
|
const visit = (current) => {
|
|
@@ -10139,7 +10669,7 @@ const findStableSetterReference = (node, scopes) => {
|
|
|
10139
10669
|
if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
|
|
10140
10670
|
if (isNodeOfType(current, "Identifier")) {
|
|
10141
10671
|
const symbol = scopes.referenceFor(current)?.resolvedSymbol;
|
|
10142
|
-
if (symbol && isStableSetterLikeSymbol(symbol)) {
|
|
10672
|
+
if (symbol && isStableSetterLikeSymbol(symbol, scopes) && !isGuardedStableSetterCall(current, symbol, scopes)) {
|
|
10143
10673
|
setterName = symbol.name;
|
|
10144
10674
|
return;
|
|
10145
10675
|
}
|
|
@@ -10255,11 +10785,11 @@ const hasRefCurrentAssignmentInComponent = (refSymbol) => {
|
|
|
10255
10785
|
}
|
|
10256
10786
|
return false;
|
|
10257
10787
|
};
|
|
10258
|
-
const isSeededDataRefSymbol = (refSymbol) => {
|
|
10788
|
+
const isSeededDataRefSymbol = (refSymbol, scopes) => {
|
|
10259
10789
|
if (!refSymbol) return false;
|
|
10260
10790
|
const initializer = refSymbol.initializer ? unwrapExpression$3(refSymbol.initializer) : null;
|
|
10261
10791
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
10262
|
-
if (getHookName(initializer.callee) !== "useRef") return false;
|
|
10792
|
+
if (getHookName(initializer.callee, scopes) !== "useRef") return false;
|
|
10263
10793
|
const firstArgument = initializer.arguments[0];
|
|
10264
10794
|
if (!firstArgument || !isAstNode(firstArgument)) return false;
|
|
10265
10795
|
const strippedArgument = unwrapExpression$3(firstArgument);
|
|
@@ -10299,7 +10829,9 @@ const isOuterFunctionScopeDep = (node, callback, scopes) => {
|
|
|
10299
10829
|
const componentOrHookScope = componentOrHookFunction ? scopes.ownScopeFor(componentOrHookFunction) : null;
|
|
10300
10830
|
return Boolean(componentOrHookScope && !isDescendantScope(symbol.scope, componentOrHookScope));
|
|
10301
10831
|
};
|
|
10302
|
-
const hasMemberCallForRoot = (node, rootName) => {
|
|
10832
|
+
const hasMemberCallForRoot = (node, rootName, scopes) => {
|
|
10833
|
+
const rootSymbol = closureCaptures(node, scopes).find((reference) => reference.resolvedSymbol?.name === rootName)?.resolvedSymbol ?? null;
|
|
10834
|
+
if (!rootSymbol) return false;
|
|
10303
10835
|
let didFindMemberCall = false;
|
|
10304
10836
|
const visit = (current) => {
|
|
10305
10837
|
if (didFindMemberCall) return;
|
|
@@ -10315,7 +10847,7 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
10315
10847
|
}
|
|
10316
10848
|
chainObject = unwrapExpression$3(chainObject.object);
|
|
10317
10849
|
}
|
|
10318
|
-
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
|
|
10850
|
+
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName && scopes.symbolFor(chainObject) === rootSymbol) {
|
|
10319
10851
|
didFindMemberCall = true;
|
|
10320
10852
|
return;
|
|
10321
10853
|
}
|
|
@@ -10333,9 +10865,11 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
10333
10865
|
visit(node);
|
|
10334
10866
|
return didFindMemberCall;
|
|
10335
10867
|
};
|
|
10336
|
-
const addAggregatePropsDependency = (captureKeys, declaredKeys, callback) => {
|
|
10337
|
-
|
|
10338
|
-
if (
|
|
10868
|
+
const addAggregatePropsDependency = (captureKeys, declaredKeys, callback, scopes) => {
|
|
10869
|
+
const propsCaptureKeys = [...captureKeys].filter((captureKey) => captureKey.startsWith("props."));
|
|
10870
|
+
if (propsCaptureKeys.length < 2 || declaredKeys.has("props")) return;
|
|
10871
|
+
if (propsCaptureKeys.every((captureKey) => [...declaredKeys].some((declaredKey) => isMatchingDepOrPrefix(declaredKey, captureKey)))) return;
|
|
10872
|
+
if (hasMemberCallForRoot(callback, "props", scopes)) captureKeys.add("props");
|
|
10339
10873
|
};
|
|
10340
10874
|
const exhaustiveDeps = defineRule({
|
|
10341
10875
|
id: "exhaustive-deps",
|
|
@@ -10390,7 +10924,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10390
10924
|
};
|
|
10391
10925
|
const isAutoDependenciesHook = (hookName) => settings.experimental_autoDependenciesHooks.includes(hookName);
|
|
10392
10926
|
return { CallExpression(node) {
|
|
10393
|
-
const hookName = getHookName(node.callee);
|
|
10927
|
+
const hookName = getHookName(node.callee, context.scopes);
|
|
10394
10928
|
if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
|
|
10395
10929
|
const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
|
|
10396
10930
|
const depsArgumentIndex = getDepsArgumentIndex(hookName);
|
|
@@ -10447,7 +10981,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10447
10981
|
if (outerAssignments.length > 0) return;
|
|
10448
10982
|
const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
|
|
10449
10983
|
const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
|
|
10450
|
-
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol)) context.report({
|
|
10984
|
+
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol, context.scopes)) context.report({
|
|
10451
10985
|
node: callbackToAnalyze,
|
|
10452
10986
|
message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
|
|
10453
10987
|
});
|
|
@@ -10546,7 +11080,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10546
11080
|
const fullChain = stringifyMemberChain(stripped);
|
|
10547
11081
|
if (fullChain && fullChain.endsWith(".current") && isNodeOfType(stripped, "MemberExpression") && isNodeOfType(stripped.object, "Identifier")) {
|
|
10548
11082
|
const refSymbol = context.scopes.symbolFor(stripped.object);
|
|
10549
|
-
if (refSymbol && symbolHasStableHookOrigin(refSymbol)) {
|
|
11083
|
+
if (refSymbol && symbolHasStableHookOrigin(refSymbol, context.scopes)) {
|
|
10550
11084
|
if (!didReportRefCurrentDep) {
|
|
10551
11085
|
context.report({
|
|
10552
11086
|
node: elementNode,
|
|
@@ -10584,7 +11118,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10584
11118
|
declaredKeys.add(key);
|
|
10585
11119
|
declaredKeyToReportNode.set(key, elementNode);
|
|
10586
11120
|
}
|
|
10587
|
-
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument);
|
|
11121
|
+
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument, context.scopes);
|
|
10588
11122
|
const missingCaptureKeys = [];
|
|
10589
11123
|
for (const captureKey of captureKeys) {
|
|
10590
11124
|
let isCoveredByDeclared = false;
|
|
@@ -10670,7 +11204,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10670
11204
|
if (stableCapturedNames.has(rootName) || stableCapturedNames.has(declaredKey)) continue;
|
|
10671
11205
|
if (outerFunctionCapturedNames.has(rootName)) continue;
|
|
10672
11206
|
const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
|
|
10673
|
-
if (
|
|
11207
|
+
if (isExtraDepAllowedForHook(hookName, reportNode, context.scopes)) continue;
|
|
10674
11208
|
if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
|
|
10675
11209
|
const rootSymbol = getRootSymbol(reportNode, context.scopes);
|
|
10676
11210
|
if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
|
|
@@ -12342,20 +12876,6 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
12342
12876
|
return names;
|
|
12343
12877
|
};
|
|
12344
12878
|
//#endregion
|
|
12345
|
-
//#region src/plugin/utils/get-function-binding-name.ts
|
|
12346
|
-
const getFunctionBindingIdentifier = (functionNode) => {
|
|
12347
|
-
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
12348
|
-
const parent = functionNode.parent;
|
|
12349
|
-
if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
|
|
12350
|
-
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionNode && isNodeOfType(parent.left, "Identifier")) return parent.left;
|
|
12351
|
-
if (isNodeOfType(parent, "CallExpression")) {
|
|
12352
|
-
const callParent = parent.parent;
|
|
12353
|
-
if (isNodeOfType(callParent, "VariableDeclarator") && isNodeOfType(callParent.id, "Identifier")) return callParent.id;
|
|
12354
|
-
}
|
|
12355
|
-
return null;
|
|
12356
|
-
};
|
|
12357
|
-
const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(functionNode)?.name ?? null;
|
|
12358
|
-
//#endregion
|
|
12359
12879
|
//#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
|
|
12360
12880
|
const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
|
|
12361
12881
|
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
@@ -13042,7 +13562,7 @@ const jsCacheStorage = defineRule({
|
|
|
13042
13562
|
CallExpression(node) {
|
|
13043
13563
|
if (!isMemberProperty(node.callee, "getItem")) return;
|
|
13044
13564
|
const receiver = stripParenExpression(node.callee.object);
|
|
13045
|
-
if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS
|
|
13565
|
+
if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS.has(receiver.name)) return;
|
|
13046
13566
|
if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
|
|
13047
13567
|
const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
|
|
13048
13568
|
const storageKey = String(node.arguments[0].value);
|
|
@@ -13309,6 +13829,11 @@ const jsEarlyExit = defineRule({
|
|
|
13309
13829
|
});
|
|
13310
13830
|
//#endregion
|
|
13311
13831
|
//#region src/plugin/rules/js-performance/js-flatmap-filter.ts
|
|
13832
|
+
const BOUNDED_PIPELINE_SOURCE_METHOD_NAMES = new Set(["slice", "split"]);
|
|
13833
|
+
const isBoundedPipelineSource = (node) => {
|
|
13834
|
+
const receiver = stripParenExpression(node);
|
|
13835
|
+
return isNodeOfType(receiver, "CallExpression") && isNodeOfType(receiver.callee, "MemberExpression") && isNodeOfType(receiver.callee.property, "Identifier") && BOUNDED_PIPELINE_SOURCE_METHOD_NAMES.has(receiver.callee.property.name);
|
|
13836
|
+
};
|
|
13312
13837
|
const jsFlatmapFilter = defineRule({
|
|
13313
13838
|
id: "js-flatmap-filter",
|
|
13314
13839
|
title: ".map().filter(Boolean) loops twice",
|
|
@@ -13326,6 +13851,7 @@ const jsFlatmapFilter = defineRule({
|
|
|
13326
13851
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
13327
13852
|
if (innerCall.callee.property.name !== "map") return;
|
|
13328
13853
|
const receiver = stripParenExpression(innerCall.callee.object);
|
|
13854
|
+
if (receiver && isBoundedPipelineSource(receiver)) return;
|
|
13329
13855
|
if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
|
|
13330
13856
|
const elements = receiver.elements ?? [];
|
|
13331
13857
|
if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
|
|
@@ -13500,7 +14026,7 @@ const jsHoistIntl = defineRule({
|
|
|
13500
14026
|
});
|
|
13501
14027
|
//#endregion
|
|
13502
14028
|
//#region src/plugin/utils/create-loop-aware-visitors.ts
|
|
13503
|
-
const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
14029
|
+
const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
|
|
13504
14030
|
"map",
|
|
13505
14031
|
"flatMap",
|
|
13506
14032
|
"forEach",
|
|
@@ -13519,7 +14045,7 @@ const isIteratorCallback = (node) => {
|
|
|
13519
14045
|
const parent = node.parent;
|
|
13520
14046
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
13521
14047
|
if (!parent.arguments.includes(node)) return false;
|
|
13522
|
-
return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(parent.callee.property.name);
|
|
14048
|
+
return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(parent.callee.property.name);
|
|
13523
14049
|
};
|
|
13524
14050
|
const createLoopAwareVisitors = (innerVisitors, options = {}) => {
|
|
13525
14051
|
let loopDepth = 0;
|
|
@@ -13748,7 +14274,7 @@ const findIndexedArrayObject = (callbackBody, indexParameterName) => {
|
|
|
13748
14274
|
});
|
|
13749
14275
|
return indexedArrayObject;
|
|
13750
14276
|
};
|
|
13751
|
-
const unwrapChainExpression$
|
|
14277
|
+
const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
13752
14278
|
const LENGTH_EQUALITY_OPERATORS = new Set(["===", "=="]);
|
|
13753
14279
|
const LENGTH_MISMATCH_OPERATORS = new Set([
|
|
13754
14280
|
"!==",
|
|
@@ -13760,17 +14286,17 @@ const LENGTH_MISMATCH_OPERATORS = new Set([
|
|
|
13760
14286
|
]);
|
|
13761
14287
|
const LENGTH_ANY_COMPARISON_OPERATORS = new Set([...LENGTH_EQUALITY_OPERATORS, ...LENGTH_MISMATCH_OPERATORS]);
|
|
13762
14288
|
const isLengthComparison = (candidate, receiverArray, indexedArray, operators) => {
|
|
13763
|
-
const binaryGuard = unwrapChainExpression$
|
|
14289
|
+
const binaryGuard = unwrapChainExpression$3(candidate);
|
|
13764
14290
|
if (!isNodeOfType(binaryGuard, "BinaryExpression")) return false;
|
|
13765
14291
|
if (!operators.has(binaryGuard.operator)) return false;
|
|
13766
|
-
const leftSide = unwrapChainExpression$
|
|
13767
|
-
const rightSide = unwrapChainExpression$
|
|
14292
|
+
const leftSide = unwrapChainExpression$3(binaryGuard.left);
|
|
14293
|
+
const rightSide = unwrapChainExpression$3(binaryGuard.right);
|
|
13768
14294
|
if (!isMemberProperty(leftSide, "length")) return false;
|
|
13769
14295
|
if (!isMemberProperty(rightSide, "length")) return false;
|
|
13770
|
-
const leftLengthObject = unwrapChainExpression$
|
|
13771
|
-
const rightLengthObject = unwrapChainExpression$
|
|
13772
|
-
const normalizedReceiver = unwrapChainExpression$
|
|
13773
|
-
const normalizedIndexed = unwrapChainExpression$
|
|
14296
|
+
const leftLengthObject = unwrapChainExpression$3(leftSide.object);
|
|
14297
|
+
const rightLengthObject = unwrapChainExpression$3(rightSide.object);
|
|
14298
|
+
const normalizedReceiver = unwrapChainExpression$3(receiverArray);
|
|
14299
|
+
const normalizedIndexed = unwrapChainExpression$3(indexedArray);
|
|
13774
14300
|
const matchesReceiverThenIndexed = areExpressionsStructurallyEqual(leftLengthObject, normalizedReceiver) && areExpressionsStructurallyEqual(rightLengthObject, normalizedIndexed);
|
|
13775
14301
|
const matchesIndexedThenReceiver = areExpressionsStructurallyEqual(leftLengthObject, normalizedIndexed) && areExpressionsStructurallyEqual(rightLengthObject, normalizedReceiver);
|
|
13776
14302
|
return matchesReceiverThenIndexed || matchesIndexedThenReceiver;
|
|
@@ -13857,18 +14383,18 @@ const LENGTH_PRESERVING_METHOD_NAMES = new Set([
|
|
|
13857
14383
|
"toReversed"
|
|
13858
14384
|
]);
|
|
13859
14385
|
const peelLengthPreservingDerivation = (expression) => {
|
|
13860
|
-
let current = unwrapChainExpression$
|
|
14386
|
+
let current = unwrapChainExpression$3(expression);
|
|
13861
14387
|
for (;;) {
|
|
13862
14388
|
if (isNodeOfType(current, "ArrayExpression") && current.elements?.length === 1 && isNodeOfType(current.elements[0], "SpreadElement")) {
|
|
13863
|
-
current = unwrapChainExpression$
|
|
14389
|
+
current = unwrapChainExpression$3(current.elements[0].argument);
|
|
13864
14390
|
continue;
|
|
13865
14391
|
}
|
|
13866
14392
|
if (isNodeOfType(current, "CallExpression")) {
|
|
13867
|
-
const callee = unwrapChainExpression$
|
|
14393
|
+
const callee = unwrapChainExpression$3(current.callee);
|
|
13868
14394
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
13869
|
-
const calleeObject = unwrapChainExpression$
|
|
14395
|
+
const calleeObject = unwrapChainExpression$3(callee.object);
|
|
13870
14396
|
if (isNodeOfType(calleeObject, "Identifier") && calleeObject.name === "Array" && callee.property.name === "from" && current.arguments?.length === 1) {
|
|
13871
|
-
current = unwrapChainExpression$
|
|
14397
|
+
current = unwrapChainExpression$3(current.arguments[0]);
|
|
13872
14398
|
continue;
|
|
13873
14399
|
}
|
|
13874
14400
|
if (LENGTH_PRESERVING_METHOD_NAMES.has(callee.property.name)) {
|
|
@@ -13913,7 +14439,7 @@ const resolveComparedArraySource = (expression, scopeNode) => {
|
|
|
13913
14439
|
const initializer = findConstInitializer(current.name, scopeNode);
|
|
13914
14440
|
if (!initializer) return current;
|
|
13915
14441
|
const peeledInitializer = peelLengthPreservingDerivation(initializer);
|
|
13916
|
-
if (peeledInitializer === unwrapChainExpression$
|
|
14442
|
+
if (peeledInitializer === unwrapChainExpression$3(initializer)) return current;
|
|
13917
14443
|
current = peeledInitializer;
|
|
13918
14444
|
}
|
|
13919
14445
|
return current;
|
|
@@ -19643,54 +20169,6 @@ const nextjsNoAElement = defineRule({
|
|
|
19643
20169
|
} })
|
|
19644
20170
|
});
|
|
19645
20171
|
//#endregion
|
|
19646
|
-
//#region src/plugin/utils/collect-effect-invoked-functions.ts
|
|
19647
|
-
const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
|
|
19648
|
-
"then",
|
|
19649
|
-
"catch",
|
|
19650
|
-
"finally"
|
|
19651
|
-
]);
|
|
19652
|
-
const collectEffectInvokedFunctions = (effectCallback) => {
|
|
19653
|
-
const invokedFunctions = new Set([effectCallback]);
|
|
19654
|
-
const localFunctionBindings = /* @__PURE__ */ new Map();
|
|
19655
|
-
const calledBindingNames = /* @__PURE__ */ new Set();
|
|
19656
|
-
const pendingFunctions = [effectCallback];
|
|
19657
|
-
const enqueue = (candidate) => {
|
|
19658
|
-
const strippedCandidate = candidate ? stripParenExpression(candidate) : candidate;
|
|
19659
|
-
if (!isFunctionLike$1(strippedCandidate) || invokedFunctions.has(strippedCandidate)) return;
|
|
19660
|
-
invokedFunctions.add(strippedCandidate);
|
|
19661
|
-
pendingFunctions.push(strippedCandidate);
|
|
19662
|
-
};
|
|
19663
|
-
const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
|
|
19664
|
-
while (pendingFunctions.length > 0) {
|
|
19665
|
-
const currentFunction = pendingFunctions.pop();
|
|
19666
|
-
if (!currentFunction) break;
|
|
19667
|
-
walkAst(currentFunction, (child) => {
|
|
19668
|
-
if (child !== currentFunction && isFunctionLike$1(child)) {
|
|
19669
|
-
if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) localFunctionBindings.set(child.id.name, child);
|
|
19670
|
-
return false;
|
|
19671
|
-
}
|
|
19672
|
-
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
19673
|
-
const initializer = child.init ? stripParenExpression(child.init) : null;
|
|
19674
|
-
if (isFunctionLike$1(initializer)) localFunctionBindings.set(child.id.name, initializer);
|
|
19675
|
-
return;
|
|
19676
|
-
}
|
|
19677
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
19678
|
-
const callee = stripParenExpression(child.callee);
|
|
19679
|
-
if (isFunctionLike$1(callee)) {
|
|
19680
|
-
enqueue(callee);
|
|
19681
|
-
return;
|
|
19682
|
-
}
|
|
19683
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
19684
|
-
calledBindingNames.add(callee.name);
|
|
19685
|
-
return;
|
|
19686
|
-
}
|
|
19687
|
-
if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
19688
|
-
});
|
|
19689
|
-
for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
|
|
19690
|
-
}
|
|
19691
|
-
return invokedFunctions;
|
|
19692
|
-
};
|
|
19693
|
-
//#endregion
|
|
19694
20172
|
//#region src/plugin/utils/contains-fetch-call.ts
|
|
19695
20173
|
const isFetchCall$1 = (node) => {
|
|
19696
20174
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
@@ -20674,6 +21152,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
20674
21152
|
continue;
|
|
20675
21153
|
}
|
|
20676
21154
|
if (isNodeOfType(statement, "ExportNamedDeclaration")) {
|
|
21155
|
+
if (statement.exportKind === "type") continue;
|
|
20677
21156
|
const declaration = statement.declaration;
|
|
20678
21157
|
if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
|
|
20679
21158
|
recordVariableDeclaration(declaration);
|
|
@@ -20688,6 +21167,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
20688
21167
|
}
|
|
20689
21168
|
for (const specifier of statement.specifiers ?? []) {
|
|
20690
21169
|
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
21170
|
+
if (specifier.exportKind === "type") continue;
|
|
20691
21171
|
const local = specifier.local;
|
|
20692
21172
|
const exported = specifier.exported;
|
|
20693
21173
|
if (!isNodeOfType(local, "Identifier")) continue;
|
|
@@ -20736,25 +21216,37 @@ const resolveImportedExportName = (importSpecifier) => {
|
|
|
20736
21216
|
if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
|
|
20737
21217
|
return null;
|
|
20738
21218
|
};
|
|
20739
|
-
const
|
|
21219
|
+
const findReExportTargetsForName = (programRoot, exportedName) => {
|
|
20740
21220
|
if (!isNodeOfType(programRoot, "Program")) return [];
|
|
20741
|
-
const
|
|
21221
|
+
const exportAllTargets = [];
|
|
20742
21222
|
for (const statement of programRoot.body ?? []) {
|
|
20743
21223
|
if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
|
|
21224
|
+
if (statement.exportKind === "type") continue;
|
|
20744
21225
|
const sourceValue = statement.source.value;
|
|
20745
21226
|
if (typeof sourceValue !== "string") continue;
|
|
20746
21227
|
for (const specifier of statement.specifiers ?? []) {
|
|
20747
21228
|
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
21229
|
+
if (specifier.exportKind === "type") continue;
|
|
20748
21230
|
const exported = specifier.exported;
|
|
20749
|
-
if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null)
|
|
21231
|
+
if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) !== exportedName) continue;
|
|
21232
|
+
const local = specifier.local;
|
|
21233
|
+
const importedName = isNodeOfType(local, "Identifier") ? local.name : isNodeOfType(local, "Literal") && typeof local.value === "string" ? local.value : null;
|
|
21234
|
+
if (importedName) return [{
|
|
21235
|
+
importedName,
|
|
21236
|
+
source: sourceValue
|
|
21237
|
+
}];
|
|
20750
21238
|
}
|
|
20751
21239
|
}
|
|
20752
21240
|
if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
|
|
21241
|
+
if (statement.exportKind === "type" || statement.exported) continue;
|
|
20753
21242
|
const sourceValue = statement.source.value;
|
|
20754
|
-
if (typeof sourceValue === "string")
|
|
21243
|
+
if (typeof sourceValue === "string") exportAllTargets.push({
|
|
21244
|
+
importedName: exportedName,
|
|
21245
|
+
source: sourceValue
|
|
21246
|
+
});
|
|
20755
21247
|
}
|
|
20756
21248
|
}
|
|
20757
|
-
return
|
|
21249
|
+
return exportAllTargets;
|
|
20758
21250
|
};
|
|
20759
21251
|
//#endregion
|
|
20760
21252
|
//#region src/plugin/utils/attach-parent-references.ts
|
|
@@ -20843,139 +21335,6 @@ const parseSourceFile = (absoluteFilePath) => {
|
|
|
20843
21335
|
return parsedProgram;
|
|
20844
21336
|
};
|
|
20845
21337
|
//#endregion
|
|
20846
|
-
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
20847
|
-
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
20848
|
-
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
20849
|
-
const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
20850
|
-
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
20851
|
-
const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
|
|
20852
|
-
const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
|
|
20853
|
-
const createNonBarrelInfo = () => ({
|
|
20854
|
-
isBarrel: false,
|
|
20855
|
-
exportsByName: /* @__PURE__ */ new Map(),
|
|
20856
|
-
starExportSources: []
|
|
20857
|
-
});
|
|
20858
|
-
const addImportedBinding = (importedBindings, binding) => {
|
|
20859
|
-
importedBindings.set(binding.localName, {
|
|
20860
|
-
...binding,
|
|
20861
|
-
didExport: false
|
|
20862
|
-
});
|
|
20863
|
-
};
|
|
20864
|
-
const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
|
|
20865
|
-
for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
|
|
20866
|
-
localName: specifier.exportedName,
|
|
20867
|
-
importedName: specifier.localName,
|
|
20868
|
-
source,
|
|
20869
|
-
isTypeOnly: specifier.isTypeOnly
|
|
20870
|
-
});
|
|
20871
|
-
};
|
|
20872
|
-
const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
|
|
20873
|
-
const trimmedImportClause = importClause.trim();
|
|
20874
|
-
const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
|
|
20875
|
-
if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
|
|
20876
|
-
localName: namespaceMatch[1],
|
|
20877
|
-
importedName: "*",
|
|
20878
|
-
source,
|
|
20879
|
-
isTypeOnly: declarationIsTypeOnly
|
|
20880
|
-
});
|
|
20881
|
-
const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
|
|
20882
|
-
if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
|
|
20883
|
-
const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
|
|
20884
|
-
if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
|
|
20885
|
-
localName: defaultImportName,
|
|
20886
|
-
importedName: "default",
|
|
20887
|
-
source,
|
|
20888
|
-
isTypeOnly: declarationIsTypeOnly
|
|
20889
|
-
});
|
|
20890
|
-
};
|
|
20891
|
-
const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
|
|
20892
|
-
let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
|
|
20893
|
-
collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
|
|
20894
|
-
return "";
|
|
20895
|
-
});
|
|
20896
|
-
withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
|
|
20897
|
-
const isTypeOnly = Boolean(typeKeyword);
|
|
20898
|
-
if (namespaceExportName) {
|
|
20899
|
-
exportsByName.set(namespaceExportName, {
|
|
20900
|
-
exportedName: namespaceExportName,
|
|
20901
|
-
importedName: "*",
|
|
20902
|
-
source,
|
|
20903
|
-
isTypeOnly
|
|
20904
|
-
});
|
|
20905
|
-
return "";
|
|
20906
|
-
}
|
|
20907
|
-
if (specifiersText) {
|
|
20908
|
-
for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
|
|
20909
|
-
exportedName: specifier.exportedName,
|
|
20910
|
-
importedName: specifier.localName,
|
|
20911
|
-
source,
|
|
20912
|
-
isTypeOnly: specifier.isTypeOnly
|
|
20913
|
-
});
|
|
20914
|
-
return "";
|
|
20915
|
-
}
|
|
20916
|
-
starExportSources.push(source);
|
|
20917
|
-
return "";
|
|
20918
|
-
});
|
|
20919
|
-
withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
|
|
20920
|
-
for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
|
|
20921
|
-
const importedBinding = importedBindings.get(specifier.localName);
|
|
20922
|
-
if (!importedBinding) return _match;
|
|
20923
|
-
importedBinding.didExport = true;
|
|
20924
|
-
exportsByName.set(specifier.exportedName, {
|
|
20925
|
-
exportedName: specifier.exportedName,
|
|
20926
|
-
importedName: importedBinding.importedName,
|
|
20927
|
-
source: importedBinding.source,
|
|
20928
|
-
isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
|
|
20929
|
-
});
|
|
20930
|
-
}
|
|
20931
|
-
return "";
|
|
20932
|
-
});
|
|
20933
|
-
return withoutKnownDeclarations;
|
|
20934
|
-
};
|
|
20935
|
-
const hasUnexportedRuntimeImport = (importedBindings) => {
|
|
20936
|
-
for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
|
|
20937
|
-
return false;
|
|
20938
|
-
};
|
|
20939
|
-
const classifyBarrelModule = (sourceText) => {
|
|
20940
|
-
const strippedSource = stripJsComments(sourceText).trim();
|
|
20941
|
-
if (!strippedSource) return createNonBarrelInfo();
|
|
20942
|
-
const importedBindings = /* @__PURE__ */ new Map();
|
|
20943
|
-
const exportsByName = /* @__PURE__ */ new Map();
|
|
20944
|
-
const starExportSources = [];
|
|
20945
|
-
if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
|
|
20946
|
-
return {
|
|
20947
|
-
isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
|
|
20948
|
-
exportsByName,
|
|
20949
|
-
starExportSources
|
|
20950
|
-
};
|
|
20951
|
-
};
|
|
20952
|
-
const getBarrelIndexModuleInfo = (filePath) => {
|
|
20953
|
-
if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
|
|
20954
|
-
recordContentProbe(filePath);
|
|
20955
|
-
let fileStat;
|
|
20956
|
-
try {
|
|
20957
|
-
fileStat = fs.statSync(filePath);
|
|
20958
|
-
} catch {
|
|
20959
|
-
fileStat = null;
|
|
20960
|
-
}
|
|
20961
|
-
const cachedResult = barrelIndexModuleInfoCache.get(filePath);
|
|
20962
|
-
if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
|
|
20963
|
-
if (fileStat === null) return createNonBarrelInfo();
|
|
20964
|
-
let moduleInfo = createNonBarrelInfo();
|
|
20965
|
-
try {
|
|
20966
|
-
moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
|
|
20967
|
-
} catch {
|
|
20968
|
-
moduleInfo = createNonBarrelInfo();
|
|
20969
|
-
}
|
|
20970
|
-
barrelIndexModuleInfoCache.set(filePath, {
|
|
20971
|
-
mtimeMs: fileStat.mtimeMs,
|
|
20972
|
-
size: fileStat.size,
|
|
20973
|
-
moduleInfo
|
|
20974
|
-
});
|
|
20975
|
-
return moduleInfo;
|
|
20976
|
-
};
|
|
20977
|
-
const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
|
|
20978
|
-
//#endregion
|
|
20979
21338
|
//#region src/plugin/utils/resolve-relative-import-path.ts
|
|
20980
21339
|
const MODULE_FILE_EXTENSIONS = [
|
|
20981
21340
|
".ts",
|
|
@@ -21092,35 +21451,6 @@ const resolveModuleFileFromAbsolutePath = (importPath) => {
|
|
|
21092
21451
|
};
|
|
21093
21452
|
const resolveRelativeImportPath = (filename, source) => resolveModuleFileFromAbsolutePath(path.resolve(path.dirname(filename), source));
|
|
21094
21453
|
//#endregion
|
|
21095
|
-
//#region src/plugin/utils/resolve-barrel-export-file-path.ts
|
|
21096
|
-
const getUniqueFilePath = (filePaths) => {
|
|
21097
|
-
const uniqueFilePaths = new Set(filePaths);
|
|
21098
|
-
if (uniqueFilePaths.size !== 1) return null;
|
|
21099
|
-
const [filePath] = uniqueFilePaths;
|
|
21100
|
-
return filePath ?? null;
|
|
21101
|
-
};
|
|
21102
|
-
const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
|
|
21103
|
-
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
|
|
21104
|
-
if (!resolvedTargetPath) return null;
|
|
21105
|
-
const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
|
|
21106
|
-
if (nestedTargetPath) return nestedTargetPath;
|
|
21107
|
-
return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
|
|
21108
|
-
};
|
|
21109
|
-
const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
|
|
21110
|
-
if (visitedFilePaths.has(barrelFilePath)) return null;
|
|
21111
|
-
visitedFilePaths.add(barrelFilePath);
|
|
21112
|
-
const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
|
|
21113
|
-
if (!moduleInfo.isBarrel) return null;
|
|
21114
|
-
const target = moduleInfo.exportsByName.get(exportedName);
|
|
21115
|
-
if (target) {
|
|
21116
|
-
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
|
|
21117
|
-
if (!resolvedTargetPath) return null;
|
|
21118
|
-
return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
|
|
21119
|
-
}
|
|
21120
|
-
if (exportedName === "default") return null;
|
|
21121
|
-
return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
|
|
21122
|
-
};
|
|
21123
|
-
//#endregion
|
|
21124
21454
|
//#region src/plugin/utils/resolve-tsconfig-alias.ts
|
|
21125
21455
|
const TSCONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
|
|
21126
21456
|
const isObjectRecord = (value) => typeof value === "object" && value !== null;
|
|
@@ -21299,25 +21629,29 @@ const resolveTsconfigAliasPath = (fromFilename, source) => {
|
|
|
21299
21629
|
};
|
|
21300
21630
|
//#endregion
|
|
21301
21631
|
//#region src/plugin/utils/resolve-module-path.ts
|
|
21302
|
-
const resolveModulePath = (fromFilename, source) =>
|
|
21632
|
+
const resolveModulePath = (fromFilename, source) => {
|
|
21633
|
+
if (path.isAbsolute(source)) return null;
|
|
21634
|
+
return source.startsWith(".") ? resolveRelativeImportPath(fromFilename, source) : resolveTsconfigAliasPath(fromFilename, source);
|
|
21635
|
+
};
|
|
21303
21636
|
//#endregion
|
|
21304
21637
|
//#region src/plugin/utils/resolve-cross-file-function-export.ts
|
|
21305
21638
|
const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
|
|
21306
21639
|
if (visitedFilePaths.size >= 4) return null;
|
|
21307
21640
|
if (visitedFilePaths.has(filePath)) return null;
|
|
21308
21641
|
visitedFilePaths.add(filePath);
|
|
21309
|
-
const
|
|
21310
|
-
const programRoot = parseSourceFile(actualFilePath);
|
|
21642
|
+
const programRoot = parseSourceFile(filePath);
|
|
21311
21643
|
if (!programRoot) return null;
|
|
21312
21644
|
const exported = findExportedFunctionBody(programRoot, exportedName);
|
|
21313
21645
|
if (exported) return exported;
|
|
21314
|
-
|
|
21315
|
-
|
|
21646
|
+
const resolvedCandidates = /* @__PURE__ */ new Set();
|
|
21647
|
+
for (const target of findReExportTargetsForName(programRoot, exportedName)) {
|
|
21648
|
+
const nextFilePath = resolveModulePath(filePath, target.source);
|
|
21316
21649
|
if (!nextFilePath) continue;
|
|
21317
|
-
const resolved = resolveFunctionExportInFile(nextFilePath,
|
|
21318
|
-
if (resolved)
|
|
21650
|
+
const resolved = resolveFunctionExportInFile(nextFilePath, target.importedName, new Set(visitedFilePaths));
|
|
21651
|
+
if (resolved) resolvedCandidates.add(resolved);
|
|
21319
21652
|
}
|
|
21320
|
-
return null;
|
|
21653
|
+
if (resolvedCandidates.size !== 1) return null;
|
|
21654
|
+
return resolvedCandidates.values().next().value ?? null;
|
|
21321
21655
|
};
|
|
21322
21656
|
const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
|
|
21323
21657
|
const resolvedFilePath = resolveModulePath(fromFilename, source);
|
|
@@ -22102,6 +22436,26 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
22102
22436
|
const importDeclaration = declarationNode.parent;
|
|
22103
22437
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22104
22438
|
}));
|
|
22439
|
+
const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.some((def) => {
|
|
22440
|
+
if (def.type !== "ImportBinding") return false;
|
|
22441
|
+
const declarationNode = def.node;
|
|
22442
|
+
if (!isNodeOfType(declarationNode, "ImportNamespaceSpecifier") && !isNodeOfType(declarationNode, "ImportDefaultSpecifier")) return false;
|
|
22443
|
+
const importDeclaration = declarationNode.parent;
|
|
22444
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22445
|
+
}));
|
|
22446
|
+
const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
22447
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
|
|
22448
|
+
const callee = stripParenExpression(declarator.init.callee);
|
|
22449
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
22450
|
+
const reference = getRef(analysis, callee);
|
|
22451
|
+
if (!reference?.resolved) return callee.name === hookName;
|
|
22452
|
+
return isReactNamedImportReference(reference, hookName);
|
|
22453
|
+
}
|
|
22454
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
|
|
22455
|
+
const namespaceReference = getRef(analysis, callee.object);
|
|
22456
|
+
if (!namespaceReference?.resolved) return callee.object.name === "React";
|
|
22457
|
+
return isReactNamespaceImportReference(namespaceReference);
|
|
22458
|
+
};
|
|
22105
22459
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
22106
22460
|
if (!node) return false;
|
|
22107
22461
|
if (isNodeOfType(node, "Identifier")) {
|
|
@@ -22500,6 +22854,46 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
|
|
|
22500
22854
|
"parseInt"
|
|
22501
22855
|
]);
|
|
22502
22856
|
const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
|
|
22857
|
+
const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
|
|
22858
|
+
"isRawJSON",
|
|
22859
|
+
"parse",
|
|
22860
|
+
"rawJSON",
|
|
22861
|
+
"stringify"
|
|
22862
|
+
])], ["Math", new Set([
|
|
22863
|
+
"abs",
|
|
22864
|
+
"acos",
|
|
22865
|
+
"acosh",
|
|
22866
|
+
"asin",
|
|
22867
|
+
"asinh",
|
|
22868
|
+
"atan",
|
|
22869
|
+
"atan2",
|
|
22870
|
+
"atanh",
|
|
22871
|
+
"cbrt",
|
|
22872
|
+
"ceil",
|
|
22873
|
+
"clz32",
|
|
22874
|
+
"cos",
|
|
22875
|
+
"cosh",
|
|
22876
|
+
"exp",
|
|
22877
|
+
"floor",
|
|
22878
|
+
"fround",
|
|
22879
|
+
"hypot",
|
|
22880
|
+
"imul",
|
|
22881
|
+
"log",
|
|
22882
|
+
"log10",
|
|
22883
|
+
"log1p",
|
|
22884
|
+
"log2",
|
|
22885
|
+
"max",
|
|
22886
|
+
"min",
|
|
22887
|
+
"pow",
|
|
22888
|
+
"round",
|
|
22889
|
+
"sign",
|
|
22890
|
+
"sin",
|
|
22891
|
+
"sinh",
|
|
22892
|
+
"sqrt",
|
|
22893
|
+
"tan",
|
|
22894
|
+
"tanh",
|
|
22895
|
+
"trunc"
|
|
22896
|
+
])]]);
|
|
22503
22897
|
const PURE_MEMBER_TRANSFORM_NAMES = new Set([
|
|
22504
22898
|
"concat",
|
|
22505
22899
|
"filter",
|
|
@@ -22546,6 +22940,42 @@ const getParameterBindingIdentity = (analysis, functionNode, parameter) => {
|
|
|
22546
22940
|
}
|
|
22547
22941
|
return parameter;
|
|
22548
22942
|
};
|
|
22943
|
+
const isAsyncOrGeneratorFunction = (functionNode) => Boolean(functionNode.async === true || functionNode.generator === true);
|
|
22944
|
+
const isModuleFunction = (functionNode) => {
|
|
22945
|
+
let ancestor = functionNode.parent;
|
|
22946
|
+
while (ancestor) {
|
|
22947
|
+
if (isFunctionLike$1(ancestor)) return false;
|
|
22948
|
+
if (isNodeOfType(ancestor, "Program")) return true;
|
|
22949
|
+
ancestor = ancestor.parent;
|
|
22950
|
+
}
|
|
22951
|
+
return false;
|
|
22952
|
+
};
|
|
22953
|
+
const getFunctionBindingNames = (functionNode) => {
|
|
22954
|
+
const names = /* @__PURE__ */ new Set();
|
|
22955
|
+
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) names.add(functionNode.id.name);
|
|
22956
|
+
const parent = functionNode.parent;
|
|
22957
|
+
if (parent && isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) names.add(parent.id.name);
|
|
22958
|
+
return names;
|
|
22959
|
+
};
|
|
22960
|
+
const collectModuleBindingNames = (functionNode) => {
|
|
22961
|
+
let program = functionNode.parent;
|
|
22962
|
+
while (program && !isNodeOfType(program, "Program")) program = program.parent;
|
|
22963
|
+
const bindingNames = /* @__PURE__ */ new Set();
|
|
22964
|
+
if (!program || !isNodeOfType(program, "Program")) return bindingNames;
|
|
22965
|
+
for (const statement of program.body ?? []) {
|
|
22966
|
+
if (isNodeOfType(statement, "ImportDeclaration")) {
|
|
22967
|
+
for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier.local, "Identifier")) bindingNames.add(specifier.local.name);
|
|
22968
|
+
continue;
|
|
22969
|
+
}
|
|
22970
|
+
const declaration = isNodeOfType(statement, "ExportNamedDeclaration") || isNodeOfType(statement, "ExportDefaultDeclaration") ? statement.declaration : statement;
|
|
22971
|
+
if (isNodeOfType(declaration, "VariableDeclaration")) {
|
|
22972
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, bindingNames);
|
|
22973
|
+
continue;
|
|
22974
|
+
}
|
|
22975
|
+
if ((isNodeOfType(declaration, "FunctionDeclaration") || isNodeOfType(declaration, "ClassDeclaration")) && isNodeOfType(declaration.id, "Identifier")) bindingNames.add(declaration.id.name);
|
|
22976
|
+
}
|
|
22977
|
+
return bindingNames;
|
|
22978
|
+
};
|
|
22549
22979
|
const buildSubstitutions = (analysis, functionNode, argumentExpressions, parentFrame) => {
|
|
22550
22980
|
const substitutions = /* @__PURE__ */ new Map();
|
|
22551
22981
|
const parameters = getFunctionParameters(functionNode);
|
|
@@ -22676,7 +23106,7 @@ const collectIntroducedBindings = (analysis, functionNode) => {
|
|
|
22676
23106
|
for (const parameter of getFunctionParameters(functionNode)) if (isNodeOfType(parameter, "Identifier")) introducedBindings.add(getParameterBindingIdentity(analysis, functionNode, parameter));
|
|
22677
23107
|
return introducedBindings;
|
|
22678
23108
|
};
|
|
22679
|
-
const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
|
|
23109
|
+
const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilename) => {
|
|
22680
23110
|
const effectFunction = getEffectFn(analysis, effectNode);
|
|
22681
23111
|
if (!effectFunction || !isFunctionLike$1(effectFunction) || effectFunction.async === true) return [];
|
|
22682
23112
|
const invokedFunctionEvidence = collectEffectInvokedFunctions(effectFunction);
|
|
@@ -22685,7 +23115,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
|
|
|
22685
23115
|
invocation: null,
|
|
22686
23116
|
isDeferred: false,
|
|
22687
23117
|
introducedBindings: /* @__PURE__ */ new Set(),
|
|
22688
|
-
substitutions: /* @__PURE__ */ new Map()
|
|
23118
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
23119
|
+
currentFilename
|
|
22689
23120
|
};
|
|
22690
23121
|
const frames = [rootFrame];
|
|
22691
23122
|
walkAst(effectFunction, (child) => {
|
|
@@ -22706,7 +23137,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
|
|
|
22706
23137
|
invocation: child,
|
|
22707
23138
|
isDeferred,
|
|
22708
23139
|
introducedBindings,
|
|
22709
|
-
substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame)
|
|
23140
|
+
substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame),
|
|
23141
|
+
currentFilename
|
|
22710
23142
|
});
|
|
22711
23143
|
};
|
|
22712
23144
|
if (isFunctionLike$1(callee)) {
|
|
@@ -22759,6 +23191,174 @@ const isOpaqueHookCall = (callExpression) => {
|
|
|
22759
23191
|
const calleeName = getCallCalleeName$1(callExpression);
|
|
22760
23192
|
return Boolean(calleeName && /^use[A-Z0-9]/.test(calleeName) && calleeName !== "useMemo" && calleeName !== "useCallback" && calleeName !== "useEffectEvent");
|
|
22761
23193
|
};
|
|
23194
|
+
const analyzeHelperStatements = (statements, environment, usedParameterIndices, analyzeExpression) => {
|
|
23195
|
+
let canContinue = true;
|
|
23196
|
+
for (const statement of statements) {
|
|
23197
|
+
if (!canContinue) {
|
|
23198
|
+
if (!isNodeOfType(statement, "EmptyStatement")) return {
|
|
23199
|
+
canContinue: false,
|
|
23200
|
+
isValid: false
|
|
23201
|
+
};
|
|
23202
|
+
continue;
|
|
23203
|
+
}
|
|
23204
|
+
if (isNodeOfType(statement, "EmptyStatement")) continue;
|
|
23205
|
+
if (isNodeOfType(statement, "ReturnStatement")) {
|
|
23206
|
+
if (!statement.argument || !analyzeExpression(statement.argument, environment, usedParameterIndices)) return {
|
|
23207
|
+
canContinue: false,
|
|
23208
|
+
isValid: false
|
|
23209
|
+
};
|
|
23210
|
+
canContinue = false;
|
|
23211
|
+
continue;
|
|
23212
|
+
}
|
|
23213
|
+
if (isNodeOfType(statement, "BlockStatement")) {
|
|
23214
|
+
const blockSummary = analyzeHelperStatements(statement.body ?? [], environment, usedParameterIndices, analyzeExpression);
|
|
23215
|
+
if (!blockSummary.isValid) return blockSummary;
|
|
23216
|
+
canContinue = blockSummary.canContinue;
|
|
23217
|
+
continue;
|
|
23218
|
+
}
|
|
23219
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
23220
|
+
if (!analyzeExpression(statement.test, environment, usedParameterIndices)) return {
|
|
23221
|
+
canContinue: false,
|
|
23222
|
+
isValid: false
|
|
23223
|
+
};
|
|
23224
|
+
const consequentSummary = analyzeHelperStatements([statement.consequent], environment, usedParameterIndices, analyzeExpression);
|
|
23225
|
+
if (!consequentSummary.isValid) return consequentSummary;
|
|
23226
|
+
const alternateSummary = statement.alternate ? analyzeHelperStatements([statement.alternate], environment, usedParameterIndices, analyzeExpression) : {
|
|
23227
|
+
canContinue: true,
|
|
23228
|
+
isValid: true
|
|
23229
|
+
};
|
|
23230
|
+
if (!alternateSummary.isValid) return alternateSummary;
|
|
23231
|
+
canContinue = consequentSummary.canContinue || alternateSummary.canContinue;
|
|
23232
|
+
continue;
|
|
23233
|
+
}
|
|
23234
|
+
return {
|
|
23235
|
+
canContinue: false,
|
|
23236
|
+
isValid: false
|
|
23237
|
+
};
|
|
23238
|
+
}
|
|
23239
|
+
return {
|
|
23240
|
+
canContinue,
|
|
23241
|
+
isValid: true
|
|
23242
|
+
};
|
|
23243
|
+
};
|
|
23244
|
+
const analyzeHelperExpression = (expression, environment, usedParameterIndices) => {
|
|
23245
|
+
const node = stripParenExpression(expression);
|
|
23246
|
+
if (isNodeOfType(node, "Literal") || isNodeOfType(node, "TemplateElement")) return true;
|
|
23247
|
+
if (isNodeOfType(node, "Identifier")) {
|
|
23248
|
+
const parameterIndex = environment.parameterIndices.get(node.name);
|
|
23249
|
+
if (parameterIndex !== void 0) {
|
|
23250
|
+
if (parameterIndex !== null) usedParameterIndices.add(parameterIndex);
|
|
23251
|
+
return true;
|
|
23252
|
+
}
|
|
23253
|
+
return node.name === "undefined" || node.name === "NaN" || node.name === "Infinity";
|
|
23254
|
+
}
|
|
23255
|
+
if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => !element || analyzeHelperExpression(element, environment, usedParameterIndices));
|
|
23256
|
+
if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).every((property) => {
|
|
23257
|
+
if (isNodeOfType(property, "SpreadElement")) return analyzeHelperExpression(property.argument, environment, usedParameterIndices);
|
|
23258
|
+
if (!isNodeOfType(property, "Property") || property.kind !== "init" || property.method === true) return false;
|
|
23259
|
+
if (property.computed && !analyzeHelperExpression(property.key, environment, usedParameterIndices)) return false;
|
|
23260
|
+
return analyzeHelperExpression(property.value, environment, usedParameterIndices);
|
|
23261
|
+
});
|
|
23262
|
+
if (isNodeOfType(node, "TemplateLiteral")) return (node.expressions ?? []).every((templateExpression) => analyzeHelperExpression(templateExpression, environment, usedParameterIndices));
|
|
23263
|
+
if (isNodeOfType(node, "UnaryExpression")) return node.operator !== "delete" && analyzeHelperExpression(node.argument, environment, usedParameterIndices);
|
|
23264
|
+
if (isNodeOfType(node, "BinaryExpression") || isNodeOfType(node, "LogicalExpression")) return analyzeHelperExpression(node.left, environment, usedParameterIndices) && analyzeHelperExpression(node.right, environment, usedParameterIndices);
|
|
23265
|
+
if (isNodeOfType(node, "ConditionalExpression")) return analyzeHelperExpression(node.test, environment, usedParameterIndices) && analyzeHelperExpression(node.consequent, environment, usedParameterIndices) && analyzeHelperExpression(node.alternate, environment, usedParameterIndices);
|
|
23266
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
23267
|
+
if (!analyzeHelperExpression(node.object, environment, usedParameterIndices)) return false;
|
|
23268
|
+
return !node.computed || analyzeHelperExpression(node.property, environment, usedParameterIndices);
|
|
23269
|
+
}
|
|
23270
|
+
if (isFunctionLike$1(node)) {
|
|
23271
|
+
if (isAsyncOrGeneratorFunction(node)) return false;
|
|
23272
|
+
const callbackParameterIndices = new Map(environment.parameterIndices);
|
|
23273
|
+
for (const parameter of getFunctionParameters(node)) {
|
|
23274
|
+
if (!isNodeOfType(parameter, "Identifier")) return false;
|
|
23275
|
+
callbackParameterIndices.set(parameter.name, null);
|
|
23276
|
+
}
|
|
23277
|
+
const callbackEnvironment = {
|
|
23278
|
+
parameterIndices: callbackParameterIndices,
|
|
23279
|
+
recursiveNames: environment.recursiveNames,
|
|
23280
|
+
shadowedGlobalNames: environment.shadowedGlobalNames
|
|
23281
|
+
};
|
|
23282
|
+
if (!isNodeOfType(node.body, "BlockStatement")) return analyzeHelperExpression(node.body, callbackEnvironment, usedParameterIndices);
|
|
23283
|
+
const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
|
|
23284
|
+
return callbackSummary.isValid && !callbackSummary.canContinue;
|
|
23285
|
+
}
|
|
23286
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
23287
|
+
const callee = stripParenExpression(node.callee);
|
|
23288
|
+
const calleeRoot = getMemberRoot(callee);
|
|
23289
|
+
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);
|
|
23290
|
+
const namespaceName = isNodeOfType(calleeRoot, "Identifier") && !environment.parameterIndices.has(calleeRoot.name) && !environment.recursiveNames.has(calleeRoot.name) && !environment.shadowedGlobalNames.has(calleeRoot.name) ? calleeRoot.name : null;
|
|
23291
|
+
const namespaceMemberName = getStaticMemberName(callee);
|
|
23292
|
+
const isPureNamespaceCall = isNodeOfType(callee, "MemberExpression") && namespaceName !== null && namespaceMemberName !== null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(namespaceName)?.has(namespaceMemberName) === true;
|
|
23293
|
+
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
|
|
23294
|
+
if (!isPureGlobalCall && !isPureNamespaceCall && !isPureMemberTransform) return false;
|
|
23295
|
+
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression") && !analyzeHelperExpression(callee.object, environment, usedParameterIndices)) return false;
|
|
23296
|
+
return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
|
|
23297
|
+
}
|
|
23298
|
+
if (isNodeOfType(node, "SpreadElement")) return analyzeHelperExpression(node.argument, environment, usedParameterIndices);
|
|
23299
|
+
return false;
|
|
23300
|
+
};
|
|
23301
|
+
const helperSummaryCache = /* @__PURE__ */ new WeakMap();
|
|
23302
|
+
const summarizeHelperReturn = (functionNode) => {
|
|
23303
|
+
if (!isFunctionLike$1(functionNode) || !isModuleFunction(functionNode)) return null;
|
|
23304
|
+
if (helperSummaryCache.has(functionNode)) return helperSummaryCache.get(functionNode) ?? null;
|
|
23305
|
+
if (isAsyncOrGeneratorFunction(functionNode)) {
|
|
23306
|
+
helperSummaryCache.set(functionNode, null);
|
|
23307
|
+
return null;
|
|
23308
|
+
}
|
|
23309
|
+
const parameterIndices = /* @__PURE__ */ new Map();
|
|
23310
|
+
const parameters = getFunctionParameters(functionNode);
|
|
23311
|
+
for (let parameterIndex = 0; parameterIndex < parameters.length; parameterIndex += 1) {
|
|
23312
|
+
const parameter = parameters[parameterIndex];
|
|
23313
|
+
if (!parameter || !isNodeOfType(parameter, "Identifier") || parameterIndices.has(parameter.name)) {
|
|
23314
|
+
helperSummaryCache.set(functionNode, null);
|
|
23315
|
+
return null;
|
|
23316
|
+
}
|
|
23317
|
+
parameterIndices.set(parameter.name, parameterIndex);
|
|
23318
|
+
}
|
|
23319
|
+
const environment = {
|
|
23320
|
+
parameterIndices,
|
|
23321
|
+
recursiveNames: getFunctionBindingNames(functionNode),
|
|
23322
|
+
shadowedGlobalNames: collectModuleBindingNames(functionNode)
|
|
23323
|
+
};
|
|
23324
|
+
const usedParameterIndices = /* @__PURE__ */ new Set();
|
|
23325
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) {
|
|
23326
|
+
if (!analyzeHelperExpression(functionNode.body, environment, usedParameterIndices)) {
|
|
23327
|
+
helperSummaryCache.set(functionNode, null);
|
|
23328
|
+
return null;
|
|
23329
|
+
}
|
|
23330
|
+
} else {
|
|
23331
|
+
const controlFlowSummary = analyzeHelperStatements(functionNode.body.body ?? [], environment, usedParameterIndices, analyzeHelperExpression);
|
|
23332
|
+
if (!controlFlowSummary.isValid || controlFlowSummary.canContinue) {
|
|
23333
|
+
helperSummaryCache.set(functionNode, null);
|
|
23334
|
+
return null;
|
|
23335
|
+
}
|
|
23336
|
+
}
|
|
23337
|
+
const summary = { usedParameterIndices };
|
|
23338
|
+
helperSummaryCache.set(functionNode, summary);
|
|
23339
|
+
return summary;
|
|
23340
|
+
};
|
|
23341
|
+
const resolveValueHelperFunction = (analysis, callee, currentFilename) => {
|
|
23342
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
23343
|
+
const reference = getRef(analysis, callee);
|
|
23344
|
+
if (!reference?.resolved) return null;
|
|
23345
|
+
const importDefinition = reference.resolved.defs.find((definition) => definition.type === "ImportBinding");
|
|
23346
|
+
if (importDefinition) {
|
|
23347
|
+
if (!currentFilename) return null;
|
|
23348
|
+
const specifier = importDefinition.node;
|
|
23349
|
+
if (!isNodeOfType(specifier, "ImportSpecifier") && !isNodeOfType(specifier, "ImportDefaultSpecifier")) return null;
|
|
23350
|
+
const importDeclaration = specifier.parent;
|
|
23351
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
|
|
23352
|
+
if (importDeclaration.importKind === "type" || isNodeOfType(specifier, "ImportSpecifier") && specifier.importKind === "type") return null;
|
|
23353
|
+
const source = importDeclaration.source?.value;
|
|
23354
|
+
if (typeof source !== "string") return null;
|
|
23355
|
+
const exportedName = resolveImportedExportName(specifier);
|
|
23356
|
+
if (!exportedName) return null;
|
|
23357
|
+
return resolveCrossFileFunctionExport(currentFilename, source, exportedName);
|
|
23358
|
+
}
|
|
23359
|
+
const callable = resolveWrappedCallable(analysis, callee);
|
|
23360
|
+
return callable && isModuleFunction(callable) ? callable : null;
|
|
23361
|
+
};
|
|
22762
23362
|
const isLocallyConstructedObjectMember = (reference, memberExpression) => isNodeOfType(memberExpression, "MemberExpression") && reference.resolved?.defs.some((definition) => {
|
|
22763
23363
|
const definitionNode = definition.node;
|
|
22764
23364
|
return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
|
|
@@ -22844,38 +23444,55 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
22844
23444
|
const calleeRoot = getMemberRoot(callee);
|
|
22845
23445
|
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;
|
|
22846
23446
|
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
|
|
22847
|
-
if (isPureMemberTransform)
|
|
22848
|
-
|
|
22849
|
-
|
|
22850
|
-
|
|
22851
|
-
|
|
22852
|
-
|
|
22853
|
-
if (isNodeOfType(callee, "MemberExpression")) {
|
|
22854
|
-
evidence.hasUnknownSource = true;
|
|
23447
|
+
if (isPureGlobalCall || isPureMemberTransform) {
|
|
23448
|
+
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
23449
|
+
for (const argument of node.arguments ?? []) {
|
|
23450
|
+
if (isFunctionLike$1(argument)) continue;
|
|
23451
|
+
mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
23452
|
+
}
|
|
22855
23453
|
return evidence;
|
|
22856
23454
|
}
|
|
22857
23455
|
if (remainingCallFrames <= 0) {
|
|
22858
23456
|
evidence.hasUnknownSource = true;
|
|
22859
23457
|
return evidence;
|
|
22860
23458
|
}
|
|
22861
|
-
const
|
|
22862
|
-
if (
|
|
22863
|
-
|
|
23459
|
+
const localHelperFunction = resolveWrappedCallable(analysis, callee);
|
|
23460
|
+
if (localHelperFunction && !isModuleFunction(localHelperFunction)) {
|
|
23461
|
+
if (isAsyncOrGeneratorFunction(localHelperFunction) || functionInvokesItself(analysis, localHelperFunction)) {
|
|
23462
|
+
evidence.hasUnknownSource = true;
|
|
23463
|
+
return evidence;
|
|
23464
|
+
}
|
|
23465
|
+
const localHelperFrame = {
|
|
23466
|
+
functionNode: localHelperFunction,
|
|
23467
|
+
invocation: node,
|
|
23468
|
+
isDeferred: false,
|
|
23469
|
+
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23470
|
+
substitutions: buildSubstitutions(analysis, localHelperFunction, node.arguments ?? [], frame),
|
|
23471
|
+
currentFilename: frame.currentFilename
|
|
23472
|
+
};
|
|
23473
|
+
const returnedExpressions = getReturnedExpressions(localHelperFunction);
|
|
23474
|
+
if (returnedExpressions.length === 0) {
|
|
23475
|
+
evidence.hasUnknownSource = true;
|
|
23476
|
+
return evidence;
|
|
23477
|
+
}
|
|
23478
|
+
for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, localHelperFrame, remainingCallFrames - 1, new Set(visitedBindings)));
|
|
22864
23479
|
return evidence;
|
|
22865
23480
|
}
|
|
22866
|
-
const
|
|
22867
|
-
|
|
22868
|
-
|
|
22869
|
-
isDeferred: false,
|
|
22870
|
-
introducedBindings: /* @__PURE__ */ new Set(),
|
|
22871
|
-
substitutions: buildSubstitutions(analysis, callable, node.arguments ?? [], frame)
|
|
22872
|
-
};
|
|
22873
|
-
const returnedExpressions = getReturnedExpressions(callable);
|
|
22874
|
-
if (returnedExpressions.length === 0) {
|
|
23481
|
+
const helperFunction = resolveValueHelperFunction(analysis, callee, frame.currentFilename);
|
|
23482
|
+
const helperSummary = helperFunction ? summarizeHelperReturn(helperFunction) : null;
|
|
23483
|
+
if (!helperSummary) {
|
|
22875
23484
|
evidence.hasUnknownSource = true;
|
|
22876
23485
|
return evidence;
|
|
22877
23486
|
}
|
|
22878
|
-
|
|
23487
|
+
const argumentsForHelper = node.arguments ?? [];
|
|
23488
|
+
for (const parameterIndex of helperSummary.usedParameterIndices) {
|
|
23489
|
+
const argument = argumentsForHelper[parameterIndex];
|
|
23490
|
+
if (!argument) {
|
|
23491
|
+
evidence.hasUnknownSource = true;
|
|
23492
|
+
return evidence;
|
|
23493
|
+
}
|
|
23494
|
+
mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames - 1, new Set(visitedBindings)));
|
|
23495
|
+
}
|
|
22879
23496
|
return evidence;
|
|
22880
23497
|
}
|
|
22881
23498
|
if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
|
|
@@ -22893,6 +23510,20 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
22893
23510
|
}
|
|
22894
23511
|
return evidence;
|
|
22895
23512
|
};
|
|
23513
|
+
const collectRenderValueEvidence = (analysis, expression, componentFunction, currentFilename) => {
|
|
23514
|
+
const evidence = collectValueEvidence(analysis, expression, {
|
|
23515
|
+
functionNode: componentFunction,
|
|
23516
|
+
invocation: null,
|
|
23517
|
+
isDeferred: false,
|
|
23518
|
+
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23519
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
23520
|
+
currentFilename
|
|
23521
|
+
}, 1);
|
|
23522
|
+
return {
|
|
23523
|
+
sourceReferences: evidence.sourceReferences,
|
|
23524
|
+
isExclusivelyRenderKnown: evidence.sourceReferences.size > 0 && !evidence.hasUnknownSource && !evidence.hasDeferredIntroducedValue && !evidence.readsExternalValue
|
|
23525
|
+
};
|
|
23526
|
+
};
|
|
22896
23527
|
const findStateSetterReference = (analysis, callExpression) => {
|
|
22897
23528
|
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
22898
23529
|
const callee = stripParenExpression(callExpression.callee);
|
|
@@ -22955,8 +23586,8 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
|
|
|
22955
23586
|
}
|
|
22956
23587
|
return false;
|
|
22957
23588
|
};
|
|
22958
|
-
const collectEffectStateWriteFacts = (analysis, effectNode) => {
|
|
22959
|
-
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode);
|
|
23589
|
+
const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
|
|
23590
|
+
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
|
|
22960
23591
|
if (frames.length === 0) return [];
|
|
22961
23592
|
const effectHasCleanup = hasCleanup(analysis, effectNode);
|
|
22962
23593
|
const cleanupManagedStateDeclarators = /* @__PURE__ */ new Set();
|
|
@@ -22976,7 +23607,8 @@ const collectEffectStateWriteFacts = (analysis, effectNode) => {
|
|
|
22976
23607
|
invocation: callExpression,
|
|
22977
23608
|
isDeferred: frame.isDeferred,
|
|
22978
23609
|
introducedBindings: collectIntroducedBindings(analysis, writtenValue),
|
|
22979
|
-
substitutions: /* @__PURE__ */ new Map()
|
|
23610
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
23611
|
+
currentFilename
|
|
22980
23612
|
};
|
|
22981
23613
|
valueEvidence = emptyEvidence();
|
|
22982
23614
|
const returnedExpressions = getReturnedExpressions(writtenValue);
|
|
@@ -23025,7 +23657,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
23025
23657
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
23026
23658
|
if (!dependencyReferences) return;
|
|
23027
23659
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
23028
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
23660
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
23029
23661
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
23030
23662
|
context.report({
|
|
23031
23663
|
node: fact.callExpression,
|
|
@@ -24286,6 +24918,139 @@ const createRelativeImportSource = (filename, targetFilePath) => {
|
|
|
24286
24918
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
24287
24919
|
};
|
|
24288
24920
|
//#endregion
|
|
24921
|
+
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
24922
|
+
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
24923
|
+
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24924
|
+
const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24925
|
+
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24926
|
+
const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
|
|
24927
|
+
const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
|
|
24928
|
+
const createNonBarrelInfo = () => ({
|
|
24929
|
+
isBarrel: false,
|
|
24930
|
+
exportsByName: /* @__PURE__ */ new Map(),
|
|
24931
|
+
starExportSources: []
|
|
24932
|
+
});
|
|
24933
|
+
const addImportedBinding = (importedBindings, binding) => {
|
|
24934
|
+
importedBindings.set(binding.localName, {
|
|
24935
|
+
...binding,
|
|
24936
|
+
didExport: false
|
|
24937
|
+
});
|
|
24938
|
+
};
|
|
24939
|
+
const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
|
|
24940
|
+
for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
|
|
24941
|
+
localName: specifier.exportedName,
|
|
24942
|
+
importedName: specifier.localName,
|
|
24943
|
+
source,
|
|
24944
|
+
isTypeOnly: specifier.isTypeOnly
|
|
24945
|
+
});
|
|
24946
|
+
};
|
|
24947
|
+
const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
|
|
24948
|
+
const trimmedImportClause = importClause.trim();
|
|
24949
|
+
const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
|
|
24950
|
+
if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
|
|
24951
|
+
localName: namespaceMatch[1],
|
|
24952
|
+
importedName: "*",
|
|
24953
|
+
source,
|
|
24954
|
+
isTypeOnly: declarationIsTypeOnly
|
|
24955
|
+
});
|
|
24956
|
+
const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
|
|
24957
|
+
if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
|
|
24958
|
+
const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
|
|
24959
|
+
if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
|
|
24960
|
+
localName: defaultImportName,
|
|
24961
|
+
importedName: "default",
|
|
24962
|
+
source,
|
|
24963
|
+
isTypeOnly: declarationIsTypeOnly
|
|
24964
|
+
});
|
|
24965
|
+
};
|
|
24966
|
+
const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
|
|
24967
|
+
let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
|
|
24968
|
+
collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
|
|
24969
|
+
return "";
|
|
24970
|
+
});
|
|
24971
|
+
withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
|
|
24972
|
+
const isTypeOnly = Boolean(typeKeyword);
|
|
24973
|
+
if (namespaceExportName) {
|
|
24974
|
+
exportsByName.set(namespaceExportName, {
|
|
24975
|
+
exportedName: namespaceExportName,
|
|
24976
|
+
importedName: "*",
|
|
24977
|
+
source,
|
|
24978
|
+
isTypeOnly
|
|
24979
|
+
});
|
|
24980
|
+
return "";
|
|
24981
|
+
}
|
|
24982
|
+
if (specifiersText) {
|
|
24983
|
+
for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
|
|
24984
|
+
exportedName: specifier.exportedName,
|
|
24985
|
+
importedName: specifier.localName,
|
|
24986
|
+
source,
|
|
24987
|
+
isTypeOnly: specifier.isTypeOnly
|
|
24988
|
+
});
|
|
24989
|
+
return "";
|
|
24990
|
+
}
|
|
24991
|
+
starExportSources.push(source);
|
|
24992
|
+
return "";
|
|
24993
|
+
});
|
|
24994
|
+
withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
|
|
24995
|
+
for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
|
|
24996
|
+
const importedBinding = importedBindings.get(specifier.localName);
|
|
24997
|
+
if (!importedBinding) return _match;
|
|
24998
|
+
importedBinding.didExport = true;
|
|
24999
|
+
exportsByName.set(specifier.exportedName, {
|
|
25000
|
+
exportedName: specifier.exportedName,
|
|
25001
|
+
importedName: importedBinding.importedName,
|
|
25002
|
+
source: importedBinding.source,
|
|
25003
|
+
isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
|
|
25004
|
+
});
|
|
25005
|
+
}
|
|
25006
|
+
return "";
|
|
25007
|
+
});
|
|
25008
|
+
return withoutKnownDeclarations;
|
|
25009
|
+
};
|
|
25010
|
+
const hasUnexportedRuntimeImport = (importedBindings) => {
|
|
25011
|
+
for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
|
|
25012
|
+
return false;
|
|
25013
|
+
};
|
|
25014
|
+
const classifyBarrelModule = (sourceText) => {
|
|
25015
|
+
const strippedSource = stripJsComments(sourceText).trim();
|
|
25016
|
+
if (!strippedSource) return createNonBarrelInfo();
|
|
25017
|
+
const importedBindings = /* @__PURE__ */ new Map();
|
|
25018
|
+
const exportsByName = /* @__PURE__ */ new Map();
|
|
25019
|
+
const starExportSources = [];
|
|
25020
|
+
if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
|
|
25021
|
+
return {
|
|
25022
|
+
isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
|
|
25023
|
+
exportsByName,
|
|
25024
|
+
starExportSources
|
|
25025
|
+
};
|
|
25026
|
+
};
|
|
25027
|
+
const getBarrelIndexModuleInfo = (filePath) => {
|
|
25028
|
+
if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
|
|
25029
|
+
recordContentProbe(filePath);
|
|
25030
|
+
let fileStat;
|
|
25031
|
+
try {
|
|
25032
|
+
fileStat = fs.statSync(filePath);
|
|
25033
|
+
} catch {
|
|
25034
|
+
fileStat = null;
|
|
25035
|
+
}
|
|
25036
|
+
const cachedResult = barrelIndexModuleInfoCache.get(filePath);
|
|
25037
|
+
if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
|
|
25038
|
+
if (fileStat === null) return createNonBarrelInfo();
|
|
25039
|
+
let moduleInfo = createNonBarrelInfo();
|
|
25040
|
+
try {
|
|
25041
|
+
moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
|
|
25042
|
+
} catch {
|
|
25043
|
+
moduleInfo = createNonBarrelInfo();
|
|
25044
|
+
}
|
|
25045
|
+
barrelIndexModuleInfoCache.set(filePath, {
|
|
25046
|
+
mtimeMs: fileStat.mtimeMs,
|
|
25047
|
+
size: fileStat.size,
|
|
25048
|
+
moduleInfo
|
|
25049
|
+
});
|
|
25050
|
+
return moduleInfo;
|
|
25051
|
+
};
|
|
25052
|
+
const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
|
|
25053
|
+
//#endregion
|
|
24289
25054
|
//#region src/react-native-dependency-names.ts
|
|
24290
25055
|
const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
|
|
24291
25056
|
"expo",
|
|
@@ -24488,6 +25253,35 @@ const classifyReactNativeFileTarget = (context) => {
|
|
|
24488
25253
|
};
|
|
24489
25254
|
const isReactNativeFileActive = (context) => classifyReactNativeFileTarget(context) !== "web";
|
|
24490
25255
|
//#endregion
|
|
25256
|
+
//#region src/plugin/utils/resolve-barrel-export-file-path.ts
|
|
25257
|
+
const getUniqueFilePath = (filePaths) => {
|
|
25258
|
+
const uniqueFilePaths = new Set(filePaths);
|
|
25259
|
+
if (uniqueFilePaths.size !== 1) return null;
|
|
25260
|
+
const [filePath] = uniqueFilePaths;
|
|
25261
|
+
return filePath ?? null;
|
|
25262
|
+
};
|
|
25263
|
+
const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
|
|
25264
|
+
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
|
|
25265
|
+
if (!resolvedTargetPath) return null;
|
|
25266
|
+
const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
|
|
25267
|
+
if (nestedTargetPath) return nestedTargetPath;
|
|
25268
|
+
return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
|
|
25269
|
+
};
|
|
25270
|
+
const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
|
|
25271
|
+
if (visitedFilePaths.has(barrelFilePath)) return null;
|
|
25272
|
+
visitedFilePaths.add(barrelFilePath);
|
|
25273
|
+
const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
|
|
25274
|
+
if (!moduleInfo.isBarrel) return null;
|
|
25275
|
+
const target = moduleInfo.exportsByName.get(exportedName);
|
|
25276
|
+
if (target) {
|
|
25277
|
+
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
|
|
25278
|
+
if (!resolvedTargetPath) return null;
|
|
25279
|
+
return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
|
|
25280
|
+
}
|
|
25281
|
+
if (exportedName === "default") return null;
|
|
25282
|
+
return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
|
|
25283
|
+
};
|
|
25284
|
+
//#endregion
|
|
24491
25285
|
//#region src/plugin/rules/bundle-size/no-barrel-import.ts
|
|
24492
25286
|
const TYPE_DECLARATION_FILE_PATTERN = /\.d\.[cm]?ts$/;
|
|
24493
25287
|
const SERVER_ONLY_FILE_PATTERN = /\.server\.[cm]?[jt]sx?$/;
|
|
@@ -24817,7 +25611,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
24817
25611
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
24818
25612
|
return false;
|
|
24819
25613
|
};
|
|
24820
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES
|
|
25614
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
24821
25615
|
"forEach",
|
|
24822
25616
|
"map",
|
|
24823
25617
|
"filter",
|
|
@@ -24838,7 +25632,7 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
24838
25632
|
if (parent.callee === functionNode) return true;
|
|
24839
25633
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
24840
25634
|
const callee = parent.callee;
|
|
24841
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES
|
|
25635
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
24842
25636
|
};
|
|
24843
25637
|
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
24844
25638
|
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
@@ -25782,57 +26576,6 @@ const noDefaultProps = defineRule({
|
|
|
25782
26576
|
} })
|
|
25783
26577
|
});
|
|
25784
26578
|
//#endregion
|
|
25785
|
-
//#region src/plugin/rules/state-and-effects/no-derived-state.ts
|
|
25786
|
-
const getStateName$1 = (stateDeclarator) => {
|
|
25787
|
-
if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
|
|
25788
|
-
if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
|
|
25789
|
-
const stateBinding = stateDeclarator.id.elements?.[0];
|
|
25790
|
-
if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
|
|
25791
|
-
const setterBinding = stateDeclarator.id.elements?.[1];
|
|
25792
|
-
if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
|
|
25793
|
-
if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
|
|
25794
|
-
return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
|
|
25795
|
-
};
|
|
25796
|
-
const noDerivedState = defineRule({
|
|
25797
|
-
id: "no-derived-state",
|
|
25798
|
-
title: "Derived value copied into state",
|
|
25799
|
-
severity: "warn",
|
|
25800
|
-
tags: ["test-noise"],
|
|
25801
|
-
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",
|
|
25802
|
-
create: (context) => ({ CallExpression(node) {
|
|
25803
|
-
if (!isUseEffect(node)) return;
|
|
25804
|
-
const analysis = getProgramAnalysis(node);
|
|
25805
|
-
if (!analysis) return;
|
|
25806
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
25807
|
-
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
25808
|
-
const stateName = getStateName$1(fact.stateDeclarator);
|
|
25809
|
-
context.report({
|
|
25810
|
-
node: fact.callExpression,
|
|
25811
|
-
message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
|
|
25812
|
-
});
|
|
25813
|
-
}
|
|
25814
|
-
} })
|
|
25815
|
-
});
|
|
25816
|
-
//#endregion
|
|
25817
|
-
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
25818
|
-
const noDerivedStateEffect = defineRule({
|
|
25819
|
-
id: "no-derived-state-effect",
|
|
25820
|
-
title: "Derived state stored in an effect",
|
|
25821
|
-
severity: "warn",
|
|
25822
|
-
tags: ["test-noise"],
|
|
25823
|
-
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",
|
|
25824
|
-
create: (context) => ({ CallExpression(node) {
|
|
25825
|
-
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
25826
|
-
const analysis = getProgramAnalysis(node);
|
|
25827
|
-
if (!analysis) return;
|
|
25828
|
-
if (!collectEffectStateWriteFacts(analysis, node).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
25829
|
-
context.report({
|
|
25830
|
-
node,
|
|
25831
|
-
message: "You pay an extra render for state you can derive from other values."
|
|
25832
|
-
});
|
|
25833
|
-
} })
|
|
25834
|
-
});
|
|
25835
|
-
//#endregion
|
|
25836
26579
|
//#region src/plugin/utils/is-component-function.ts
|
|
25837
26580
|
const isFunctionAssignedToComponent = (functionNode) => {
|
|
25838
26581
|
let cursor = functionNode.parent ?? null;
|
|
@@ -25966,6 +26709,236 @@ const createComponentPropStackTracker = (callbacks) => {
|
|
|
25966
26709
|
};
|
|
25967
26710
|
};
|
|
25968
26711
|
//#endregion
|
|
26712
|
+
//#region src/plugin/rules/state-and-effects/utils/collect-render-state-write-facts.ts
|
|
26713
|
+
const findReferenceDeclarator = (reference) => {
|
|
26714
|
+
for (const definition of reference.resolved?.defs ?? []) {
|
|
26715
|
+
const definitionNode = definition.node;
|
|
26716
|
+
if (isNodeOfType(definitionNode, "VariableDeclarator")) return definitionNode;
|
|
26717
|
+
}
|
|
26718
|
+
return null;
|
|
26719
|
+
};
|
|
26720
|
+
const resolveSimpleRenderSource = (analysis, expression, visitedBindings = /* @__PURE__ */ new Set()) => {
|
|
26721
|
+
const node = stripParenExpression(expression);
|
|
26722
|
+
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26723
|
+
const reference = getRef(analysis, node);
|
|
26724
|
+
if (!reference?.resolved || visitedBindings.has(reference.resolved)) return null;
|
|
26725
|
+
if (isProp(analysis, reference)) {
|
|
26726
|
+
if (isWholePropsObjectReference(analysis, reference)) return null;
|
|
26727
|
+
return { bindingIdentity: reference.resolved };
|
|
26728
|
+
}
|
|
26729
|
+
if (isState(analysis, reference)) {
|
|
26730
|
+
const stateDeclarator = getUseStateDecl(analysis, reference);
|
|
26731
|
+
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || isExternallyDrivenState(analysis, reference)) return null;
|
|
26732
|
+
return { bindingIdentity: reference.resolved };
|
|
26733
|
+
}
|
|
26734
|
+
if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
|
|
26735
|
+
const declarator = findReferenceDeclarator(reference);
|
|
26736
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
26737
|
+
const nextVisitedBindings = new Set(visitedBindings);
|
|
26738
|
+
nextVisitedBindings.add(reference.resolved);
|
|
26739
|
+
return resolveSimpleRenderSource(analysis, declarator.init, nextVisitedBindings);
|
|
26740
|
+
};
|
|
26741
|
+
const doesEvidenceMatchSource = (evidence, source) => evidence.isExclusivelyRenderKnown && evidence.sourceReferences.size > 0 && [...evidence.sourceReferences].every((sourceReference) => sourceReference.resolved === source.bindingIdentity);
|
|
26742
|
+
const getDirectBranchStatements = (branch) => {
|
|
26743
|
+
if (isNodeOfType(branch, "BlockStatement")) return branch.body ?? [];
|
|
26744
|
+
return [branch];
|
|
26745
|
+
};
|
|
26746
|
+
const getDirectCallExpression = (statement) => {
|
|
26747
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26748
|
+
const expression = stripParenExpression(statement.expression);
|
|
26749
|
+
return isNodeOfType(expression, "CallExpression") ? expression : null;
|
|
26750
|
+
};
|
|
26751
|
+
const getDirectAssignmentExpression = (statement) => {
|
|
26752
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26753
|
+
const expression = stripParenExpression(statement.expression);
|
|
26754
|
+
return isNodeOfType(expression, "AssignmentExpression") && expression.operator === "=" ? expression : null;
|
|
26755
|
+
};
|
|
26756
|
+
const findDirectStateSetterReference = (analysis, callExpression) => {
|
|
26757
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
26758
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
26759
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
26760
|
+
const reference = getRef(analysis, callee);
|
|
26761
|
+
return reference && isStateSetter(analysis, reference) ? reference : null;
|
|
26762
|
+
};
|
|
26763
|
+
const getStaticRefCurrentReference = (analysis, expression) => {
|
|
26764
|
+
const node = stripParenExpression(expression);
|
|
26765
|
+
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.object, "Identifier") || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
|
|
26766
|
+
const reference = getRef(analysis, node.object);
|
|
26767
|
+
if (!reference) return null;
|
|
26768
|
+
const declarator = findReferenceDeclarator(reference);
|
|
26769
|
+
return declarator && isGenuineReactHookDeclarator(analysis, declarator, "useRef") ? reference : null;
|
|
26770
|
+
};
|
|
26771
|
+
const getStateTracker = (analysis, expression) => {
|
|
26772
|
+
const node = stripParenExpression(expression);
|
|
26773
|
+
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26774
|
+
const reference = getRef(analysis, node);
|
|
26775
|
+
if (!reference || !isState(analysis, reference)) return null;
|
|
26776
|
+
const declarator = getUseStateDecl(analysis, reference);
|
|
26777
|
+
if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useState")) return null;
|
|
26778
|
+
return {
|
|
26779
|
+
kind: "state",
|
|
26780
|
+
declarator
|
|
26781
|
+
};
|
|
26782
|
+
};
|
|
26783
|
+
const getRefTracker = (analysis, expression) => {
|
|
26784
|
+
const reference = getStaticRefCurrentReference(analysis, expression);
|
|
26785
|
+
return reference ? {
|
|
26786
|
+
kind: "ref",
|
|
26787
|
+
reference
|
|
26788
|
+
} : null;
|
|
26789
|
+
};
|
|
26790
|
+
const getTrackerInitializer = (tracker) => {
|
|
26791
|
+
const declarator = tracker.kind === "state" ? tracker.declarator : findReferenceDeclarator(tracker.reference);
|
|
26792
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return null;
|
|
26793
|
+
const initializer = declarator.init.arguments?.[0];
|
|
26794
|
+
return initializer ? initializer : null;
|
|
26795
|
+
};
|
|
26796
|
+
const isTrackerSynchronized = (analysis, guard) => {
|
|
26797
|
+
for (const statement of guard.statements) {
|
|
26798
|
+
if (guard.tracker.kind === "state") {
|
|
26799
|
+
const callExpression = getDirectCallExpression(statement);
|
|
26800
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26801
|
+
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26802
|
+
if (!setterReference || getUseStateDecl(analysis, setterReference) !== guard.tracker.declarator || (callExpression.arguments ?? []).length !== 1) continue;
|
|
26803
|
+
const writtenValue = callExpression.arguments?.[0];
|
|
26804
|
+
if (writtenValue && resolveSimpleRenderSource(analysis, writtenValue)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26805
|
+
continue;
|
|
26806
|
+
}
|
|
26807
|
+
const assignmentExpression = getDirectAssignmentExpression(statement);
|
|
26808
|
+
if (!assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression")) continue;
|
|
26809
|
+
if (getStaticRefCurrentReference(analysis, assignmentExpression.left)?.resolved !== guard.tracker.reference.resolved) continue;
|
|
26810
|
+
if (resolveSimpleRenderSource(analysis, assignmentExpression.right)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26811
|
+
}
|
|
26812
|
+
return false;
|
|
26813
|
+
};
|
|
26814
|
+
const parseRenderTrackerGuard = (analysis, statement) => {
|
|
26815
|
+
if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isNodeOfType(statement.test, "BinaryExpression") || statement.test.operator !== "!==" || !isNodeOfType(statement.consequent, "BlockStatement")) return null;
|
|
26816
|
+
const left = statement.test.left;
|
|
26817
|
+
const right = statement.test.right;
|
|
26818
|
+
const candidates = [{
|
|
26819
|
+
sourceExpression: left,
|
|
26820
|
+
trackerExpression: right
|
|
26821
|
+
}, {
|
|
26822
|
+
sourceExpression: right,
|
|
26823
|
+
trackerExpression: left
|
|
26824
|
+
}];
|
|
26825
|
+
for (const candidate of candidates) {
|
|
26826
|
+
const source = resolveSimpleRenderSource(analysis, candidate.sourceExpression);
|
|
26827
|
+
if (!source) continue;
|
|
26828
|
+
const tracker = getStateTracker(analysis, candidate.trackerExpression) ?? getRefTracker(analysis, candidate.trackerExpression);
|
|
26829
|
+
if (!tracker) continue;
|
|
26830
|
+
const trackerInitializer = getTrackerInitializer(tracker);
|
|
26831
|
+
if (!trackerInitializer || resolveSimpleRenderSource(analysis, trackerInitializer)?.bindingIdentity !== source.bindingIdentity) continue;
|
|
26832
|
+
const guard = {
|
|
26833
|
+
source,
|
|
26834
|
+
tracker,
|
|
26835
|
+
statements: getDirectBranchStatements(statement.consequent)
|
|
26836
|
+
};
|
|
26837
|
+
return isTrackerSynchronized(analysis, guard) ? guard : null;
|
|
26838
|
+
}
|
|
26839
|
+
return null;
|
|
26840
|
+
};
|
|
26841
|
+
const isExclusiveDestinationSetterReference = (setterReference, callExpression) => {
|
|
26842
|
+
if (!setterReference.resolved || !isNodeOfType(callExpression, "CallExpression")) return false;
|
|
26843
|
+
const references = setterReference.resolved.references.filter((reference) => !reference.init);
|
|
26844
|
+
if (references.length !== 1) return false;
|
|
26845
|
+
return references[0].identifier === callExpression.callee;
|
|
26846
|
+
};
|
|
26847
|
+
const getStateInitializer = (stateDeclarator) => {
|
|
26848
|
+
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression")) return null;
|
|
26849
|
+
const initializer = stateDeclarator.init.arguments?.[0];
|
|
26850
|
+
return initializer ? initializer : null;
|
|
26851
|
+
};
|
|
26852
|
+
const collectRenderStateWriteFacts = (analysis, componentBody, currentFilename) => {
|
|
26853
|
+
if (!isNodeOfType(componentBody, "BlockStatement") || !componentBody.parent || !isFunctionLike$1(componentBody.parent)) return [];
|
|
26854
|
+
const componentFunction = componentBody.parent;
|
|
26855
|
+
const facts = [];
|
|
26856
|
+
for (const statement of componentBody.body ?? []) {
|
|
26857
|
+
const guard = parseRenderTrackerGuard(analysis, statement);
|
|
26858
|
+
if (!guard) continue;
|
|
26859
|
+
for (const branchStatement of guard.statements) {
|
|
26860
|
+
const callExpression = getDirectCallExpression(branchStatement);
|
|
26861
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26862
|
+
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26863
|
+
if (!setterReference || (callExpression.arguments ?? []).length !== 1 || !isExclusiveDestinationSetterReference(setterReference, callExpression)) continue;
|
|
26864
|
+
const stateDeclarator = getUseStateDecl(analysis, setterReference);
|
|
26865
|
+
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || guard.tracker.kind === "state" && stateDeclarator === guard.tracker.declarator) continue;
|
|
26866
|
+
const writtenValue = callExpression.arguments?.[0];
|
|
26867
|
+
const initializer = getStateInitializer(stateDeclarator);
|
|
26868
|
+
if (!writtenValue || !initializer || isFunctionLike$1(writtenValue) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, writtenValue, componentFunction, currentFilename), guard.source) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, initializer, componentFunction, currentFilename), guard.source)) continue;
|
|
26869
|
+
facts.push({
|
|
26870
|
+
callExpression,
|
|
26871
|
+
stateDeclarator
|
|
26872
|
+
});
|
|
26873
|
+
}
|
|
26874
|
+
}
|
|
26875
|
+
return facts;
|
|
26876
|
+
};
|
|
26877
|
+
//#endregion
|
|
26878
|
+
//#region src/plugin/rules/state-and-effects/no-derived-state.ts
|
|
26879
|
+
const getStateName$1 = (stateDeclarator) => {
|
|
26880
|
+
if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
|
|
26881
|
+
if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
|
|
26882
|
+
const stateBinding = stateDeclarator.id.elements?.[0];
|
|
26883
|
+
if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
|
|
26884
|
+
const setterBinding = stateDeclarator.id.elements?.[1];
|
|
26885
|
+
if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
|
|
26886
|
+
if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
|
|
26887
|
+
return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
|
|
26888
|
+
};
|
|
26889
|
+
const noDerivedState = defineRule({
|
|
26890
|
+
id: "no-derived-state",
|
|
26891
|
+
title: "Derived value copied into state",
|
|
26892
|
+
severity: "warn",
|
|
26893
|
+
tags: ["test-noise"],
|
|
26894
|
+
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",
|
|
26895
|
+
create: (context) => {
|
|
26896
|
+
const reportStateWrite = (callExpression, stateDeclarator) => {
|
|
26897
|
+
const stateName = getStateName$1(stateDeclarator);
|
|
26898
|
+
context.report({
|
|
26899
|
+
node: callExpression,
|
|
26900
|
+
message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
|
|
26901
|
+
});
|
|
26902
|
+
};
|
|
26903
|
+
return {
|
|
26904
|
+
...createComponentPropStackTracker({ onComponentEnter: (componentBody) => {
|
|
26905
|
+
if (!componentBody) return;
|
|
26906
|
+
const analysis = getProgramAnalysis(componentBody);
|
|
26907
|
+
if (!analysis) return;
|
|
26908
|
+
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26909
|
+
} }).visitors,
|
|
26910
|
+
CallExpression(node) {
|
|
26911
|
+
if (!isUseEffect(node)) return;
|
|
26912
|
+
const analysis = getProgramAnalysis(node);
|
|
26913
|
+
if (!analysis) return;
|
|
26914
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
26915
|
+
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
26916
|
+
reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26917
|
+
}
|
|
26918
|
+
}
|
|
26919
|
+
};
|
|
26920
|
+
}
|
|
26921
|
+
});
|
|
26922
|
+
//#endregion
|
|
26923
|
+
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
26924
|
+
const noDerivedStateEffect = defineRule({
|
|
26925
|
+
id: "no-derived-state-effect",
|
|
26926
|
+
title: "Derived state stored in an effect",
|
|
26927
|
+
severity: "warn",
|
|
26928
|
+
tags: ["test-noise"],
|
|
26929
|
+
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",
|
|
26930
|
+
create: (context) => ({ CallExpression(node) {
|
|
26931
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
26932
|
+
const analysis = getProgramAnalysis(node);
|
|
26933
|
+
if (!analysis) return;
|
|
26934
|
+
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
26935
|
+
context.report({
|
|
26936
|
+
node,
|
|
26937
|
+
message: "You pay an extra render for state you can derive from other values."
|
|
26938
|
+
});
|
|
26939
|
+
} })
|
|
26940
|
+
});
|
|
26941
|
+
//#endregion
|
|
25969
26942
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
25970
26943
|
const isInitialOnlyPropName = (propName) => {
|
|
25971
26944
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -27471,14 +28444,14 @@ const hasDocumentClassListMutation = (node) => {
|
|
|
27471
28444
|
//#endregion
|
|
27472
28445
|
//#region src/plugin/rules/state-and-effects/no-effect-event-handler.ts
|
|
27473
28446
|
const hasEventLikeNode = (node) => findTriggeredSideEffectCalleeName(node) !== null || hasDocumentClassListMutation(node);
|
|
27474
|
-
const unwrapChainExpression$
|
|
28447
|
+
const unwrapChainExpression$2 = (node) => {
|
|
27475
28448
|
if (!node) return null;
|
|
27476
28449
|
if (isNodeOfType(node, "ChainExpression")) return node.expression;
|
|
27477
28450
|
return node;
|
|
27478
28451
|
};
|
|
27479
28452
|
const collectGuardExpressions = (node, into) => {
|
|
27480
28453
|
if (!node) return;
|
|
27481
|
-
const unwrappedNode = unwrapChainExpression$
|
|
28454
|
+
const unwrappedNode = unwrapChainExpression$2(node);
|
|
27482
28455
|
if (!unwrappedNode) return;
|
|
27483
28456
|
const rootIdentifierName = getRootIdentifierName(unwrappedNode);
|
|
27484
28457
|
if (rootIdentifierName) {
|
|
@@ -27509,7 +28482,7 @@ const isReturnOnlyStatement = (node) => {
|
|
|
27509
28482
|
};
|
|
27510
28483
|
const hasEventLikeRemainingStatements = (statements) => statements.some((statement) => !isNodeOfType(statement, "ReturnStatement") && hasEventLikeNode(statement));
|
|
27511
28484
|
const doesGuardMatchDependency = (guardExpression, dependencyExpression) => {
|
|
27512
|
-
const unwrappedDependencyExpression = unwrapChainExpression$
|
|
28485
|
+
const unwrappedDependencyExpression = unwrapChainExpression$2(dependencyExpression);
|
|
27513
28486
|
if (!unwrappedDependencyExpression) return false;
|
|
27514
28487
|
if (areExpressionsStructurallyEqual(guardExpression.expression, unwrappedDependencyExpression)) return true;
|
|
27515
28488
|
return isNodeOfType(unwrappedDependencyExpression, "Identifier") && unwrappedDependencyExpression.name === guardExpression.rootIdentifierName;
|
|
@@ -29329,28 +30302,26 @@ const noGiantComponent = defineRule({
|
|
|
29329
30302
|
const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
|
|
29330
30303
|
return lineCount > 300 ? lineCount : null;
|
|
29331
30304
|
};
|
|
29332
|
-
const reportOversizedComponent = (nameNode, componentName
|
|
30305
|
+
const reportOversizedComponent = (nameNode, componentName) => {
|
|
29333
30306
|
context.report({
|
|
29334
30307
|
node: nameNode,
|
|
29335
|
-
message: `Component "${componentName}" is
|
|
30308
|
+
message: `Component "${componentName}" is over 300 lines long, which is hard to read & change. Split it into a few smaller components.`
|
|
29336
30309
|
});
|
|
29337
30310
|
};
|
|
29338
30311
|
return {
|
|
29339
30312
|
FunctionDeclaration(node) {
|
|
29340
30313
|
if (!node.id?.name || !isUppercaseName(node.id.name)) return;
|
|
29341
|
-
|
|
29342
|
-
if (lineCount === null) return;
|
|
30314
|
+
if (getOversizedComponentLineCount(node) === null) return;
|
|
29343
30315
|
if (!functionContainsReactRenderOutput(node, context.scopes)) return;
|
|
29344
|
-
reportOversizedComponent(node.id, node.id.name
|
|
30316
|
+
reportOversizedComponent(node.id, node.id.name);
|
|
29345
30317
|
},
|
|
29346
30318
|
VariableDeclarator(node) {
|
|
29347
30319
|
if (!isNodeOfType(node.id, "Identifier") || !isUppercaseName(node.id.name)) return;
|
|
29348
30320
|
const functionNode = unwrapReactHocFunction(node.init);
|
|
29349
30321
|
if (!functionNode) return;
|
|
29350
|
-
|
|
29351
|
-
if (lineCount === null) return;
|
|
30322
|
+
if (getOversizedComponentLineCount(functionNode) === null) return;
|
|
29352
30323
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
29353
|
-
reportOversizedComponent(node.id, node.id.name
|
|
30324
|
+
reportOversizedComponent(node.id, node.id.name);
|
|
29354
30325
|
}
|
|
29355
30326
|
};
|
|
29356
30327
|
}
|
|
@@ -29568,57 +30539,6 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
29568
30539
|
} })
|
|
29569
30540
|
});
|
|
29570
30541
|
//#endregion
|
|
29571
|
-
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
29572
|
-
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
29573
|
-
let ancestor = bindingIdentifier.parent;
|
|
29574
|
-
while (ancestor) {
|
|
29575
|
-
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
29576
|
-
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
29577
|
-
ancestor = ancestor.parent ?? null;
|
|
29578
|
-
}
|
|
29579
|
-
return null;
|
|
29580
|
-
};
|
|
29581
|
-
//#endregion
|
|
29582
|
-
//#region src/plugin/utils/executes-during-render.ts
|
|
29583
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
29584
|
-
"map",
|
|
29585
|
-
"filter",
|
|
29586
|
-
"forEach",
|
|
29587
|
-
"flatMap",
|
|
29588
|
-
"reduce",
|
|
29589
|
-
"reduceRight",
|
|
29590
|
-
"some",
|
|
29591
|
-
"every",
|
|
29592
|
-
"find",
|
|
29593
|
-
"findIndex",
|
|
29594
|
-
"findLast",
|
|
29595
|
-
"findLastIndex",
|
|
29596
|
-
"sort",
|
|
29597
|
-
"toSorted"
|
|
29598
|
-
]);
|
|
29599
|
-
const REACT_RENDER_PHASE_HOOK_NAMES = new Set(["useMemo", "useState"]);
|
|
29600
|
-
const isGlobalArrayFromMember = (node, scopes) => {
|
|
29601
|
-
const unwrappedNode = stripParenExpression(node);
|
|
29602
|
-
if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed || !isNodeOfType(unwrappedNode.object, "Identifier") || unwrappedNode.object.name !== "Array" || !scopes.isGlobalReference(unwrappedNode.object) || !isNodeOfType(unwrappedNode.property, "Identifier")) return false;
|
|
29603
|
-
return unwrappedNode.property.name === "from";
|
|
29604
|
-
};
|
|
29605
|
-
const isConstAliasOfGlobalArrayFrom = (node, scopes) => {
|
|
29606
|
-
const unwrappedNode = stripParenExpression(node);
|
|
29607
|
-
if (!isNodeOfType(unwrappedNode, "Identifier")) return false;
|
|
29608
|
-
const binding = findVariableInitializer(unwrappedNode, unwrappedNode.name);
|
|
29609
|
-
if (!binding?.initializer) return false;
|
|
29610
|
-
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
29611
|
-
return Boolean(declarator && scopes.symbolFor(unwrappedNode)?.declarationNode === declarator && declarator.init && declarator.parent && isNodeOfType(declarator.parent, "VariableDeclaration") && declarator.parent.kind === "const" && isGlobalArrayFromMember(declarator.init, scopes));
|
|
29612
|
-
};
|
|
29613
|
-
const executesDuringRender = (functionNode, scopes) => {
|
|
29614
|
-
const parent = functionNode.parent;
|
|
29615
|
-
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
29616
|
-
if (parent.callee === functionNode) return true;
|
|
29617
|
-
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;
|
|
29618
|
-
if (scopes && parent.arguments?.[1] === functionNode && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
|
|
29619
|
-
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;
|
|
29620
|
-
};
|
|
29621
|
-
//#endregion
|
|
29622
30542
|
//#region src/plugin/utils/find-enclosing-jsx-opening-element.ts
|
|
29623
30543
|
const findEnclosingJsxOpeningElement = (node) => {
|
|
29624
30544
|
let cursor = node.parent;
|
|
@@ -29630,17 +30550,6 @@ const findEnclosingJsxOpeningElement = (node) => {
|
|
|
29630
30550
|
return null;
|
|
29631
30551
|
};
|
|
29632
30552
|
//#endregion
|
|
29633
|
-
//#region src/plugin/utils/find-render-phase-component-or-hook.ts
|
|
29634
|
-
const findRenderPhaseComponentOrHook = (node, scopes) => {
|
|
29635
|
-
let functionNode = findEnclosingFunction(node);
|
|
29636
|
-
while (functionNode) {
|
|
29637
|
-
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
29638
|
-
if (!executesDuringRender(functionNode, scopes)) return null;
|
|
29639
|
-
functionNode = findEnclosingFunction(functionNode);
|
|
29640
|
-
}
|
|
29641
|
-
return null;
|
|
29642
|
-
};
|
|
29643
|
-
//#endregion
|
|
29644
30553
|
//#region src/plugin/utils/has-client-render-evidence.ts
|
|
29645
30554
|
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
29646
30555
|
if (fileHasUseClientDirective) return true;
|
|
@@ -29749,9 +30658,6 @@ const isGatedByFalsyInitialState = (node, scopes) => {
|
|
|
29749
30658
|
return false;
|
|
29750
30659
|
};
|
|
29751
30660
|
//#endregion
|
|
29752
|
-
//#region src/plugin/utils/is-event-handler-attribute.ts
|
|
29753
|
-
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
|
|
29754
|
-
//#endregion
|
|
29755
30661
|
//#region src/plugin/rules/performance/no-hydration-branch-on-browser-global.ts
|
|
29756
30662
|
const evaluateEquality = (operator, left, right) => {
|
|
29757
30663
|
if (operator === "===" || operator === "==") return left === right;
|
|
@@ -30052,6 +30958,131 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
30052
30958
|
} })
|
|
30053
30959
|
});
|
|
30054
30960
|
//#endregion
|
|
30961
|
+
//#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
|
|
30962
|
+
const TIMER_FUNCTION_NAMES = new Set([
|
|
30963
|
+
"cancelAnimationFrame",
|
|
30964
|
+
"clearInterval",
|
|
30965
|
+
"clearTimeout",
|
|
30966
|
+
"queueMicrotask",
|
|
30967
|
+
"requestAnimationFrame",
|
|
30968
|
+
"setInterval",
|
|
30969
|
+
"setTimeout"
|
|
30970
|
+
]);
|
|
30971
|
+
const STORAGE_MUTATION_METHOD_NAMES = new Set([
|
|
30972
|
+
"clear",
|
|
30973
|
+
"removeItem",
|
|
30974
|
+
"setItem"
|
|
30975
|
+
]);
|
|
30976
|
+
const STORAGE_RECEIVER_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
30977
|
+
const EXTERNAL_READ_METHOD_NAMES = new Set(["getBoundingClientRect", "getClientRects"]);
|
|
30978
|
+
const NOTIFICATION_RECEIVER_NAMES = new Set([
|
|
30979
|
+
"message",
|
|
30980
|
+
"notification",
|
|
30981
|
+
"toast"
|
|
30982
|
+
]);
|
|
30983
|
+
const NOTIFICATION_METHOD_NAMES = new Set([
|
|
30984
|
+
"error",
|
|
30985
|
+
"info",
|
|
30986
|
+
"loading",
|
|
30987
|
+
"open",
|
|
30988
|
+
"show",
|
|
30989
|
+
"success",
|
|
30990
|
+
"warning"
|
|
30991
|
+
]);
|
|
30992
|
+
const getMemberCall = (node) => {
|
|
30993
|
+
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
30994
|
+
if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
|
|
30995
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
30996
|
+
return {
|
|
30997
|
+
methodName: node.callee.property.name,
|
|
30998
|
+
receiver: stripParenExpression(node.callee.object)
|
|
30999
|
+
};
|
|
31000
|
+
};
|
|
31001
|
+
const isNotificationReceiver = (receiver, scopes) => {
|
|
31002
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
31003
|
+
if (!NOTIFICATION_RECEIVER_NAMES.has(receiver.name)) return false;
|
|
31004
|
+
const symbol = scopes.symbolFor(receiver);
|
|
31005
|
+
if (symbol?.kind === "import") return true;
|
|
31006
|
+
if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
|
|
31007
|
+
const callee = symbol.initializer.callee;
|
|
31008
|
+
return isNodeOfType(callee, "Identifier") && /^use(?:Message|Notification|Toast)$/.test(callee.name);
|
|
31009
|
+
};
|
|
31010
|
+
const getKnownImpureCall = (callExpression, scopes) => {
|
|
31011
|
+
if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
|
|
31012
|
+
const memberCall = getMemberCall(callExpression);
|
|
31013
|
+
if (!memberCall) return null;
|
|
31014
|
+
const { methodName, receiver } = memberCall;
|
|
31015
|
+
if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && STORAGE_RECEIVER_NAMES.has(receiver.name) && scopes.isGlobalReference(receiver)) return `${receiver.name}.${methodName}()`;
|
|
31016
|
+
if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
|
|
31017
|
+
if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && isNotificationReceiver(receiver, scopes)) return `${receiver.name}.${methodName}()`;
|
|
31018
|
+
return null;
|
|
31019
|
+
};
|
|
31020
|
+
const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
|
|
31021
|
+
let rootIdentifier = null;
|
|
31022
|
+
if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
|
|
31023
|
+
else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
|
|
31024
|
+
if (!rootIdentifier) return null;
|
|
31025
|
+
const updaterScope = scopes.ownScopeFor(updater);
|
|
31026
|
+
if (!updaterScope) return null;
|
|
31027
|
+
const symbol = scopes.symbolFor(rootIdentifier);
|
|
31028
|
+
if (!symbol) return `the external value "${rootIdentifier.name}"`;
|
|
31029
|
+
if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
|
|
31030
|
+
return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
|
|
31031
|
+
};
|
|
31032
|
+
const findImpureUpdaterOperation = (updater, scopes) => {
|
|
31033
|
+
const analysis = getProgramAnalysis(updater);
|
|
31034
|
+
let operation = null;
|
|
31035
|
+
walkAst(updater, (child) => {
|
|
31036
|
+
if (operation) return false;
|
|
31037
|
+
if (child !== updater && isFunctionLike$1(child) && !executesDuringRender(child, scopes)) return false;
|
|
31038
|
+
if (isNodeOfType(child, "CallExpression")) {
|
|
31039
|
+
if (isNodeOfType(child.callee, "Identifier") && analysis) {
|
|
31040
|
+
const calleeReference = getRef(analysis, child.callee);
|
|
31041
|
+
if (calleeReference && isStateSetterCall(analysis, calleeReference)) {
|
|
31042
|
+
operation = `the nested state update "${child.callee.name}()"`;
|
|
31043
|
+
return false;
|
|
31044
|
+
}
|
|
31045
|
+
}
|
|
31046
|
+
const impureCall = getKnownImpureCall(child, scopes);
|
|
31047
|
+
if (impureCall) {
|
|
31048
|
+
operation = impureCall;
|
|
31049
|
+
return false;
|
|
31050
|
+
}
|
|
31051
|
+
}
|
|
31052
|
+
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
31053
|
+
operation = getExternalAssignmentDescription(child.left, updater, scopes);
|
|
31054
|
+
if (operation) return false;
|
|
31055
|
+
}
|
|
31056
|
+
if (isNodeOfType(child, "UpdateExpression")) {
|
|
31057
|
+
operation = getExternalAssignmentDescription(child.argument, updater, scopes);
|
|
31058
|
+
if (operation) return false;
|
|
31059
|
+
}
|
|
31060
|
+
});
|
|
31061
|
+
return operation;
|
|
31062
|
+
};
|
|
31063
|
+
const noImpureStateUpdater = defineRule({
|
|
31064
|
+
id: "no-impure-state-updater",
|
|
31065
|
+
title: "State updater has side effects",
|
|
31066
|
+
severity: "error",
|
|
31067
|
+
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.",
|
|
31068
|
+
create: (context) => ({ CallExpression(node) {
|
|
31069
|
+
const updater = node.arguments?.[0];
|
|
31070
|
+
if (!updater || !isFunctionLike$1(updater)) return;
|
|
31071
|
+
const analysis = getProgramAnalysis(node);
|
|
31072
|
+
if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
|
|
31073
|
+
const calleeReference = getRef(analysis, node.callee);
|
|
31074
|
+
if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
|
|
31075
|
+
const stateDeclarator = getUseStateDecl(analysis, calleeReference);
|
|
31076
|
+
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
|
|
31077
|
+
const operation = findImpureUpdaterOperation(updater, context.scopes);
|
|
31078
|
+
if (!operation) return;
|
|
31079
|
+
context.report({
|
|
31080
|
+
node: updater,
|
|
31081
|
+
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.`
|
|
31082
|
+
});
|
|
31083
|
+
} })
|
|
31084
|
+
});
|
|
31085
|
+
//#endregion
|
|
30055
31086
|
//#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
|
|
30056
31087
|
const MESSAGE$21 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
|
|
30057
31088
|
const REACT_USE_REF_OPTIONS = {
|
|
@@ -30218,7 +31249,7 @@ const noInitializeState = defineRule({
|
|
|
30218
31249
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
30219
31250
|
const analysis = getProgramAnalysis(node);
|
|
30220
31251
|
if (!analysis) return;
|
|
30221
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
31252
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
30222
31253
|
if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
|
|
30223
31254
|
const stateName = getStateName(fact.stateDeclarator);
|
|
30224
31255
|
context.report({
|
|
@@ -31850,6 +32881,41 @@ const noMutableInDeps = defineRule({
|
|
|
31850
32881
|
}
|
|
31851
32882
|
});
|
|
31852
32883
|
//#endregion
|
|
32884
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
32885
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
32886
|
+
let node = callExpression;
|
|
32887
|
+
let parent = node.parent;
|
|
32888
|
+
while (parent) {
|
|
32889
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
32890
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
32891
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
32892
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
32893
|
+
node = parent;
|
|
32894
|
+
parent = node.parent;
|
|
32895
|
+
continue;
|
|
32896
|
+
}
|
|
32897
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
32898
|
+
node = parent;
|
|
32899
|
+
parent = node.parent;
|
|
32900
|
+
continue;
|
|
32901
|
+
}
|
|
32902
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
32903
|
+
node = parent;
|
|
32904
|
+
parent = node.parent;
|
|
32905
|
+
continue;
|
|
32906
|
+
}
|
|
32907
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
32908
|
+
const expressions = parent.expressions ?? [];
|
|
32909
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
32910
|
+
node = parent;
|
|
32911
|
+
parent = node.parent;
|
|
32912
|
+
continue;
|
|
32913
|
+
}
|
|
32914
|
+
return false;
|
|
32915
|
+
}
|
|
32916
|
+
return false;
|
|
32917
|
+
};
|
|
32918
|
+
//#endregion
|
|
31853
32919
|
//#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
|
|
31854
32920
|
const LODASH_MUTATOR_NAMES = new Set([
|
|
31855
32921
|
"set",
|
|
@@ -33140,7 +34206,7 @@ const isUseRefIdentifier = (identifier) => {
|
|
|
33140
34206
|
};
|
|
33141
34207
|
const COMMAND_PROP_NAME_PATTERN = /^(fetch|load|refetch|dispatch|register|render)([A-Z_]|$)/;
|
|
33142
34208
|
const SETTER_NAMED_PROP_PATTERN = /^set[A-Z]/;
|
|
33143
|
-
const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
34209
|
+
const unwrapChainExpression$1 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
33144
34210
|
const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
|
|
33145
34211
|
"useCallback",
|
|
33146
34212
|
"useMemo",
|
|
@@ -33175,7 +34241,7 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
33175
34241
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
33176
34242
|
const node = def.node;
|
|
33177
34243
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
33178
|
-
const initializer = unwrapChainExpression(node.init);
|
|
34244
|
+
const initializer = unwrapChainExpression$1(node.init);
|
|
33179
34245
|
const wrappedFunction = getWrapperHookWrappedFunction(initializer);
|
|
33180
34246
|
if (wrappedFunction) {
|
|
33181
34247
|
if (wrappedFunction.async) return false;
|
|
@@ -33185,10 +34251,170 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
33185
34251
|
return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
|
|
33186
34252
|
}));
|
|
33187
34253
|
};
|
|
34254
|
+
const getDeclarationKind = (declarator) => {
|
|
34255
|
+
const declaration = declarator.parent;
|
|
34256
|
+
return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
|
|
34257
|
+
};
|
|
34258
|
+
const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
34259
|
+
const getDestructuredPropertyName = (bindingIdentifier) => {
|
|
34260
|
+
const property = bindingIdentifier.parent;
|
|
34261
|
+
if (!property || !isNodeOfType(property, "Property")) return null;
|
|
34262
|
+
if (property.computed) return isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" ? String(property.key.value) : null;
|
|
34263
|
+
return isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
34264
|
+
};
|
|
34265
|
+
const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
|
|
34266
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
34267
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
34268
|
+
const callbackReference = getRef(analysis, unwrappedExpression);
|
|
34269
|
+
const callbackVariable = callbackReference?.resolved;
|
|
34270
|
+
if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable)) return null;
|
|
34271
|
+
if (isProp(analysis, callbackReference)) {
|
|
34272
|
+
if (isWholePropsObjectReference(analysis, callbackReference)) return null;
|
|
34273
|
+
const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
|
|
34274
|
+
return (bindingIdentifier && getDestructuredPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
|
|
34275
|
+
}
|
|
34276
|
+
if (hasMutableBindingWrite(callbackReference)) return null;
|
|
34277
|
+
const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
34278
|
+
if (definitions.length !== 1) return null;
|
|
34279
|
+
const declarator = definitions[0];
|
|
34280
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || getDeclarationKind(declarator) !== "const" || !declarator.init) return null;
|
|
34281
|
+
visitedVariables.add(callbackVariable);
|
|
34282
|
+
if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
34283
|
+
const bindingIdentifier = callbackVariable.defs[0]?.name;
|
|
34284
|
+
const propertyName = bindingIdentifier ? getDestructuredPropertyName(bindingIdentifier) : null;
|
|
34285
|
+
const propsReference = getDownstreamRefs(analysis, declarator.init).find((candidateReference) => isWholePropsObjectReference(analysis, candidateReference));
|
|
34286
|
+
return propertyName && propsReference ? propertyName : null;
|
|
34287
|
+
}
|
|
34288
|
+
if (!isNodeOfType(declarator.id, "Identifier")) return null;
|
|
34289
|
+
return getParentCallbackPropName(analysis, declarator.init, visitedVariables);
|
|
34290
|
+
}
|
|
34291
|
+
if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
|
|
34292
|
+
const callbackName = getStaticMemberPropertyName(unwrappedExpression);
|
|
34293
|
+
if (!callbackName) return null;
|
|
34294
|
+
const receiver = stripParenExpression(unwrappedExpression.object);
|
|
34295
|
+
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
34296
|
+
const receiverReference = getRef(analysis, receiver);
|
|
34297
|
+
if (!receiverReference || !isWholePropsObjectReference(analysis, receiverReference)) return null;
|
|
34298
|
+
return callbackName;
|
|
34299
|
+
};
|
|
34300
|
+
const isNullishRefInitializer = (initializer) => {
|
|
34301
|
+
if (!initializer) return true;
|
|
34302
|
+
const unwrappedInitializer = stripParenExpression(initializer);
|
|
34303
|
+
if (isNodeOfType(unwrappedInitializer, "Literal")) return unwrappedInitializer.value === null;
|
|
34304
|
+
if (!isNodeOfType(unwrappedInitializer, "Identifier")) return false;
|
|
34305
|
+
return unwrappedInitializer.name === "undefined";
|
|
34306
|
+
};
|
|
34307
|
+
const getDirectComponentBodyStatement = (node, componentBody) => {
|
|
34308
|
+
let current = node;
|
|
34309
|
+
while (current?.parent && current.parent !== componentBody) current = current.parent;
|
|
34310
|
+
return current?.parent === componentBody ? current : null;
|
|
34311
|
+
};
|
|
34312
|
+
const getVariableForDeclarator = (analysis, declarator) => {
|
|
34313
|
+
for (const scope of analysis.scopeManager.scopes) {
|
|
34314
|
+
const variable = scope.variables.find((candidateVariable) => candidateVariable.defs.some((definition) => definition.node === declarator));
|
|
34315
|
+
if (variable) return variable;
|
|
34316
|
+
}
|
|
34317
|
+
return null;
|
|
34318
|
+
};
|
|
34319
|
+
const getRefMember = (identifier) => {
|
|
34320
|
+
const receiver = findTransparentExpressionRoot(identifier);
|
|
34321
|
+
const member = receiver.parent;
|
|
34322
|
+
if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== receiver) return null;
|
|
34323
|
+
return member;
|
|
34324
|
+
};
|
|
34325
|
+
const getRefMemberAssignment = (identifier) => {
|
|
34326
|
+
const member = getRefMember(identifier);
|
|
34327
|
+
if (!member) return null;
|
|
34328
|
+
const memberRoot = findTransparentExpressionRoot(member);
|
|
34329
|
+
const assignment = memberRoot.parent;
|
|
34330
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== memberRoot) return null;
|
|
34331
|
+
return assignment;
|
|
34332
|
+
};
|
|
34333
|
+
const getRefAliasDeclarator = (identifier) => {
|
|
34334
|
+
const initializer = findTransparentExpressionRoot(identifier);
|
|
34335
|
+
const declarator = initializer.parent;
|
|
34336
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== initializer || !isNodeOfType(declarator.id, "Identifier") || getDeclarationKind(declarator) !== "const") return null;
|
|
34337
|
+
return declarator;
|
|
34338
|
+
};
|
|
34339
|
+
const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
|
|
34340
|
+
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
34341
|
+
const receiverReference = getRef(analysis, receiver);
|
|
34342
|
+
if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
|
|
34343
|
+
const variables = /* @__PURE__ */ new Set();
|
|
34344
|
+
let currentVariable = receiverReference.resolved;
|
|
34345
|
+
let refCall = null;
|
|
34346
|
+
while (currentVariable && !variables.has(currentVariable)) {
|
|
34347
|
+
variables.add(currentVariable);
|
|
34348
|
+
const definitions = currentVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
34349
|
+
if (definitions.length !== 1) return null;
|
|
34350
|
+
const declarator = definitions[0];
|
|
34351
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) return null;
|
|
34352
|
+
if (isNodeOfType(declarator.init, "CallExpression") && isReactUseRefCall(declarator.init)) {
|
|
34353
|
+
refCall = declarator.init;
|
|
34354
|
+
break;
|
|
34355
|
+
}
|
|
34356
|
+
if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
|
|
34357
|
+
const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
|
|
34358
|
+
if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
|
|
34359
|
+
currentVariable = upstreamReference.resolved;
|
|
34360
|
+
}
|
|
34361
|
+
if (!refCall) return null;
|
|
34362
|
+
const pendingVariables = [...variables];
|
|
34363
|
+
while (pendingVariables.length > 0) {
|
|
34364
|
+
const variable = pendingVariables.pop();
|
|
34365
|
+
if (!variable) continue;
|
|
34366
|
+
for (const candidateReference of variable.references) {
|
|
34367
|
+
const aliasDeclarator = getRefAliasDeclarator(candidateReference.identifier);
|
|
34368
|
+
if (!aliasDeclarator) continue;
|
|
34369
|
+
const aliasVariable = getVariableForDeclarator(analysis, aliasDeclarator);
|
|
34370
|
+
if (!aliasVariable || variables.has(aliasVariable)) continue;
|
|
34371
|
+
if (aliasVariable.references.some((aliasReference) => aliasReference.isWrite() && !aliasReference.init)) return null;
|
|
34372
|
+
variables.add(aliasVariable);
|
|
34373
|
+
pendingVariables.push(aliasVariable);
|
|
34374
|
+
}
|
|
34375
|
+
}
|
|
34376
|
+
return {
|
|
34377
|
+
refCall,
|
|
34378
|
+
variables
|
|
34379
|
+
};
|
|
34380
|
+
};
|
|
34381
|
+
const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactUseRefCall) => {
|
|
34382
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
34383
|
+
if (!isNodeOfType(callee, "MemberExpression") || getStaticMemberPropertyName(callee) !== "current") return null;
|
|
34384
|
+
const bindingProvenance = getRefBindingProvenance(analysis, stripParenExpression(callee.object), isReactUseRefCall);
|
|
34385
|
+
if (!bindingProvenance) return null;
|
|
34386
|
+
const { refCall, variables } = bindingProvenance;
|
|
34387
|
+
const componentFunction = findEnclosingFunction(effectCall);
|
|
34388
|
+
if (!componentFunction || !isFunctionLike$1(componentFunction) || !isComponentFunction$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
|
|
34389
|
+
if (!getDirectComponentBodyStatement(effectCall, componentFunction.body)) return null;
|
|
34390
|
+
const callbackPropNames = /* @__PURE__ */ new Set();
|
|
34391
|
+
const initializer = refCall.arguments?.[0];
|
|
34392
|
+
const initializerCallbackName = initializer ? getParentCallbackPropName(analysis, initializer) : null;
|
|
34393
|
+
if (initializerCallbackName) callbackPropNames.add(initializerCallbackName);
|
|
34394
|
+
else if (!isNullishRefInitializer(initializer)) return null;
|
|
34395
|
+
for (const variable of variables) for (const candidateReference of variable.references) {
|
|
34396
|
+
if (candidateReference.init) continue;
|
|
34397
|
+
if (candidateReference.isWrite()) return null;
|
|
34398
|
+
const identifier = candidateReference.identifier;
|
|
34399
|
+
if (getRefAliasDeclarator(identifier)) continue;
|
|
34400
|
+
const member = getRefMember(identifier);
|
|
34401
|
+
if (!member || getStaticMemberPropertyName(member) !== "current") return null;
|
|
34402
|
+
const memberParent = findTransparentExpressionRoot(member).parent;
|
|
34403
|
+
if (memberParent && (isNodeOfType(memberParent, "UpdateExpression") || isNodeOfType(memberParent, "UnaryExpression") && memberParent.operator === "delete")) return null;
|
|
34404
|
+
const assignment = getRefMemberAssignment(identifier);
|
|
34405
|
+
if (!assignment) continue;
|
|
34406
|
+
const assignmentStatement = findTransparentExpressionRoot(assignment).parent;
|
|
34407
|
+
if (assignment.operator !== "=" || !assignmentStatement || !isNodeOfType(assignmentStatement, "ExpressionStatement") || assignmentStatement.parent !== componentFunction.body) return null;
|
|
34408
|
+
const assignedCallbackName = getParentCallbackPropName(analysis, assignment.right);
|
|
34409
|
+
if (!assignedCallbackName) return null;
|
|
34410
|
+
callbackPropNames.add(assignedCallbackName);
|
|
34411
|
+
}
|
|
34412
|
+
return callbackPropNames.size > 0 ? { callbackPropNames } : null;
|
|
34413
|
+
};
|
|
33188
34414
|
const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
33189
34415
|
const node = def.node;
|
|
33190
34416
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
33191
|
-
return getWrapperHookWrappedFunction(unwrapChainExpression(node.init)) !== null;
|
|
34417
|
+
return getWrapperHookWrappedFunction(unwrapChainExpression$1(node.init)) !== null;
|
|
33192
34418
|
}));
|
|
33193
34419
|
const isHandlerBagArgument = (analysis, argument) => {
|
|
33194
34420
|
if (!isNodeOfType(argument, "ObjectExpression")) return false;
|
|
@@ -33210,7 +34436,7 @@ const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
|
|
|
33210
34436
|
const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
33211
34437
|
const node = def.node;
|
|
33212
34438
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
33213
|
-
const init = unwrapChainExpression(node.init);
|
|
34439
|
+
const init = unwrapChainExpression$1(node.init);
|
|
33214
34440
|
if (!isNodeOfType(init, "CallExpression")) return false;
|
|
33215
34441
|
const callee = init.callee;
|
|
33216
34442
|
if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
|
|
@@ -33240,71 +34466,79 @@ const noPassDataToParent = defineRule({
|
|
|
33240
34466
|
severity: "warn",
|
|
33241
34467
|
tags: ["test-noise"],
|
|
33242
34468
|
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",
|
|
33243
|
-
create: (context) =>
|
|
33244
|
-
|
|
33245
|
-
|
|
33246
|
-
|
|
33247
|
-
|
|
33248
|
-
|
|
33249
|
-
|
|
33250
|
-
|
|
33251
|
-
|
|
33252
|
-
|
|
33253
|
-
const
|
|
33254
|
-
if (!
|
|
33255
|
-
|
|
33256
|
-
if (!
|
|
33257
|
-
const
|
|
33258
|
-
|
|
33259
|
-
|
|
33260
|
-
|
|
33261
|
-
if (
|
|
33262
|
-
|
|
33263
|
-
|
|
33264
|
-
|
|
33265
|
-
|
|
33266
|
-
|
|
33267
|
-
|
|
33268
|
-
|
|
33269
|
-
|
|
33270
|
-
|
|
33271
|
-
|
|
33272
|
-
|
|
33273
|
-
|
|
33274
|
-
|
|
33275
|
-
|
|
33276
|
-
|
|
33277
|
-
|
|
33278
|
-
|
|
33279
|
-
|
|
33280
|
-
|
|
33281
|
-
|
|
33282
|
-
|
|
33283
|
-
|
|
33284
|
-
|
|
33285
|
-
|
|
33286
|
-
|
|
33287
|
-
|
|
33288
|
-
|
|
33289
|
-
|
|
33290
|
-
|
|
33291
|
-
|
|
33292
|
-
|
|
33293
|
-
|
|
33294
|
-
if (
|
|
33295
|
-
if (
|
|
33296
|
-
|
|
33297
|
-
|
|
33298
|
-
|
|
33299
|
-
|
|
33300
|
-
|
|
33301
|
-
|
|
33302
|
-
|
|
33303
|
-
|
|
33304
|
-
|
|
33305
|
-
|
|
33306
|
-
|
|
33307
|
-
|
|
34469
|
+
create: (context) => {
|
|
34470
|
+
const isReactUseRefCall = (node) => isReactApiCall(node, "useRef", context.scopes, {
|
|
34471
|
+
allowGlobalReactNamespace: true,
|
|
34472
|
+
allowUnboundBareCalls: true
|
|
34473
|
+
});
|
|
34474
|
+
return { CallExpression(node) {
|
|
34475
|
+
if (!isUseEffect(node)) return;
|
|
34476
|
+
const analysis = getProgramAnalysis(node);
|
|
34477
|
+
if (!analysis) return;
|
|
34478
|
+
if (hasCleanup(analysis, node)) return;
|
|
34479
|
+
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
34480
|
+
if (!effectFnRefs) return;
|
|
34481
|
+
const effectFn = getEffectFn(analysis, node);
|
|
34482
|
+
if (!effectFn) return;
|
|
34483
|
+
for (const ref of effectFnRefs) {
|
|
34484
|
+
const callExpr = getCallExpr(ref);
|
|
34485
|
+
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
34486
|
+
const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall);
|
|
34487
|
+
if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
|
|
34488
|
+
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
34489
|
+
const calleeNode = unwrapChainExpression$1(callExpr.callee);
|
|
34490
|
+
const identifier = ref.identifier;
|
|
34491
|
+
if (callbackRefProvenance) {
|
|
34492
|
+
if ([...callbackRefProvenance.callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
|
|
34493
|
+
} else if (calleeNode === identifier) {
|
|
34494
|
+
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
34495
|
+
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
34496
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
34497
|
+
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
34498
|
+
if (isCustomHookParameter(ref)) continue;
|
|
34499
|
+
} else continue;
|
|
34500
|
+
const methodName = getCallMethodName(calleeNode);
|
|
34501
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
34502
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
34503
|
+
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
34504
|
+
if (!callbackRefProvenance && isNamespacedApiCallee(calleeNode)) continue;
|
|
34505
|
+
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) ?? ""));
|
|
34506
|
+
const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
|
|
34507
|
+
const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
|
|
34508
|
+
if (isFunctionLike$1(argument)) {
|
|
34509
|
+
if (!isSetterNamedCallee) return [];
|
|
34510
|
+
return getFunctionalUpdaterDataRefs(analysis, argument);
|
|
34511
|
+
}
|
|
34512
|
+
if (isHandlerBagArgument(analysis, argument)) return [];
|
|
34513
|
+
if (isParentWiredHookResultArgument(analysis, argument)) return [];
|
|
34514
|
+
if (isNodeOfType(argument, "Identifier")) {
|
|
34515
|
+
const argumentRef = getRef(analysis, argument);
|
|
34516
|
+
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
34517
|
+
}
|
|
34518
|
+
return getDownstreamRefs(analysis, argument);
|
|
34519
|
+
}).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
34520
|
+
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
34521
|
+
if (!argsUpstreamRefs.some((argRef) => {
|
|
34522
|
+
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
34523
|
+
if (isProp(analysis, argRef)) return false;
|
|
34524
|
+
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
34525
|
+
if (isRefCurrent(argRef)) return false;
|
|
34526
|
+
if (isConstant(argRef)) return false;
|
|
34527
|
+
if (isParentWiredHookResultRef(analysis, argRef)) return false;
|
|
34528
|
+
if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
|
|
34529
|
+
if (resolvesToFunctionBinding(argRef)) return false;
|
|
34530
|
+
const argIdentifier = argRef.identifier;
|
|
34531
|
+
if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
|
|
34532
|
+
if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
|
|
34533
|
+
return true;
|
|
34534
|
+
})) continue;
|
|
34535
|
+
context.report({
|
|
34536
|
+
node: callExpr,
|
|
34537
|
+
message: "Handing data back to a parent from a useEffect costs your users an extra render."
|
|
34538
|
+
});
|
|
34539
|
+
}
|
|
34540
|
+
} };
|
|
34541
|
+
}
|
|
33308
34542
|
});
|
|
33309
34543
|
//#endregion
|
|
33310
34544
|
//#region src/plugin/utils/is-call-result-consumed-as-argument.ts
|
|
@@ -33858,6 +35092,66 @@ const noPropCallbackInEffect = defineRule({
|
|
|
33858
35092
|
}
|
|
33859
35093
|
});
|
|
33860
35094
|
//#endregion
|
|
35095
|
+
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-render.ts
|
|
35096
|
+
const isPreservedThroughConciseArrow = (callExpression, scopes) => {
|
|
35097
|
+
let node = callExpression;
|
|
35098
|
+
let parent = node.parent;
|
|
35099
|
+
while (parent) {
|
|
35100
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
35101
|
+
node = parent;
|
|
35102
|
+
parent = node.parent;
|
|
35103
|
+
continue;
|
|
35104
|
+
}
|
|
35105
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
35106
|
+
node = parent;
|
|
35107
|
+
parent = node.parent;
|
|
35108
|
+
continue;
|
|
35109
|
+
}
|
|
35110
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
35111
|
+
node = parent;
|
|
35112
|
+
parent = node.parent;
|
|
35113
|
+
continue;
|
|
35114
|
+
}
|
|
35115
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
35116
|
+
const expressions = parent.expressions ?? [];
|
|
35117
|
+
if (expressions[expressions.length - 1] !== node) return false;
|
|
35118
|
+
node = parent;
|
|
35119
|
+
parent = node.parent;
|
|
35120
|
+
continue;
|
|
35121
|
+
}
|
|
35122
|
+
if (!isNodeOfType(parent, "ArrowFunctionExpression") || parent.body !== node) return !isResultDiscardedCall(node);
|
|
35123
|
+
const invocation = parent.parent;
|
|
35124
|
+
if (!isNodeOfType(invocation, "CallExpression") || !executesDuringRender(parent, scopes)) return true;
|
|
35125
|
+
if (invocation.arguments?.[0] === parent || invocation.arguments?.[1] === parent) {
|
|
35126
|
+
const callee = stripParenExpression(invocation.callee);
|
|
35127
|
+
return !(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "forEach" && invocation.arguments[0] === parent);
|
|
35128
|
+
}
|
|
35129
|
+
node = invocation;
|
|
35130
|
+
parent = node.parent;
|
|
35131
|
+
}
|
|
35132
|
+
return false;
|
|
35133
|
+
};
|
|
35134
|
+
const noPropCallbackInRender = defineRule({
|
|
35135
|
+
id: "no-prop-callback-in-render",
|
|
35136
|
+
title: "Prop callback invoked during render",
|
|
35137
|
+
severity: "error",
|
|
35138
|
+
recommendation: "Invoke the callback from the event or asynchronous operation that produced the value, or from an effect when synchronizing with an external system. Render must stay pure because React can replay or discard it.",
|
|
35139
|
+
create: (context) => ({ CallExpression(node) {
|
|
35140
|
+
if (!isResultDiscardedCall(node)) return;
|
|
35141
|
+
if (isPreservedThroughConciseArrow(node, context.scopes)) return;
|
|
35142
|
+
if (!findRenderPhaseComponentOrHook(node, context.scopes)) return;
|
|
35143
|
+
const analysis = getProgramAnalysis(node);
|
|
35144
|
+
if (!analysis) return;
|
|
35145
|
+
const callee = stripParenExpression(node.callee);
|
|
35146
|
+
if (isFunctionLike$1(callee)) return;
|
|
35147
|
+
if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return;
|
|
35148
|
+
context.report({
|
|
35149
|
+
node,
|
|
35150
|
+
message: "This prop callback runs during render. React can replay or discard render work, so the callback can fire more than once or for UI that never commits."
|
|
35151
|
+
});
|
|
35152
|
+
} })
|
|
35153
|
+
});
|
|
35154
|
+
//#endregion
|
|
33861
35155
|
//#region src/plugin/rules/architecture/no-prop-types.ts
|
|
33862
35156
|
const PROP_TYPES_PROPERTY = "propTypes";
|
|
33863
35157
|
const isPropTypesKey = (key, computed) => {
|
|
@@ -34552,6 +35846,63 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
34552
35846
|
}
|
|
34553
35847
|
});
|
|
34554
35848
|
//#endregion
|
|
35849
|
+
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
35850
|
+
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
35851
|
+
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
35852
|
+
if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
35853
|
+
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
35854
|
+
if (!symbol?.initializer) return null;
|
|
35855
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
35856
|
+
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
35857
|
+
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
35858
|
+
};
|
|
35859
|
+
const isSameRefCurrentMember = (node, refSymbol, scopes) => {
|
|
35860
|
+
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
|
|
35861
|
+
const receiver = stripParenExpression(node.object);
|
|
35862
|
+
return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
|
|
35863
|
+
};
|
|
35864
|
+
const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
|
|
35865
|
+
if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
|
|
35866
|
+
let descendant = assignmentExpression;
|
|
35867
|
+
let ancestor = descendant.parent;
|
|
35868
|
+
while (ancestor) {
|
|
35869
|
+
if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
|
|
35870
|
+
const { left, right } = ancestor.test;
|
|
35871
|
+
if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
|
|
35872
|
+
}
|
|
35873
|
+
descendant = ancestor;
|
|
35874
|
+
ancestor = descendant.parent;
|
|
35875
|
+
}
|
|
35876
|
+
return false;
|
|
35877
|
+
};
|
|
35878
|
+
const noRefCurrentInRender = defineRule({
|
|
35879
|
+
id: "no-ref-current-in-render",
|
|
35880
|
+
title: "Ref mutated during render",
|
|
35881
|
+
severity: "error",
|
|
35882
|
+
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.",
|
|
35883
|
+
create: (context) => {
|
|
35884
|
+
const report = (memberExpression) => {
|
|
35885
|
+
if (!resolveReactRefSymbol(memberExpression, context.scopes)) return;
|
|
35886
|
+
if (!findRenderPhaseComponentOrHook(memberExpression, context.scopes)) return;
|
|
35887
|
+
context.report({
|
|
35888
|
+
node: memberExpression,
|
|
35889
|
+
message: "This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits."
|
|
35890
|
+
});
|
|
35891
|
+
};
|
|
35892
|
+
return {
|
|
35893
|
+
AssignmentExpression(node) {
|
|
35894
|
+
const refSymbol = resolveReactRefSymbol(node.left, context.scopes);
|
|
35895
|
+
if (!refSymbol) return;
|
|
35896
|
+
if (isDocumentedLazyInitialization(node, refSymbol, context.scopes)) return;
|
|
35897
|
+
report(node.left);
|
|
35898
|
+
},
|
|
35899
|
+
UpdateExpression(node) {
|
|
35900
|
+
report(node.argument);
|
|
35901
|
+
}
|
|
35902
|
+
};
|
|
35903
|
+
}
|
|
35904
|
+
});
|
|
35905
|
+
//#endregion
|
|
34555
35906
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
34556
35907
|
const isInsideComponentContext = (node) => {
|
|
34557
35908
|
let cursor = node.parent;
|
|
@@ -40542,6 +41893,123 @@ const preferUseEffectEvent = defineRule({
|
|
|
40542
41893
|
}
|
|
40543
41894
|
});
|
|
40544
41895
|
//#endregion
|
|
41896
|
+
//#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
|
|
41897
|
+
const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
41898
|
+
"each",
|
|
41899
|
+
"every",
|
|
41900
|
+
"filter",
|
|
41901
|
+
"find",
|
|
41902
|
+
"findIndex",
|
|
41903
|
+
"findLast",
|
|
41904
|
+
"findLastIndex",
|
|
41905
|
+
"flatMap",
|
|
41906
|
+
"forEach",
|
|
41907
|
+
"map",
|
|
41908
|
+
"reduce",
|
|
41909
|
+
"reduceRight",
|
|
41910
|
+
"some",
|
|
41911
|
+
"sort",
|
|
41912
|
+
"toSorted"
|
|
41913
|
+
]);
|
|
41914
|
+
const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
41915
|
+
"from",
|
|
41916
|
+
"fromAsync",
|
|
41917
|
+
"groupBy"
|
|
41918
|
+
]);
|
|
41919
|
+
const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
41920
|
+
const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
|
|
41921
|
+
const isListenerRemovalViaNullHandler = (callNode) => {
|
|
41922
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
41923
|
+
const callee = unwrapChainExpression(callNode.callee);
|
|
41924
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
|
|
41925
|
+
};
|
|
41926
|
+
const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
|
|
41927
|
+
"off",
|
|
41928
|
+
"removeEventListener",
|
|
41929
|
+
"removeListener"
|
|
41930
|
+
]);
|
|
41931
|
+
const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
|
|
41932
|
+
if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
|
|
41933
|
+
const handlerArgument = callNode.arguments?.[1];
|
|
41934
|
+
return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
|
|
41935
|
+
};
|
|
41936
|
+
const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
41937
|
+
const callNode = unwrapChainExpression(node);
|
|
41938
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
41939
|
+
if (isListenerRemovalViaNullHandler(callNode)) return true;
|
|
41940
|
+
const callee = unwrapChainExpression(callNode.callee);
|
|
41941
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
41942
|
+
if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
|
|
41943
|
+
if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
|
|
41944
|
+
if (knownCleanupFunctionNames.has(callee.name)) return true;
|
|
41945
|
+
return false;
|
|
41946
|
+
}
|
|
41947
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
41948
|
+
if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
|
|
41949
|
+
if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
|
|
41950
|
+
return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
|
|
41951
|
+
}
|
|
41952
|
+
return false;
|
|
41953
|
+
};
|
|
41954
|
+
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);
|
|
41955
|
+
const isIteratorCallbackArgument = (node) => {
|
|
41956
|
+
const parentNode = node.parent;
|
|
41957
|
+
if (!isNodeOfType(parentNode, "CallExpression")) return false;
|
|
41958
|
+
if (!parentNode.arguments?.some((argument) => argument === node)) return false;
|
|
41959
|
+
const callee = unwrapChainExpression(parentNode.callee);
|
|
41960
|
+
if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
|
|
41961
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
|
|
41962
|
+
};
|
|
41963
|
+
const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
41964
|
+
let didFindRelease = false;
|
|
41965
|
+
walkAst(node, (child) => {
|
|
41966
|
+
if (didFindRelease) return false;
|
|
41967
|
+
if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
|
|
41968
|
+
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
41969
|
+
didFindRelease = true;
|
|
41970
|
+
return false;
|
|
41971
|
+
}
|
|
41972
|
+
});
|
|
41973
|
+
return didFindRelease;
|
|
41974
|
+
};
|
|
41975
|
+
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
41976
|
+
if (!isFunctionLike$1(node)) return false;
|
|
41977
|
+
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
41978
|
+
};
|
|
41979
|
+
const isProvablyNoOpCleanupFunction = (node) => {
|
|
41980
|
+
if (!isFunctionLike$1(node)) return false;
|
|
41981
|
+
let sawNoOpRemoval = false;
|
|
41982
|
+
let sawOtherCall = false;
|
|
41983
|
+
walkAst(node.body, (child) => {
|
|
41984
|
+
if (sawOtherCall) return false;
|
|
41985
|
+
if (isFunctionLike$1(child)) return false;
|
|
41986
|
+
const callNode = unwrapChainExpression(child);
|
|
41987
|
+
if (!isNodeOfType(callNode, "CallExpression")) return;
|
|
41988
|
+
const callee = unwrapChainExpression(callNode.callee);
|
|
41989
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
|
|
41990
|
+
sawNoOpRemoval = true;
|
|
41991
|
+
return;
|
|
41992
|
+
}
|
|
41993
|
+
sawOtherCall = true;
|
|
41994
|
+
});
|
|
41995
|
+
return sawNoOpRemoval && !sawOtherCall;
|
|
41996
|
+
};
|
|
41997
|
+
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
|
|
41998
|
+
if (!returnedValue) return false;
|
|
41999
|
+
const unwrappedValue = unwrapChainExpression(returnedValue);
|
|
42000
|
+
if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
|
|
42001
|
+
if (isNodeOfType(unwrappedValue, "Identifier")) {
|
|
42002
|
+
if (unwrappedValue.name === "undefined") return false;
|
|
42003
|
+
if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
|
|
42004
|
+
return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
|
|
42005
|
+
}
|
|
42006
|
+
if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
42007
|
+
if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
|
|
42008
|
+
if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
42009
|
+
if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
|
|
42010
|
+
return false;
|
|
42011
|
+
};
|
|
42012
|
+
//#endregion
|
|
40545
42013
|
//#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
|
|
40546
42014
|
const findUseEffectsInComponent = (componentBody) => {
|
|
40547
42015
|
const effectCalls = [];
|
|
@@ -55397,6 +56865,18 @@ const reactDoctorRules = [
|
|
|
55397
56865
|
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
55398
56866
|
}
|
|
55399
56867
|
},
|
|
56868
|
+
{
|
|
56869
|
+
key: "react-doctor/no-impure-state-updater",
|
|
56870
|
+
id: "no-impure-state-updater",
|
|
56871
|
+
source: "react-doctor",
|
|
56872
|
+
originallyExternal: false,
|
|
56873
|
+
rule: {
|
|
56874
|
+
...noImpureStateUpdater,
|
|
56875
|
+
framework: "global",
|
|
56876
|
+
category: "Bugs",
|
|
56877
|
+
requires: [...new Set(["react", ...noImpureStateUpdater.requires ?? []])]
|
|
56878
|
+
}
|
|
56879
|
+
},
|
|
55400
56880
|
{
|
|
55401
56881
|
key: "react-doctor/no-indeterminate-attribute",
|
|
55402
56882
|
id: "no-indeterminate-attribute",
|
|
@@ -55813,6 +57293,18 @@ const reactDoctorRules = [
|
|
|
55813
57293
|
requires: [...new Set(["react", ...noPropCallbackInEffect.requires ?? []])]
|
|
55814
57294
|
}
|
|
55815
57295
|
},
|
|
57296
|
+
{
|
|
57297
|
+
key: "react-doctor/no-prop-callback-in-render",
|
|
57298
|
+
id: "no-prop-callback-in-render",
|
|
57299
|
+
source: "react-doctor",
|
|
57300
|
+
originallyExternal: false,
|
|
57301
|
+
rule: {
|
|
57302
|
+
...noPropCallbackInRender,
|
|
57303
|
+
framework: "global",
|
|
57304
|
+
category: "Bugs",
|
|
57305
|
+
requires: [...new Set(["react", ...noPropCallbackInRender.requires ?? []])]
|
|
57306
|
+
}
|
|
57307
|
+
},
|
|
55816
57308
|
{
|
|
55817
57309
|
key: "react-doctor/no-prop-types",
|
|
55818
57310
|
id: "no-prop-types",
|
|
@@ -55904,6 +57396,18 @@ const reactDoctorRules = [
|
|
|
55904
57396
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
55905
57397
|
}
|
|
55906
57398
|
},
|
|
57399
|
+
{
|
|
57400
|
+
key: "react-doctor/no-ref-current-in-render",
|
|
57401
|
+
id: "no-ref-current-in-render",
|
|
57402
|
+
source: "react-doctor",
|
|
57403
|
+
originallyExternal: false,
|
|
57404
|
+
rule: {
|
|
57405
|
+
...noRefCurrentInRender,
|
|
57406
|
+
framework: "global",
|
|
57407
|
+
category: "Bugs",
|
|
57408
|
+
requires: [...new Set(["react", ...noRefCurrentInRender.requires ?? []])]
|
|
57409
|
+
}
|
|
57410
|
+
},
|
|
55907
57411
|
{
|
|
55908
57412
|
key: "react-doctor/no-render-in-render",
|
|
55909
57413
|
id: "no-render-in-render",
|
|
@@ -58496,6 +60000,11 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
58496
60000
|
"no-indeterminate-attribute",
|
|
58497
60001
|
"no-locale-format-in-render",
|
|
58498
60002
|
"no-match-media-in-state-initializer",
|
|
60003
|
+
"no-adjust-state-on-prop-change",
|
|
60004
|
+
"no-derived-state",
|
|
60005
|
+
"no-derived-state-effect",
|
|
60006
|
+
"no-event-handler",
|
|
60007
|
+
"no-initialize-state",
|
|
58499
60008
|
"no-mutating-reducer-state",
|
|
58500
60009
|
"no-unguarded-browser-global-in-render-or-hook-init",
|
|
58501
60010
|
"prefer-dynamic-import",
|
|
@@ -58549,6 +60058,9 @@ const collectNextjsSearchParamsDependencies = ({ absoluteFilePath, staticImports
|
|
|
58549
60058
|
if (hasAncestorSuspenseLayout(absoluteFilePath)) return;
|
|
58550
60059
|
for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
|
|
58551
60060
|
};
|
|
60061
|
+
const collectEffectValueHelperDependencies = ({ absoluteFilePath, staticImports }) => {
|
|
60062
|
+
for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
|
|
60063
|
+
};
|
|
58552
60064
|
const collectMutatingReducerDependencies = ({ absoluteFilePath, sourceText, staticImports, program }) => {
|
|
58553
60065
|
const namedUseReducerLocals = /* @__PURE__ */ new Set();
|
|
58554
60066
|
const reactObjectLocals = /* @__PURE__ */ new Set();
|
|
@@ -58635,6 +60147,11 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
58635
60147
|
["no-indeterminate-attribute", collectNearestManifestDependencies],
|
|
58636
60148
|
["no-locale-format-in-render", collectNearestManifestDependencies],
|
|
58637
60149
|
["no-match-media-in-state-initializer", collectNearestManifestDependencies],
|
|
60150
|
+
["no-adjust-state-on-prop-change", collectEffectValueHelperDependencies],
|
|
60151
|
+
["no-derived-state", collectEffectValueHelperDependencies],
|
|
60152
|
+
["no-derived-state-effect", collectEffectValueHelperDependencies],
|
|
60153
|
+
["no-event-handler", collectEffectValueHelperDependencies],
|
|
60154
|
+
["no-initialize-state", collectEffectValueHelperDependencies],
|
|
58638
60155
|
["no-mutating-reducer-state", collectMutatingReducerDependencies],
|
|
58639
60156
|
["no-unguarded-browser-global-in-render-or-hook-init", collectNearestManifestDependencies],
|
|
58640
60157
|
["prefer-dynamic-import", collectNearestManifestDependencies],
|