oxlint-plugin-react-doctor 0.7.4-dev.70b5d99 → 0.7.4-dev.98005f2
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 +139 -1
- package/dist/index.js +2316 -841
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1915,7 +1915,10 @@ const isGeneratedImageRenderContext = (context, node) => {
|
|
|
1915
1915
|
if (!node) return false;
|
|
1916
1916
|
const programRoot = findProgramRoot(node);
|
|
1917
1917
|
if (!programRoot) return false;
|
|
1918
|
-
|
|
1918
|
+
const generatedImageJsxNodes = collectGeneratedImageJsxNodes(programRoot);
|
|
1919
|
+
if (generatedImageJsxNodes.has(node)) return true;
|
|
1920
|
+
if (isNodeOfType(node, "JSXElement")) return generatedImageJsxNodes.has(node.openingElement);
|
|
1921
|
+
return false;
|
|
1919
1922
|
};
|
|
1920
1923
|
//#endregion
|
|
1921
1924
|
//#region src/plugin/utils/object-has-accessible-child.ts
|
|
@@ -8403,6 +8406,54 @@ const effectListenerCleanupMismatch = defineRule({
|
|
|
8403
8406
|
} })
|
|
8404
8407
|
});
|
|
8405
8408
|
//#endregion
|
|
8409
|
+
//#region src/plugin/utils/collect-effect-invoked-functions.ts
|
|
8410
|
+
const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
|
|
8411
|
+
"then",
|
|
8412
|
+
"catch",
|
|
8413
|
+
"finally"
|
|
8414
|
+
]);
|
|
8415
|
+
const collectEffectInvokedFunctions = (effectCallback) => {
|
|
8416
|
+
const invokedFunctions = new Set([effectCallback]);
|
|
8417
|
+
const localFunctionBindings = /* @__PURE__ */ new Map();
|
|
8418
|
+
const calledBindingNames = /* @__PURE__ */ new Set();
|
|
8419
|
+
const pendingFunctions = [effectCallback];
|
|
8420
|
+
const enqueue = (candidate) => {
|
|
8421
|
+
const strippedCandidate = candidate ? stripParenExpression(candidate) : candidate;
|
|
8422
|
+
if (!isFunctionLike$1(strippedCandidate) || invokedFunctions.has(strippedCandidate)) return;
|
|
8423
|
+
invokedFunctions.add(strippedCandidate);
|
|
8424
|
+
pendingFunctions.push(strippedCandidate);
|
|
8425
|
+
};
|
|
8426
|
+
const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
|
|
8427
|
+
while (pendingFunctions.length > 0) {
|
|
8428
|
+
const currentFunction = pendingFunctions.pop();
|
|
8429
|
+
if (!currentFunction) break;
|
|
8430
|
+
walkAst(currentFunction, (child) => {
|
|
8431
|
+
if (child !== currentFunction && isFunctionLike$1(child)) {
|
|
8432
|
+
if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) localFunctionBindings.set(child.id.name, child);
|
|
8433
|
+
return false;
|
|
8434
|
+
}
|
|
8435
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
8436
|
+
const initializer = child.init ? stripParenExpression(child.init) : null;
|
|
8437
|
+
if (isFunctionLike$1(initializer)) localFunctionBindings.set(child.id.name, initializer);
|
|
8438
|
+
return;
|
|
8439
|
+
}
|
|
8440
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8441
|
+
const callee = stripParenExpression(child.callee);
|
|
8442
|
+
if (isFunctionLike$1(callee)) {
|
|
8443
|
+
enqueue(callee);
|
|
8444
|
+
return;
|
|
8445
|
+
}
|
|
8446
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
8447
|
+
calledBindingNames.add(callee.name);
|
|
8448
|
+
return;
|
|
8449
|
+
}
|
|
8450
|
+
if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
8451
|
+
});
|
|
8452
|
+
for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
|
|
8453
|
+
}
|
|
8454
|
+
return invokedFunctions;
|
|
8455
|
+
};
|
|
8456
|
+
//#endregion
|
|
8406
8457
|
//#region src/plugin/utils/is-react-hook-name.ts
|
|
8407
8458
|
const isReactHookName = (name) => {
|
|
8408
8459
|
if (!name.startsWith("use")) return false;
|
|
@@ -8458,46 +8509,90 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
8458
8509
|
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
8459
8510
|
};
|
|
8460
8511
|
//#endregion
|
|
8512
|
+
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
8513
|
+
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
8514
|
+
let ancestor = bindingIdentifier.parent;
|
|
8515
|
+
while (ancestor) {
|
|
8516
|
+
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
8517
|
+
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
8518
|
+
ancestor = ancestor.parent ?? null;
|
|
8519
|
+
}
|
|
8520
|
+
return null;
|
|
8521
|
+
};
|
|
8522
|
+
//#endregion
|
|
8523
|
+
//#region src/plugin/utils/executes-during-render.ts
|
|
8524
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
|
|
8525
|
+
"map",
|
|
8526
|
+
"filter",
|
|
8527
|
+
"forEach",
|
|
8528
|
+
"flatMap",
|
|
8529
|
+
"reduce",
|
|
8530
|
+
"reduceRight",
|
|
8531
|
+
"some",
|
|
8532
|
+
"every",
|
|
8533
|
+
"find",
|
|
8534
|
+
"findIndex",
|
|
8535
|
+
"findLast",
|
|
8536
|
+
"findLastIndex",
|
|
8537
|
+
"sort",
|
|
8538
|
+
"toSorted"
|
|
8539
|
+
]);
|
|
8540
|
+
const REACT_RENDER_PHASE_HOOK_NAMES = new Set(["useMemo", "useState"]);
|
|
8541
|
+
const isGlobalArrayFromMember = (node, scopes) => {
|
|
8542
|
+
const unwrappedNode = stripParenExpression(node);
|
|
8543
|
+
if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed || !isNodeOfType(unwrappedNode.object, "Identifier") || unwrappedNode.object.name !== "Array" || !scopes.isGlobalReference(unwrappedNode.object) || !isNodeOfType(unwrappedNode.property, "Identifier")) return false;
|
|
8544
|
+
return unwrappedNode.property.name === "from";
|
|
8545
|
+
};
|
|
8546
|
+
const isConstAliasOfGlobalArrayFrom = (node, scopes) => {
|
|
8547
|
+
const unwrappedNode = stripParenExpression(node);
|
|
8548
|
+
if (!isNodeOfType(unwrappedNode, "Identifier")) return false;
|
|
8549
|
+
const binding = findVariableInitializer(unwrappedNode, unwrappedNode.name);
|
|
8550
|
+
if (!binding?.initializer) return false;
|
|
8551
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
8552
|
+
return Boolean(declarator && scopes.symbolFor(unwrappedNode)?.declarationNode === declarator && declarator.init && declarator.parent && isNodeOfType(declarator.parent, "VariableDeclaration") && declarator.parent.kind === "const" && isGlobalArrayFromMember(declarator.init, scopes));
|
|
8553
|
+
};
|
|
8554
|
+
const executesDuringRender = (functionNode, scopes) => {
|
|
8555
|
+
const parent = functionNode.parent;
|
|
8556
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
8557
|
+
if (parent.callee === functionNode) return true;
|
|
8558
|
+
if ((scopes ? isReactApiCall(parent, REACT_RENDER_PHASE_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }) : isHookCall$2(parent, REACT_RENDER_PHASE_HOOK_NAMES)) && parent.arguments?.[0] === functionNode) return true;
|
|
8559
|
+
if (scopes && parent.arguments?.[1] === functionNode && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
|
|
8560
|
+
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
|
|
8561
|
+
};
|
|
8562
|
+
//#endregion
|
|
8563
|
+
//#region src/plugin/utils/find-render-phase-component-or-hook.ts
|
|
8564
|
+
const findRenderPhaseComponentOrHook = (node, scopes) => {
|
|
8565
|
+
let functionNode = findEnclosingFunction(node);
|
|
8566
|
+
while (functionNode) {
|
|
8567
|
+
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
8568
|
+
if (!executesDuringRender(functionNode, scopes)) return null;
|
|
8569
|
+
functionNode = findEnclosingFunction(functionNode);
|
|
8570
|
+
}
|
|
8571
|
+
return null;
|
|
8572
|
+
};
|
|
8573
|
+
//#endregion
|
|
8574
|
+
//#region src/plugin/utils/get-function-binding-name.ts
|
|
8575
|
+
const getFunctionBindingIdentifier = (functionNode) => {
|
|
8576
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
8577
|
+
const parent = functionNode.parent;
|
|
8578
|
+
if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
|
|
8579
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionNode && isNodeOfType(parent.left, "Identifier")) return parent.left;
|
|
8580
|
+
if (isNodeOfType(parent, "CallExpression")) {
|
|
8581
|
+
const callParent = parent.parent;
|
|
8582
|
+
if (isNodeOfType(callParent, "VariableDeclarator") && isNodeOfType(callParent.id, "Identifier")) return callParent.id;
|
|
8583
|
+
}
|
|
8584
|
+
return null;
|
|
8585
|
+
};
|
|
8586
|
+
const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(functionNode)?.name ?? null;
|
|
8587
|
+
//#endregion
|
|
8461
8588
|
//#region src/plugin/utils/get-range-start.ts
|
|
8462
8589
|
const getRangeStart = (node) => {
|
|
8463
8590
|
const rangeStart = node.range?.[0];
|
|
8464
8591
|
return typeof rangeStart === "number" ? rangeStart : null;
|
|
8465
8592
|
};
|
|
8466
8593
|
//#endregion
|
|
8467
|
-
//#region src/plugin/utils/is-
|
|
8468
|
-
const
|
|
8469
|
-
let node = callExpression;
|
|
8470
|
-
let parent = node.parent;
|
|
8471
|
-
while (parent) {
|
|
8472
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
8473
|
-
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
8474
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
8475
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
8476
|
-
node = parent;
|
|
8477
|
-
parent = node.parent;
|
|
8478
|
-
continue;
|
|
8479
|
-
}
|
|
8480
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
8481
|
-
node = parent;
|
|
8482
|
-
parent = node.parent;
|
|
8483
|
-
continue;
|
|
8484
|
-
}
|
|
8485
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
8486
|
-
node = parent;
|
|
8487
|
-
parent = node.parent;
|
|
8488
|
-
continue;
|
|
8489
|
-
}
|
|
8490
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
8491
|
-
const expressions = parent.expressions ?? [];
|
|
8492
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
8493
|
-
node = parent;
|
|
8494
|
-
parent = node.parent;
|
|
8495
|
-
continue;
|
|
8496
|
-
}
|
|
8497
|
-
return false;
|
|
8498
|
-
}
|
|
8499
|
-
return false;
|
|
8500
|
-
};
|
|
8594
|
+
//#region src/plugin/utils/is-event-handler-attribute.ts
|
|
8595
|
+
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
|
|
8501
8596
|
//#endregion
|
|
8502
8597
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
8503
8598
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
@@ -8532,125 +8627,9 @@ const isCleanupReturningSubscribeLikeCallExpression = (node) => {
|
|
|
8532
8627
|
return true;
|
|
8533
8628
|
};
|
|
8534
8629
|
//#endregion
|
|
8535
|
-
//#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
|
|
8536
|
-
const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
|
|
8537
|
-
"each",
|
|
8538
|
-
"every",
|
|
8539
|
-
"filter",
|
|
8540
|
-
"find",
|
|
8541
|
-
"findIndex",
|
|
8542
|
-
"findLast",
|
|
8543
|
-
"findLastIndex",
|
|
8544
|
-
"flatMap",
|
|
8545
|
-
"forEach",
|
|
8546
|
-
"map",
|
|
8547
|
-
"reduce",
|
|
8548
|
-
"reduceRight",
|
|
8549
|
-
"some",
|
|
8550
|
-
"sort",
|
|
8551
|
-
"toSorted"
|
|
8552
|
-
]);
|
|
8553
|
-
const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
8554
|
-
"from",
|
|
8555
|
-
"fromAsync",
|
|
8556
|
-
"groupBy"
|
|
8557
|
-
]);
|
|
8558
|
-
const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8559
|
-
const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
|
|
8560
|
-
const isListenerRemovalViaNullHandler = (callNode) => {
|
|
8561
|
-
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
8562
|
-
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8563
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
|
|
8564
|
-
};
|
|
8565
|
-
const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
|
|
8566
|
-
"off",
|
|
8567
|
-
"removeEventListener",
|
|
8568
|
-
"removeListener"
|
|
8569
|
-
]);
|
|
8570
|
-
const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
|
|
8571
|
-
if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
|
|
8572
|
-
const handlerArgument = callNode.arguments?.[1];
|
|
8573
|
-
return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
|
|
8574
|
-
};
|
|
8575
|
-
const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8576
|
-
const callNode = unwrapChainExpression$3(node);
|
|
8577
|
-
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
8578
|
-
if (isListenerRemovalViaNullHandler(callNode)) return true;
|
|
8579
|
-
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8580
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
8581
|
-
if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
|
|
8582
|
-
if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
|
|
8583
|
-
if (knownCleanupFunctionNames.has(callee.name)) return true;
|
|
8584
|
-
return false;
|
|
8585
|
-
}
|
|
8586
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
8587
|
-
if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
|
|
8588
|
-
if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
|
|
8589
|
-
return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
|
|
8590
|
-
}
|
|
8591
|
-
return false;
|
|
8592
|
-
};
|
|
8593
|
-
const isStaticIteratorCallbackCallee = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && isNodeOfType(callee.property, "Identifier") && (callee.object.name === "Array" || callee.object.name === "Map" || callee.object.name === "Object") && STATIC_ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
|
|
8594
|
-
const isIteratorCallbackArgument = (node) => {
|
|
8595
|
-
const parentNode = node.parent;
|
|
8596
|
-
if (!isNodeOfType(parentNode, "CallExpression")) return false;
|
|
8597
|
-
if (!parentNode.arguments?.some((argument) => argument === node)) return false;
|
|
8598
|
-
const callee = unwrapChainExpression$3(parentNode.callee);
|
|
8599
|
-
if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
|
|
8600
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(callee.property.name);
|
|
8601
|
-
};
|
|
8602
|
-
const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8603
|
-
let didFindRelease = false;
|
|
8604
|
-
walkAst(node, (child) => {
|
|
8605
|
-
if (didFindRelease) return false;
|
|
8606
|
-
if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
|
|
8607
|
-
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
8608
|
-
didFindRelease = true;
|
|
8609
|
-
return false;
|
|
8610
|
-
}
|
|
8611
|
-
});
|
|
8612
|
-
return didFindRelease;
|
|
8613
|
-
};
|
|
8614
|
-
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8615
|
-
if (!isFunctionLike$1(node)) return false;
|
|
8616
|
-
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
8617
|
-
};
|
|
8618
|
-
const isProvablyNoOpCleanupFunction = (node) => {
|
|
8619
|
-
if (!isFunctionLike$1(node)) return false;
|
|
8620
|
-
let sawNoOpRemoval = false;
|
|
8621
|
-
let sawOtherCall = false;
|
|
8622
|
-
walkAst(node.body, (child) => {
|
|
8623
|
-
if (sawOtherCall) return false;
|
|
8624
|
-
if (isFunctionLike$1(child)) return false;
|
|
8625
|
-
const callNode = unwrapChainExpression$3(child);
|
|
8626
|
-
if (!isNodeOfType(callNode, "CallExpression")) return;
|
|
8627
|
-
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8628
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
|
|
8629
|
-
sawNoOpRemoval = true;
|
|
8630
|
-
return;
|
|
8631
|
-
}
|
|
8632
|
-
sawOtherCall = true;
|
|
8633
|
-
});
|
|
8634
|
-
return sawNoOpRemoval && !sawOtherCall;
|
|
8635
|
-
};
|
|
8636
|
-
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
|
|
8637
|
-
if (!returnedValue) return false;
|
|
8638
|
-
const unwrappedValue = unwrapChainExpression$3(returnedValue);
|
|
8639
|
-
if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
|
|
8640
|
-
if (isNodeOfType(unwrappedValue, "Identifier")) {
|
|
8641
|
-
if (unwrappedValue.name === "undefined") return false;
|
|
8642
|
-
if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
|
|
8643
|
-
return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
|
|
8644
|
-
}
|
|
8645
|
-
if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
8646
|
-
if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
|
|
8647
|
-
if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
8648
|
-
if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
|
|
8649
|
-
return false;
|
|
8650
|
-
};
|
|
8651
|
-
//#endregion
|
|
8652
8630
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
8653
8631
|
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
8632
|
+
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
8654
8633
|
const RESOURCE_NOUN_BY_KIND = {
|
|
8655
8634
|
subscribe: "subscription",
|
|
8656
8635
|
timer: "timer",
|
|
@@ -8661,7 +8640,66 @@ const isSubscribeOrObserveCall = (node) => {
|
|
|
8661
8640
|
if (isSubscribeLikeCallExpression(node)) return true;
|
|
8662
8641
|
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
8663
8642
|
};
|
|
8664
|
-
const
|
|
8643
|
+
const resolveExpressionKey$1 = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
8644
|
+
if (!expression) return null;
|
|
8645
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
8646
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
8647
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
8648
|
+
if (!symbol) return context.scopes.isGlobalReference(unwrappedExpression) ? `global:${unwrappedExpression.name}` : null;
|
|
8649
|
+
if (visitedSymbolIds.has(symbol.id)) return `symbol:${symbol.id}`;
|
|
8650
|
+
visitedSymbolIds.add(symbol.id);
|
|
8651
|
+
const bindingProperty = symbol.bindingIdentifier.parent;
|
|
8652
|
+
const bindingPattern = bindingProperty?.parent;
|
|
8653
|
+
const variableDeclarator = bindingPattern?.parent;
|
|
8654
|
+
const bindingPropertyName = isNodeOfType(bindingProperty, "Property") ? getStaticPropertyKeyName(bindingProperty) : null;
|
|
8655
|
+
if (bindingPropertyName && isNodeOfType(bindingPattern, "ObjectPattern") && isNodeOfType(variableDeclarator, "VariableDeclarator") && variableDeclarator.id === bindingPattern) {
|
|
8656
|
+
const objectKey = resolveExpressionKey$1(variableDeclarator.init, context, visitedSymbolIds);
|
|
8657
|
+
return objectKey ? `${objectKey}.${bindingPropertyName}` : `symbol:${symbol.id}`;
|
|
8658
|
+
}
|
|
8659
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
8660
|
+
if (symbol.kind === "const" && initializer && (isNodeOfType(initializer, "Identifier") || isNodeOfType(initializer, "MemberExpression"))) return resolveExpressionKey$1(initializer, context, visitedSymbolIds) ?? `symbol:${symbol.id}`;
|
|
8661
|
+
return `symbol:${symbol.id}`;
|
|
8662
|
+
}
|
|
8663
|
+
if (isNodeOfType(unwrappedExpression, "MemberExpression") && !unwrappedExpression.computed) {
|
|
8664
|
+
if (!isNodeOfType(unwrappedExpression.property, "Identifier")) return null;
|
|
8665
|
+
const objectKey = resolveExpressionKey$1(unwrappedExpression.object, context, visitedSymbolIds);
|
|
8666
|
+
return objectKey ? `${objectKey}.${unwrappedExpression.property.name}` : null;
|
|
8667
|
+
}
|
|
8668
|
+
if (isNodeOfType(unwrappedExpression, "ThisExpression")) return "this";
|
|
8669
|
+
if (isNodeOfType(unwrappedExpression, "Literal") && (typeof unwrappedExpression.value === "string" || typeof unwrappedExpression.value === "number")) return `literal:${String(unwrappedExpression.value)}`;
|
|
8670
|
+
if (isFunctionLike$1(unwrappedExpression)) {
|
|
8671
|
+
const rangeStart = getRangeStart(unwrappedExpression);
|
|
8672
|
+
return rangeStart === null ? null : `function:${rangeStart}`;
|
|
8673
|
+
}
|
|
8674
|
+
return null;
|
|
8675
|
+
};
|
|
8676
|
+
const findAssignedResourceKey = (resourceNode, context) => {
|
|
8677
|
+
let currentNode = resourceNode;
|
|
8678
|
+
let parentNode = currentNode.parent;
|
|
8679
|
+
while (isNodeOfType(parentNode, "ChainExpression")) {
|
|
8680
|
+
currentNode = parentNode;
|
|
8681
|
+
parentNode = currentNode.parent;
|
|
8682
|
+
}
|
|
8683
|
+
if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode) return resolveExpressionKey$1(parentNode.id, context);
|
|
8684
|
+
if (isNodeOfType(parentNode, "AssignmentExpression") && parentNode.right === currentNode) return resolveExpressionKey$1(parentNode.left, context);
|
|
8685
|
+
return null;
|
|
8686
|
+
};
|
|
8687
|
+
const getCallRegistrationDetails = (callNode, context) => {
|
|
8688
|
+
const callee = stripParenExpression(callNode.callee);
|
|
8689
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return {
|
|
8690
|
+
receiverKey: null,
|
|
8691
|
+
registrationVerbName: null,
|
|
8692
|
+
eventKey: null,
|
|
8693
|
+
handlerKey: null
|
|
8694
|
+
};
|
|
8695
|
+
return {
|
|
8696
|
+
receiverKey: resolveExpressionKey$1(callee.object, context),
|
|
8697
|
+
registrationVerbName: callee.property.name,
|
|
8698
|
+
eventKey: resolveExpressionKey$1(callNode.arguments?.[0], context),
|
|
8699
|
+
handlerKey: resolveExpressionKey$1(callNode.arguments?.[1], context)
|
|
8700
|
+
};
|
|
8701
|
+
};
|
|
8702
|
+
const findSubscribeLikeUsages = (callback, context) => {
|
|
8665
8703
|
const usages = [];
|
|
8666
8704
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8667
8705
|
let cleanupArgument = null;
|
|
@@ -8670,13 +8708,22 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
8670
8708
|
const lastCallbackStatement = callbackStatements[callbackStatements.length - 1];
|
|
8671
8709
|
if (isNodeOfType(lastCallbackStatement, "ReturnStatement") && lastCallbackStatement.argument) cleanupArgument = lastCallbackStatement.argument;
|
|
8672
8710
|
}
|
|
8711
|
+
const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
|
|
8673
8712
|
walkAst(callback, (child) => {
|
|
8674
|
-
if (child
|
|
8713
|
+
if (child !== callback && isFunctionLike$1(child)) {
|
|
8714
|
+
if (child === cleanupArgument) return false;
|
|
8715
|
+
if (!effectInvokedFunctions.has(child) && !isSynchronousIteratorCallback(child)) return false;
|
|
8716
|
+
}
|
|
8675
8717
|
if (isSocketConstruction(child)) {
|
|
8676
8718
|
usages.push({
|
|
8677
8719
|
kind: "socket",
|
|
8678
8720
|
node: child,
|
|
8679
|
-
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8721
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket",
|
|
8722
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
8723
|
+
receiverKey: null,
|
|
8724
|
+
registrationVerbName: null,
|
|
8725
|
+
eventKey: null,
|
|
8726
|
+
handlerKey: null
|
|
8680
8727
|
});
|
|
8681
8728
|
return;
|
|
8682
8729
|
}
|
|
@@ -8685,118 +8732,348 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
8685
8732
|
usages.push({
|
|
8686
8733
|
kind: "timer",
|
|
8687
8734
|
node: child,
|
|
8688
|
-
resourceName: child.callee.name
|
|
8735
|
+
resourceName: child.callee.name,
|
|
8736
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
8737
|
+
receiverKey: null,
|
|
8738
|
+
registrationVerbName: child.callee.name,
|
|
8739
|
+
eventKey: null,
|
|
8740
|
+
handlerKey: null
|
|
8689
8741
|
});
|
|
8690
8742
|
return;
|
|
8691
8743
|
}
|
|
8692
|
-
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME))
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
const bindings = {
|
|
8702
|
-
cleanupFunctionNames: /* @__PURE__ */ new Set(),
|
|
8703
|
-
subscriptionNames: /* @__PURE__ */ new Set(),
|
|
8704
|
-
effectScopeVariableNames: /* @__PURE__ */ new Set()
|
|
8705
|
-
};
|
|
8706
|
-
if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
|
|
8707
|
-
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
|
|
8708
|
-
walkAst(effectCallback.body, (child) => {
|
|
8709
|
-
if (!isSubscribeOrObserveCall(child)) return;
|
|
8710
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8711
|
-
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
8712
|
-
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
8713
|
-
bindings.subscriptionNames.add(child.callee.object.name);
|
|
8714
|
-
});
|
|
8715
|
-
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
8716
|
-
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
8717
|
-
for (const declarator of child.declarations ?? []) {
|
|
8718
|
-
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
8719
|
-
const bindingName = declarator.id.name;
|
|
8720
|
-
bindings.effectScopeVariableNames.add(bindingName);
|
|
8721
|
-
const init = declarator.init;
|
|
8722
|
-
if (!init) continue;
|
|
8723
|
-
if (isSocketConstruction(init)) {
|
|
8724
|
-
bindings.subscriptionNames.add(bindingName);
|
|
8725
|
-
continue;
|
|
8726
|
-
}
|
|
8727
|
-
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
8728
|
-
if (isSubscribeLikeCallExpression(init)) {
|
|
8729
|
-
bindings.subscriptionNames.add(bindingName);
|
|
8730
|
-
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
8731
|
-
}
|
|
8732
|
-
}
|
|
8733
|
-
});
|
|
8734
|
-
walkAst(effectCallback.body, (child) => {
|
|
8735
|
-
if (child !== effectCallback.body && (isNodeOfType(child, "ArrowFunctionExpression") || isNodeOfType(child, "FunctionExpression"))) return false;
|
|
8736
|
-
if (isNodeOfType(child, "FunctionDeclaration") && child.id && isCleanupFunctionLike(child, bindings.cleanupFunctionNames, bindings.subscriptionNames)) {
|
|
8737
|
-
bindings.cleanupFunctionNames.add(child.id.name);
|
|
8738
|
-
return false;
|
|
8739
|
-
}
|
|
8740
|
-
});
|
|
8741
|
-
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
8742
|
-
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
8743
|
-
for (const declarator of child.declarations ?? []) {
|
|
8744
|
-
if (!isNodeOfType(declarator.id, "Identifier") || !declarator.init) continue;
|
|
8745
|
-
if (isCleanupFunctionLike(declarator.init, bindings.cleanupFunctionNames, bindings.subscriptionNames)) bindings.cleanupFunctionNames.add(declarator.id.name);
|
|
8744
|
+
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) {
|
|
8745
|
+
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
8746
|
+
usages.push({
|
|
8747
|
+
kind: "subscribe",
|
|
8748
|
+
node: child,
|
|
8749
|
+
resourceName: child.callee.property.name,
|
|
8750
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
8751
|
+
...registrationDetails
|
|
8752
|
+
});
|
|
8746
8753
|
}
|
|
8747
8754
|
});
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
const
|
|
8755
|
+
return usages.filter((usage) => isNodeReachableWithinFunction(usage.node, context));
|
|
8756
|
+
};
|
|
8757
|
+
const isNodeReachableWithinFunction = (node, context) => {
|
|
8758
|
+
const owner = context.cfg.enclosingFunction(node);
|
|
8759
|
+
if (!owner) return true;
|
|
8760
|
+
const functionCfg = context.cfg.cfgFor(owner);
|
|
8761
|
+
if (!functionCfg) return true;
|
|
8762
|
+
const targetBlock = functionCfg.blockOf(node);
|
|
8763
|
+
if (!targetBlock) return true;
|
|
8764
|
+
const visitedBlocks = new Set([functionCfg.entry]);
|
|
8765
|
+
const pendingBlocks = [functionCfg.entry];
|
|
8766
|
+
while (pendingBlocks.length > 0) {
|
|
8767
|
+
const currentBlock = pendingBlocks.pop();
|
|
8768
|
+
if (!currentBlock) break;
|
|
8769
|
+
if (currentBlock === targetBlock) return true;
|
|
8770
|
+
for (const edge of currentBlock.successors) {
|
|
8771
|
+
if (visitedBlocks.has(edge.to)) continue;
|
|
8772
|
+
visitedBlocks.add(edge.to);
|
|
8773
|
+
pendingBlocks.push(edge.to);
|
|
8774
|
+
}
|
|
8775
|
+
}
|
|
8776
|
+
return false;
|
|
8777
|
+
};
|
|
8778
|
+
const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, context) => {
|
|
8779
|
+
let pathAnchor = usageNode;
|
|
8780
|
+
let pathOwner = findEnclosingFunction(pathAnchor);
|
|
8781
|
+
while (pathOwner && isSynchronousIteratorCallback(pathOwner)) {
|
|
8782
|
+
if (matchingNodes.length > 0 && matchingNodes.every((matchingNode) => context.cfg.enclosingFunction(matchingNode) === pathOwner)) break;
|
|
8783
|
+
const iteratorCall = pathOwner.parent;
|
|
8784
|
+
if (!isNodeOfType(iteratorCall, "CallExpression")) break;
|
|
8785
|
+
pathAnchor = iteratorCall;
|
|
8786
|
+
pathOwner = findEnclosingFunction(pathAnchor);
|
|
8787
|
+
}
|
|
8788
|
+
const owner = context.cfg.enclosingFunction(pathAnchor);
|
|
8789
|
+
if (!owner) return false;
|
|
8790
|
+
const functionCfg = context.cfg.cfgFor(owner);
|
|
8791
|
+
if (!functionCfg) return false;
|
|
8792
|
+
const usageBlock = functionCfg.blockOf(pathAnchor);
|
|
8793
|
+
if (!usageBlock) return false;
|
|
8794
|
+
const usageStart = getRangeStart(usageNode);
|
|
8795
|
+
const matchingBlocks = new Set(matchingNodes.flatMap((matchingNode) => {
|
|
8796
|
+
if (context.cfg.enclosingFunction(matchingNode) !== owner) return [];
|
|
8797
|
+
const matchingBlock = functionCfg.blockOf(matchingNode);
|
|
8798
|
+
if (!matchingBlock) return [];
|
|
8799
|
+
const matchingStart = getRangeStart(matchingNode);
|
|
8800
|
+
if (matchingBlock === usageBlock && usageStart !== null && matchingStart !== null && matchingStart < usageStart) return [];
|
|
8801
|
+
return [matchingBlock];
|
|
8802
|
+
}));
|
|
8803
|
+
if (matchingBlocks.has(usageBlock)) return true;
|
|
8804
|
+
const visitedBlocks = new Set([usageBlock]);
|
|
8805
|
+
const pendingBlocks = [usageBlock];
|
|
8806
|
+
while (pendingBlocks.length > 0) {
|
|
8807
|
+
const currentBlock = pendingBlocks.pop();
|
|
8808
|
+
if (!currentBlock) break;
|
|
8809
|
+
for (const edge of currentBlock.successors) {
|
|
8810
|
+
if (matchingBlocks.has(edge.to)) continue;
|
|
8811
|
+
if (edge.to === functionCfg.exit) return false;
|
|
8812
|
+
if (visitedBlocks.has(edge.to)) continue;
|
|
8813
|
+
visitedBlocks.add(edge.to);
|
|
8814
|
+
pendingBlocks.push(edge.to);
|
|
8815
|
+
}
|
|
8816
|
+
}
|
|
8817
|
+
return matchingBlocks.size > 0;
|
|
8818
|
+
};
|
|
8819
|
+
const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
|
|
8754
8820
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8755
8821
|
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
8756
|
-
const
|
|
8822
|
+
const releaseCalls = [];
|
|
8757
8823
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8758
|
-
if (!
|
|
8759
|
-
|
|
8760
|
-
if (releaseStart !== null) releaseStarts.push(releaseStart);
|
|
8824
|
+
if (!isNodeOfType(isNodeOfType(child, "ChainExpression") ? child.expression : child, "CallExpression")) return;
|
|
8825
|
+
releaseCalls.push(child);
|
|
8761
8826
|
});
|
|
8762
|
-
if (
|
|
8827
|
+
if (releaseCalls.length === 0) return usages;
|
|
8763
8828
|
return usages.filter((usage) => {
|
|
8764
8829
|
const usageStart = getRangeStart(usage.node);
|
|
8765
8830
|
if (usageStart === null) return true;
|
|
8766
|
-
|
|
8831
|
+
const matchingReleaseCalls = releaseCalls.filter((releaseCall) => {
|
|
8832
|
+
const releaseStart = getRangeStart(releaseCall);
|
|
8833
|
+
return releaseStart !== null && releaseStart > usageStart && doesReleaseCallMatchUsage(releaseCall, usage, context);
|
|
8834
|
+
});
|
|
8835
|
+
return !doMatchingNodesCoverEveryPathAfterUsage(usage.node, matchingReleaseCalls, context);
|
|
8767
8836
|
});
|
|
8768
8837
|
};
|
|
8769
|
-
const
|
|
8770
|
-
if (
|
|
8771
|
-
const
|
|
8772
|
-
if (
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8838
|
+
const resolveIteratorCollectionKey = (expression, context) => {
|
|
8839
|
+
if (!expression || !isNodeOfType(expression, "Identifier")) return null;
|
|
8840
|
+
const symbol = context.scopes.symbolFor(expression);
|
|
8841
|
+
if (!symbol || symbol.kind !== "parameter") return null;
|
|
8842
|
+
let callbackNode = symbol.bindingIdentifier.parent;
|
|
8843
|
+
while (callbackNode && !isFunctionLike$1(callbackNode)) callbackNode = callbackNode.parent;
|
|
8844
|
+
if (!callbackNode || !isFunctionLike$1(callbackNode)) return null;
|
|
8845
|
+
const callNode = callbackNode.parent;
|
|
8846
|
+
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8847
|
+
const callee = stripParenExpression(callNode.callee);
|
|
8848
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
|
|
8849
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from" && callNode.arguments?.[1] === callbackNode) return resolveExpressionKey$1(callNode.arguments[0], context);
|
|
8850
|
+
if (callNode.arguments?.[0] === callbackNode) return resolveExpressionKey$1(callee.object, context);
|
|
8851
|
+
}
|
|
8852
|
+
return null;
|
|
8853
|
+
};
|
|
8854
|
+
const findCollectionMappingCall = (callbackNode) => {
|
|
8855
|
+
if (!isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression") || callbackNode.async || callbackNode.generator) return null;
|
|
8856
|
+
const callNode = callbackNode.parent;
|
|
8857
|
+
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8858
|
+
const callee = stripParenExpression(callNode.callee);
|
|
8859
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return null;
|
|
8860
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from" && callNode.arguments?.[1] === callbackNode) return callNode;
|
|
8861
|
+
return callee.property.name === "map" && callNode.arguments?.[0] === callbackNode ? callNode : null;
|
|
8862
|
+
};
|
|
8863
|
+
const findMappedResourceCollectionKey = (resourceNode, context) => {
|
|
8864
|
+
const callbackNode = findEnclosingFunction(resourceNode);
|
|
8865
|
+
if (!callbackNode || !isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression")) return null;
|
|
8866
|
+
const mappingCall = findCollectionMappingCall(callbackNode);
|
|
8867
|
+
if (!mappingCall) return null;
|
|
8868
|
+
if (isNodeOfType(callbackNode.body, "BlockStatement")) {
|
|
8869
|
+
const resourceRoot = findTransparentExpressionRoot(resourceNode);
|
|
8870
|
+
const resourceDeclarator = resourceRoot.parent;
|
|
8871
|
+
const resourceDeclaration = resourceDeclarator?.parent;
|
|
8872
|
+
if (!isNodeOfType(resourceDeclarator, "VariableDeclarator") || resourceDeclarator.init !== resourceRoot || !isNodeOfType(resourceDeclarator.id, "Identifier") || !isNodeOfType(resourceDeclaration, "VariableDeclaration") || resourceDeclaration.kind !== "const" || resourceDeclaration.parent !== callbackNode.body) return null;
|
|
8873
|
+
const returnStatements = [];
|
|
8874
|
+
walkAst(callbackNode.body, (child) => {
|
|
8875
|
+
if (child !== callbackNode.body && isFunctionLike$1(child)) return false;
|
|
8876
|
+
if (isNodeOfType(child, "ReturnStatement")) returnStatements.push(child);
|
|
8877
|
+
});
|
|
8878
|
+
const returnStatement = returnStatements[0];
|
|
8879
|
+
const callbackStatements = callbackNode.body.body ?? [];
|
|
8880
|
+
const returnedIdentifier = isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument ? stripParenExpression(returnStatement.argument) : null;
|
|
8881
|
+
const resourceSymbol = context.scopes.symbolFor(resourceDeclarator.id);
|
|
8882
|
+
if (returnStatements.length !== 1 || callbackStatements[callbackStatements.length - 1] !== returnStatement || !isNodeOfType(returnedIdentifier, "Identifier") || !resourceSymbol || context.scopes.symbolFor(returnedIdentifier)?.id !== resourceSymbol.id || !doMatchingNodesCoverEveryPathAfterUsage(resourceNode, [returnStatement], context)) return null;
|
|
8883
|
+
} else if (findTransparentExpressionRoot(resourceNode) !== callbackNode.body) return null;
|
|
8884
|
+
const mappingRoot = findTransparentExpressionRoot(mappingCall);
|
|
8885
|
+
const collectionDeclarator = mappingRoot.parent;
|
|
8886
|
+
return isNodeOfType(collectionDeclarator, "VariableDeclarator") && collectionDeclarator.init === mappingRoot ? resolveExpressionKey$1(collectionDeclarator.id, context) : null;
|
|
8887
|
+
};
|
|
8888
|
+
const findContainingCollectionKey = (resourceNode, context) => {
|
|
8889
|
+
const mappedCollectionKey = findMappedResourceCollectionKey(resourceNode, context);
|
|
8890
|
+
if (mappedCollectionKey !== null) return mappedCollectionKey;
|
|
8891
|
+
let currentNode = resourceNode;
|
|
8892
|
+
let parentNode = currentNode.parent;
|
|
8893
|
+
while (parentNode) {
|
|
8894
|
+
if (isFunctionLike$1(parentNode)) return null;
|
|
8895
|
+
if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode) return resolveExpressionKey$1(parentNode.id, context);
|
|
8896
|
+
currentNode = parentNode;
|
|
8897
|
+
parentNode = currentNode.parent;
|
|
8898
|
+
}
|
|
8899
|
+
return null;
|
|
8900
|
+
};
|
|
8901
|
+
const isWithinAssignmentTarget = (identifier) => {
|
|
8902
|
+
let currentNode = identifier;
|
|
8903
|
+
let parentNode = currentNode.parent;
|
|
8904
|
+
while (parentNode) {
|
|
8905
|
+
if (isNodeOfType(parentNode, "AssignmentExpression")) return parentNode.left === currentNode;
|
|
8906
|
+
if (isNodeOfType(parentNode, "UpdateExpression") || isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "delete") return parentNode.argument === currentNode;
|
|
8907
|
+
if (isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return parentNode.left === currentNode;
|
|
8908
|
+
currentNode = parentNode;
|
|
8909
|
+
parentNode = currentNode.parent;
|
|
8910
|
+
}
|
|
8911
|
+
return false;
|
|
8912
|
+
};
|
|
8913
|
+
const resolveStableValue = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
8914
|
+
if (!expression) return null;
|
|
8915
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
8916
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return unwrappedExpression;
|
|
8917
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
8918
|
+
const isUnreassignedMutableBinding = (symbol?.kind === "let" || symbol?.kind === "var") && isNodeOfType(symbol.declarationNode, "VariableDeclarator") && symbol.declarationNode.id === symbol.bindingIdentifier && symbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier)) && symbol.scope.symbols.filter((candidate) => candidate.name === symbol.name).length === 1;
|
|
8919
|
+
const recursiveFunctionSymbol = symbol?.kind === "function" && isFunctionLike$1(symbol.declarationNode) ? context.scopes.ownScopeFor(symbol.declarationNode)?.symbols.find((candidate) => candidate.name === symbol.name && candidate.declarationNode === symbol.declarationNode) : null;
|
|
8920
|
+
const isUnreassignedFunctionBinding = symbol?.kind === "function" && symbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier)) && (!recursiveFunctionSymbol || recursiveFunctionSymbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier))) && symbol.scope.symbols.filter((candidate) => candidate.name === symbol.name).length === 1;
|
|
8921
|
+
if (!symbol || symbol.kind !== "const" && !isUnreassignedMutableBinding && !isUnreassignedFunctionBinding || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return unwrappedExpression;
|
|
8922
|
+
visitedSymbolIds.add(symbol.id);
|
|
8923
|
+
return resolveStableValue(symbol.initializer, context, visitedSymbolIds);
|
|
8924
|
+
};
|
|
8925
|
+
const resolveObjectExpression = (expression, context) => {
|
|
8926
|
+
const resolvedExpression = resolveStableValue(expression, context);
|
|
8927
|
+
return isNodeOfType(resolvedExpression, "ObjectExpression") ? resolvedExpression : null;
|
|
8928
|
+
};
|
|
8929
|
+
const getListenerAbortControllerKey = (usage, context) => {
|
|
8930
|
+
if (usage.registrationVerbName !== "addEventListener" || !isNodeOfType(usage.node, "CallExpression")) return null;
|
|
8931
|
+
const optionsArgument = usage.node.arguments?.[2];
|
|
8932
|
+
const optionsObject = resolveObjectExpression(optionsArgument, context);
|
|
8933
|
+
if (!optionsObject) return null;
|
|
8934
|
+
for (const property of optionsObject.properties ?? []) {
|
|
8935
|
+
if (!isNodeOfType(property, "Property") || getStaticPropertyKeyName(property) !== "signal") continue;
|
|
8936
|
+
const signalKey = resolveExpressionKey$1(property.value, context);
|
|
8937
|
+
return signalKey?.endsWith(".signal") ? signalKey.slice(0, -7) : null;
|
|
8938
|
+
}
|
|
8939
|
+
return null;
|
|
8940
|
+
};
|
|
8941
|
+
const SYNCHRONOUS_ITERATOR_METHOD_NAMES$1 = new Set([
|
|
8942
|
+
"every",
|
|
8943
|
+
"filter",
|
|
8944
|
+
"flatMap",
|
|
8945
|
+
"forEach",
|
|
8946
|
+
"map",
|
|
8947
|
+
"reduce",
|
|
8948
|
+
"reduceRight",
|
|
8949
|
+
"some"
|
|
8950
|
+
]);
|
|
8951
|
+
const isSynchronousIteratorCallback = (functionNode) => {
|
|
8952
|
+
const callNode = functionNode.parent;
|
|
8953
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
8954
|
+
const callee = stripParenExpression(callNode.callee);
|
|
8955
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
8956
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from") return callNode.arguments?.[1] === functionNode;
|
|
8957
|
+
return SYNCHRONOUS_ITERATOR_METHOD_NAMES$1.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
|
|
8958
|
+
};
|
|
8959
|
+
const findDirectCallForReference = (identifier) => {
|
|
8960
|
+
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
8961
|
+
const callNode = expressionRoot.parent;
|
|
8962
|
+
return isNodeOfType(callNode, "CallExpression") && callNode.callee === expressionRoot ? callNode : null;
|
|
8963
|
+
};
|
|
8964
|
+
const findSingleDirectInvocation = (functionNode, caller, context) => {
|
|
8965
|
+
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
8966
|
+
if (!bindingIdentifier || resolveStableValue(bindingIdentifier, context) !== functionNode) return null;
|
|
8967
|
+
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
8968
|
+
if (!symbol) return null;
|
|
8969
|
+
const invocationCalls = symbol.references.flatMap((reference) => {
|
|
8970
|
+
const callNode = findDirectCallForReference(reference.identifier);
|
|
8971
|
+
return callNode ? [callNode] : [];
|
|
8972
|
+
});
|
|
8973
|
+
if (invocationCalls.length !== 1) return null;
|
|
8974
|
+
const invocationCall = invocationCalls[0];
|
|
8975
|
+
return findEnclosingFunction(invocationCall) === caller && isNodeReachableWithinFunction(invocationCall, context) ? invocationCall : null;
|
|
8976
|
+
};
|
|
8977
|
+
const resolveCleanupPathAnchor = (usageNode, effectCallback, context) => {
|
|
8978
|
+
const usageFunction = findEnclosingFunction(usageNode);
|
|
8979
|
+
if (!usageFunction || usageFunction === effectCallback) return usageNode;
|
|
8980
|
+
return findSingleDirectInvocation(usageFunction, effectCallback, context) ?? usageNode;
|
|
8981
|
+
};
|
|
8982
|
+
const resolveSingleAssignedCleanupFunction = (expression, usage, context) => {
|
|
8983
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
8984
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
|
|
8985
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
8986
|
+
const initializer = symbol?.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
8987
|
+
if (!symbol || symbol.kind !== "let" && symbol.kind !== "var" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || !isNodeOfType(initializer, "Literal") || initializer.value !== null || symbol.scope.symbols.filter((candidate) => candidate.name === symbol.name).length !== 1) return null;
|
|
8988
|
+
const assignmentReferences = symbol.references.filter((reference) => isWithinAssignmentTarget(reference.identifier));
|
|
8989
|
+
if (assignmentReferences.length !== 1) return null;
|
|
8990
|
+
const assignmentReference = assignmentReferences[0];
|
|
8991
|
+
const assignmentTarget = findTransparentExpressionRoot(assignmentReference.identifier);
|
|
8992
|
+
const assignmentNode = assignmentTarget.parent;
|
|
8993
|
+
if (!isNodeOfType(assignmentNode, "AssignmentExpression") || assignmentNode.operator !== "=" || assignmentNode.left !== assignmentTarget || findEnclosingFunction(assignmentNode) !== findEnclosingFunction(usage.node) || !doMatchingNodesCoverEveryPathAfterUsage(usage.node, [assignmentNode], context)) return null;
|
|
8994
|
+
const assignedValue = stripParenExpression(assignmentNode.right);
|
|
8995
|
+
return isFunctionLike$1(assignedValue) ? assignedValue : null;
|
|
8996
|
+
};
|
|
8997
|
+
const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visitedFunctions = /* @__PURE__ */ new Set()) => {
|
|
8998
|
+
if (!isFunctionLike$1(cleanupFunction) || visitedFunctions.has(cleanupFunction)) return false;
|
|
8999
|
+
visitedFunctions.add(cleanupFunction);
|
|
9000
|
+
let didCleanupFunctionMatch = false;
|
|
9001
|
+
walkAst(cleanupFunction.body, (cleanupChild) => {
|
|
9002
|
+
if (didCleanupFunctionMatch) return false;
|
|
9003
|
+
if (cleanupChild !== cleanupFunction.body && isFunctionLike$1(cleanupChild) && !isSynchronousIteratorCallback(cleanupChild)) return false;
|
|
9004
|
+
if (doesReleaseCallMatchUsage(cleanupChild, usage, context)) {
|
|
9005
|
+
didCleanupFunctionMatch = true;
|
|
9006
|
+
return false;
|
|
9007
|
+
}
|
|
9008
|
+
const helperCall = isNodeOfType(cleanupChild, "ChainExpression") ? cleanupChild.expression : cleanupChild;
|
|
9009
|
+
if (!isNodeOfType(helperCall, "CallExpression")) return;
|
|
9010
|
+
const stableHelperFunction = resolveStableValue(helperCall.callee, context);
|
|
9011
|
+
const helperFunction = isNodeOfType(stableHelperFunction, "Identifier") ? resolveSingleAssignedCleanupFunction(stableHelperFunction, usage, context) : stableHelperFunction;
|
|
9012
|
+
if (helperFunction && isFunctionLike$1(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context, visitedFunctions)) {
|
|
9013
|
+
didCleanupFunctionMatch = true;
|
|
9014
|
+
return false;
|
|
9015
|
+
}
|
|
8776
9016
|
});
|
|
9017
|
+
return didCleanupFunctionMatch;
|
|
8777
9018
|
};
|
|
8778
|
-
const
|
|
9019
|
+
const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
8779
9020
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
8780
|
-
if (
|
|
8781
|
-
|
|
8782
|
-
|
|
9021
|
+
if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
9022
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return callback.body === usage.node && isCleanupReturningSubscribeLikeCallExpression(callback.body);
|
|
9023
|
+
const matchingCleanupReturns = [];
|
|
8783
9024
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8784
|
-
if (didFindCleanupReturn) return;
|
|
8785
9025
|
if (!isNodeOfType(child, "ReturnStatement")) return;
|
|
8786
|
-
|
|
8787
|
-
|
|
9026
|
+
const returnStart = getRangeStart(child);
|
|
9027
|
+
const usageStart = getRangeStart(usage.node);
|
|
9028
|
+
if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
|
|
9029
|
+
const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
|
|
9030
|
+
if (!returnedValue) return;
|
|
9031
|
+
if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
|
|
9032
|
+
matchingCleanupReturns.push(child);
|
|
9033
|
+
return;
|
|
9034
|
+
}
|
|
9035
|
+
if (usage.kind === "subscribe" && isNodeOfType(returnedValue, "Identifier") && usage.handleKey !== null && resolveExpressionKey$1(returnedValue, context) === usage.handleKey && isCleanupReturningSubscribeLikeCallExpression(usage.node)) {
|
|
9036
|
+
matchingCleanupReturns.push(child);
|
|
9037
|
+
return;
|
|
9038
|
+
}
|
|
9039
|
+
if (isNodeOfType(returnedValue, "Identifier")) {
|
|
9040
|
+
if (returnedValue.name === "undefined" && context.scopes.isGlobalReference(returnedValue)) return;
|
|
9041
|
+
const returnedKey = resolveExpressionKey$1(returnedValue, context);
|
|
9042
|
+
if (usage.handleKey !== null && returnedKey === usage.handleKey) return;
|
|
9043
|
+
if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
|
|
9044
|
+
}
|
|
9045
|
+
const cleanupFunction = resolveStableValue(returnedValue, context);
|
|
9046
|
+
if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
|
|
9047
|
+
if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
|
|
8788
9048
|
});
|
|
8789
|
-
return
|
|
9049
|
+
return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
|
|
9050
|
+
};
|
|
9051
|
+
const findFirstUsageWithoutCleanup = (callback, usages, context) => {
|
|
9052
|
+
for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context)) return usage;
|
|
9053
|
+
return null;
|
|
9054
|
+
};
|
|
9055
|
+
const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
9056
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
9057
|
+
if (isNodeOfType(unwrappedExpression, "NewExpression") && isNodeOfType(unwrappedExpression.callee, "Identifier") && unwrappedExpression.callee.name === "AbortController") return true;
|
|
9058
|
+
if (isNodeOfType(unwrappedExpression, "MemberExpression")) return isLocalAbortControllerExpression(unwrappedExpression.object, context, visitedSymbolIds);
|
|
9059
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return false;
|
|
9060
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
9061
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
9062
|
+
visitedSymbolIds.add(symbol.id);
|
|
9063
|
+
if (symbol.initializer) return isLocalAbortControllerExpression(symbol.initializer, context, visitedSymbolIds);
|
|
9064
|
+
const bindingProperty = symbol.bindingIdentifier.parent;
|
|
9065
|
+
const bindingPattern = bindingProperty?.parent;
|
|
9066
|
+
const variableDeclarator = bindingPattern?.parent;
|
|
9067
|
+
return Boolean(isNodeOfType(bindingProperty, "Property") && isNodeOfType(bindingPattern, "ObjectPattern") && isNodeOfType(variableDeclarator, "VariableDeclarator") && variableDeclarator.init && isLocalAbortControllerExpression(variableDeclarator.init, context, visitedSymbolIds));
|
|
8790
9068
|
};
|
|
8791
|
-
const
|
|
8792
|
-
const isSelfReleasingListenerOptionProperty = (property) => {
|
|
9069
|
+
const isSelfReleasingListenerOptionProperty = (property, context) => {
|
|
8793
9070
|
if (!isNodeOfType(property, "Property")) return false;
|
|
8794
9071
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
|
|
8795
|
-
if (keyName === "signal") return
|
|
9072
|
+
if (keyName === "signal") return !isLocalAbortControllerExpression(property.value, context);
|
|
8796
9073
|
if (keyName !== "once") return false;
|
|
8797
9074
|
return isNodeOfType(property.value, "Literal") && property.value.value === true;
|
|
8798
9075
|
};
|
|
8799
|
-
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
|
|
9076
|
+
const hasSelfReleasingListenerOptions = (node, context) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some((property) => isSelfReleasingListenerOptionProperty(property, context)));
|
|
8800
9077
|
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
8801
9078
|
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
8802
9079
|
["addListener", new Set([
|
|
@@ -8822,85 +9099,185 @@ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
|
8822
9099
|
"teardown"
|
|
8823
9100
|
]);
|
|
8824
9101
|
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8825
|
-
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
8826
9102
|
const getReleaseVerbName = (node) => {
|
|
8827
|
-
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
8828
9103
|
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8829
9104
|
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8830
9105
|
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
8831
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8832
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier"))
|
|
9106
|
+
if (isNodeOfType(callee, "Identifier")) return TIMER_CLEANUP_CALLEE_NAMES.has(callee.name) || GLOBAL_RELEASE_METHOD_NAMES.has(callee.name) || UNIVERSAL_RELEASE_VERB_NAMES.has(callee.name) ? callee.name : null;
|
|
9107
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
9108
|
+
const methodName = callee.property.name;
|
|
9109
|
+
return GLOBAL_RELEASE_METHOD_NAMES.has(methodName) || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(methodName) || methodName === "on" ? methodName : null;
|
|
9110
|
+
}
|
|
8833
9111
|
return null;
|
|
8834
9112
|
};
|
|
9113
|
+
const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
9114
|
+
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
9115
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
9116
|
+
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
9117
|
+
if (usage.kind === "timer") {
|
|
9118
|
+
const expectedCleanupName = usage.registrationVerbName === "setInterval" ? "clearInterval" : "clearTimeout";
|
|
9119
|
+
if (!isNodeOfType(callee, "Identifier") || !TIMER_CLEANUP_CALLEE_NAMES.has(callee.name) || callee.name !== expectedCleanupName) return false;
|
|
9120
|
+
if (usage.handleKey !== null && resolveExpressionKey$1(callNode.arguments?.[0], context) === usage.handleKey) return true;
|
|
9121
|
+
const collectionKey = findContainingCollectionKey(usage.node, context);
|
|
9122
|
+
return collectionKey !== null && collectionKey === resolveIteratorCollectionKey(callNode.arguments?.[0], context);
|
|
9123
|
+
}
|
|
9124
|
+
if (isNodeOfType(callee, "Identifier") && usage.kind === "subscribe" && usage.handleKey !== null && resolveExpressionKey$1(callee, context) === usage.handleKey && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
9125
|
+
const releaseVerbName = getReleaseVerbName(callNode);
|
|
9126
|
+
if (!releaseVerbName) return false;
|
|
9127
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
9128
|
+
const releaseReceiverKey = resolveExpressionKey$1(callee.object, context);
|
|
9129
|
+
if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
|
|
9130
|
+
if (usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName))) return true;
|
|
9131
|
+
if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
|
|
9132
|
+
if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
|
|
9133
|
+
const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
9134
|
+
if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
|
|
9135
|
+
const releaseEventKey = resolveExpressionKey$1(callNode.arguments?.[0], context);
|
|
9136
|
+
if (usage.eventKey !== null && releaseEventKey !== null && usage.eventKey !== releaseEventKey) {
|
|
9137
|
+
const usageIteratorCollectionKey = isNodeOfType(usage.node, "CallExpression") ? resolveIteratorCollectionKey(usage.node.arguments?.[0], context) : null;
|
|
9138
|
+
const releaseIteratorCollectionKey = resolveIteratorCollectionKey(callNode.arguments?.[0], context);
|
|
9139
|
+
if (usageIteratorCollectionKey === null || usageIteratorCollectionKey !== releaseIteratorCollectionKey) return false;
|
|
9140
|
+
}
|
|
9141
|
+
if (releaseVerbName === "on") {
|
|
9142
|
+
const handlerArgument = callNode.arguments?.[1];
|
|
9143
|
+
return isNodeOfType(handlerArgument, "Literal") && handlerArgument.value === null;
|
|
9144
|
+
}
|
|
9145
|
+
if (releaseVerbName === "removeEventListener" || releaseVerbName === "removeListener" || releaseVerbName === "off") {
|
|
9146
|
+
const releaseHandler = callNode.arguments?.[1];
|
|
9147
|
+
if (!releaseHandler) return releaseVerbName === "off";
|
|
9148
|
+
return usage.handlerKey !== null && resolveExpressionKey$1(releaseHandler, context) === usage.handlerKey;
|
|
9149
|
+
}
|
|
9150
|
+
if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
|
|
9151
|
+
return true;
|
|
9152
|
+
};
|
|
8835
9153
|
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
8836
|
-
const
|
|
8837
|
-
let
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
return didFindPairedRelease;
|
|
9154
|
+
const isReturnedEffectCleanupFunction = (functionNode) => {
|
|
9155
|
+
let currentNode = functionNode;
|
|
9156
|
+
let parentNode = currentNode.parent;
|
|
9157
|
+
while (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
|
|
9158
|
+
currentNode = parentNode;
|
|
9159
|
+
parentNode = currentNode.parent;
|
|
9160
|
+
}
|
|
9161
|
+
if (!isNodeOfType(parentNode, "ReturnStatement") || parentNode.argument !== currentNode) return false;
|
|
9162
|
+
const effectCallback = findEnclosingFunction(parentNode);
|
|
9163
|
+
const effectCall = effectCallback?.parent;
|
|
9164
|
+
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isHookCall$2(effectCall, CLEANUP_EFFECT_HOOK_NAMES));
|
|
8848
9165
|
};
|
|
8849
|
-
const
|
|
8850
|
-
|
|
8851
|
-
|
|
9166
|
+
const isPotentiallyReachableFunction = (functionNode, context) => {
|
|
9167
|
+
if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode)) return true;
|
|
9168
|
+
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
9169
|
+
if (!bindingIdentifier) return false;
|
|
9170
|
+
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
9171
|
+
if (!symbol) return false;
|
|
9172
|
+
return symbol.references.some((reference) => findEnclosingFunction(reference.identifier) !== functionNode);
|
|
9173
|
+
};
|
|
9174
|
+
const isReleaseReachableForUsage = (releaseNode, usage, context) => {
|
|
9175
|
+
if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
|
|
9176
|
+
const releaseFunction = findEnclosingFunction(releaseNode);
|
|
9177
|
+
if (!releaseFunction) return true;
|
|
9178
|
+
if (releaseFunction === findEnclosingFunction(usage.node)) return true;
|
|
9179
|
+
return isPotentiallyReachableFunction(releaseFunction, context);
|
|
9180
|
+
};
|
|
9181
|
+
const fileContainsReleaseForUsage = (usage, context) => {
|
|
9182
|
+
let programNode = usage.node;
|
|
8852
9183
|
while (programNode.parent) programNode = programNode.parent;
|
|
8853
|
-
|
|
8854
|
-
if (cached) return cached;
|
|
8855
|
-
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
9184
|
+
let didFindRelease = false;
|
|
8856
9185
|
walkAst(programNode, (child) => {
|
|
8857
|
-
|
|
8858
|
-
if (
|
|
9186
|
+
if (didFindRelease) return false;
|
|
9187
|
+
if (doesReleaseCallMatchUsage(child, usage, context) && isReleaseReachableForUsage(child, usage, context)) {
|
|
9188
|
+
didFindRelease = true;
|
|
9189
|
+
return false;
|
|
9190
|
+
}
|
|
8859
9191
|
});
|
|
8860
|
-
|
|
8861
|
-
return releaseVerbNames;
|
|
9192
|
+
return didFindRelease;
|
|
8862
9193
|
};
|
|
8863
|
-
const
|
|
8864
|
-
const
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
9194
|
+
const isUseSyncExternalStoreSubscribeFunction = (functionNode, context) => {
|
|
9195
|
+
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
9196
|
+
if (!bindingIdentifier) return false;
|
|
9197
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
9198
|
+
const isSubscribeBinding = (candidateBinding) => {
|
|
9199
|
+
const symbol = context.scopes.symbolFor(candidateBinding);
|
|
9200
|
+
if (!symbol || visitedSymbolIds.has(symbol.id) || symbol.references.length === 0) return false;
|
|
9201
|
+
visitedSymbolIds.add(symbol.id);
|
|
9202
|
+
return symbol.references.every((reference) => {
|
|
9203
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
9204
|
+
const referenceParent = referenceRoot.parent;
|
|
9205
|
+
if (isNodeOfType(referenceParent, "CallExpression") && referenceParent.arguments?.[0] === referenceRoot) return isReactApiCall(referenceParent, "useSyncExternalStore", context.scopes);
|
|
9206
|
+
const aliasDeclaration = referenceParent?.parent;
|
|
9207
|
+
return Boolean(isNodeOfType(referenceParent, "VariableDeclarator") && referenceParent.init === referenceRoot && isNodeOfType(referenceParent.id, "Identifier") && isNodeOfType(aliasDeclaration, "VariableDeclaration") && aliasDeclaration.kind === "const" && isSubscribeBinding(referenceParent.id));
|
|
9208
|
+
});
|
|
9209
|
+
};
|
|
9210
|
+
return isSubscribeBinding(bindingIdentifier);
|
|
9211
|
+
};
|
|
9212
|
+
const doesResourceResultEscape = (resourceNode, allowConciseReturnEscape) => {
|
|
9213
|
+
let currentNode = resourceNode;
|
|
9214
|
+
let parentNode = currentNode.parent;
|
|
9215
|
+
while (parentNode) {
|
|
9216
|
+
if (isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode) return true;
|
|
9217
|
+
if (isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode && allowConciseReturnEscape) return true;
|
|
9218
|
+
if (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
|
|
9219
|
+
currentNode = parentNode;
|
|
9220
|
+
parentNode = currentNode.parent;
|
|
9221
|
+
continue;
|
|
9222
|
+
}
|
|
9223
|
+
return false;
|
|
9224
|
+
}
|
|
8868
9225
|
return false;
|
|
8869
9226
|
};
|
|
8870
|
-
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
9227
|
+
const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
8871
9228
|
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
8872
9229
|
const body = retainedFunction.body;
|
|
8873
9230
|
if (!body) return null;
|
|
8874
|
-
const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
|
|
8875
9231
|
let leak = null;
|
|
9232
|
+
const allowConciseReturnEscape = !isInlineRetainedHandlerFunction(retainedFunction, context);
|
|
9233
|
+
const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
|
|
9234
|
+
const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context);
|
|
8876
9235
|
walkAst(body, (child) => {
|
|
8877
9236
|
if (leak !== null) return false;
|
|
8878
9237
|
if (isFunctionLike$1(child)) return false;
|
|
8879
|
-
if (child
|
|
8880
|
-
|
|
8881
|
-
leak = {
|
|
9238
|
+
if (isSocketConstruction(child) && !doesResourceResultEscape(child, false)) {
|
|
9239
|
+
const socketUsage = {
|
|
8882
9240
|
kind: "socket",
|
|
8883
9241
|
node: child,
|
|
8884
|
-
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
9242
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket",
|
|
9243
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
9244
|
+
receiverKey: null,
|
|
9245
|
+
registrationVerbName: null,
|
|
9246
|
+
eventKey: null,
|
|
9247
|
+
handlerKey: null
|
|
8885
9248
|
};
|
|
8886
|
-
|
|
9249
|
+
if (!hasReleaseForUsage(socketUsage)) {
|
|
9250
|
+
leak = socketUsage;
|
|
9251
|
+
return false;
|
|
9252
|
+
}
|
|
8887
9253
|
}
|
|
8888
9254
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8889
|
-
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" &&
|
|
8890
|
-
|
|
9255
|
+
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && !doesResourceResultEscape(child, allowConciseReturnEscape)) {
|
|
9256
|
+
const timerUsage = {
|
|
8891
9257
|
kind: "timer",
|
|
8892
9258
|
node: child,
|
|
8893
|
-
resourceName: "setInterval"
|
|
9259
|
+
resourceName: "setInterval",
|
|
9260
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
9261
|
+
receiverKey: null,
|
|
9262
|
+
registrationVerbName: "setInterval",
|
|
9263
|
+
eventKey: null,
|
|
9264
|
+
handlerKey: null
|
|
8894
9265
|
};
|
|
8895
|
-
|
|
9266
|
+
if (!hasReleaseForUsage(timerUsage)) {
|
|
9267
|
+
leak = timerUsage;
|
|
9268
|
+
return false;
|
|
9269
|
+
}
|
|
8896
9270
|
}
|
|
8897
|
-
if (isSubscribeOrObserveCall(child) &&
|
|
8898
|
-
const
|
|
8899
|
-
|
|
9271
|
+
if (isSubscribeOrObserveCall(child) && !doesResourceResultEscape(child, allowConciseReturnEscape)) {
|
|
9272
|
+
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
9273
|
+
const subscriptionUsage = {
|
|
8900
9274
|
kind: "subscribe",
|
|
8901
9275
|
node: child,
|
|
8902
|
-
resourceName: registrationVerbName
|
|
9276
|
+
resourceName: registrationDetails.registrationVerbName ?? "subscribe",
|
|
9277
|
+
handleKey: findAssignedResourceKey(child, context),
|
|
9278
|
+
...registrationDetails
|
|
8903
9279
|
};
|
|
9280
|
+
if (!hasSelfReleasingListenerOptions(child, context) && !hasReleaseForUsage(subscriptionUsage)) leak = subscriptionUsage;
|
|
8904
9281
|
return false;
|
|
8905
9282
|
}
|
|
8906
9283
|
});
|
|
@@ -8912,6 +9289,26 @@ const isRetainedComponentScopeFunction = (functionNode) => {
|
|
|
8912
9289
|
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
8913
9290
|
return enclosingComponentOrHookName(functionNode) !== null;
|
|
8914
9291
|
};
|
|
9292
|
+
const isDirectJsxEventHandlerValue = (expression) => {
|
|
9293
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
9294
|
+
const expressionContainer = expressionRoot.parent;
|
|
9295
|
+
return isNodeOfType(expressionContainer, "JSXExpressionContainer") && expressionContainer.expression === expressionRoot && isEventHandlerAttribute(expressionContainer.parent);
|
|
9296
|
+
};
|
|
9297
|
+
const isInlineRetainedHandlerFunction = (functionNode, context) => {
|
|
9298
|
+
if (!isFunctionLike$1(functionNode)) return false;
|
|
9299
|
+
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
9300
|
+
const callbackCall = functionRoot.parent;
|
|
9301
|
+
if (isNodeOfType(callbackCall, "CallExpression") && callbackCall.arguments?.[0] === functionRoot && isHookCall$2(callbackCall, "useCallback") && isDirectJsxEventHandlerValue(callbackCall)) return true;
|
|
9302
|
+
const parentNode = functionNode.parent;
|
|
9303
|
+
if (isDirectJsxEventHandlerValue(functionNode)) return true;
|
|
9304
|
+
if (!isNodeOfType(parentNode, "Property") || parentNode.value !== functionNode || parentNode.computed) return false;
|
|
9305
|
+
const propertyName = getStaticPropertyKeyName(parentNode);
|
|
9306
|
+
if (!propertyName || !/^on[A-Z]/.test(propertyName)) return false;
|
|
9307
|
+
const objectExpression = parentNode.parent;
|
|
9308
|
+
if (!isNodeOfType(objectExpression, "ObjectExpression")) return false;
|
|
9309
|
+
const objectParent = objectExpression.parent;
|
|
9310
|
+
return (isNodeOfType(objectParent, "CallExpression") && objectParent.arguments.some((argument) => argument === objectExpression) || isNodeOfType(objectParent, "JSXExpressionContainer")) && findRenderPhaseComponentOrHook(parentNode, context.scopes) !== null;
|
|
9311
|
+
};
|
|
8915
9312
|
const effectNeedsCleanup = defineRule({
|
|
8916
9313
|
id: "effect-needs-cleanup",
|
|
8917
9314
|
title: "Effect subscription or timer never cleaned up",
|
|
@@ -8920,7 +9317,8 @@ const effectNeedsCleanup = defineRule({
|
|
|
8920
9317
|
recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",
|
|
8921
9318
|
create: (context) => {
|
|
8922
9319
|
const reportRetainedLeak = (retainedFunction) => {
|
|
8923
|
-
|
|
9320
|
+
if (!isPotentiallyReachableFunction(retainedFunction, context)) return;
|
|
9321
|
+
const leak = findRetainedFunctionLeak(retainedFunction, context);
|
|
8924
9322
|
if (!leak) return;
|
|
8925
9323
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
8926
9324
|
context.report({
|
|
@@ -8932,30 +9330,31 @@ const effectNeedsCleanup = defineRule({
|
|
|
8932
9330
|
CallExpression(node) {
|
|
8933
9331
|
if (isHookCall$2(node, "useCallback")) {
|
|
8934
9332
|
const retainedCallback = getEffectCallback(node);
|
|
8935
|
-
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
9333
|
+
if (retainedCallback && !isInlineRetainedHandlerFunction(retainedCallback, context)) reportRetainedLeak(retainedCallback);
|
|
8936
9334
|
return;
|
|
8937
9335
|
}
|
|
8938
|
-
if (!isHookCall$2(node,
|
|
9336
|
+
if (!isHookCall$2(node, CLEANUP_EFFECT_HOOK_NAMES)) return;
|
|
8939
9337
|
const callback = getEffectCallback(node);
|
|
8940
9338
|
if (!callback) return;
|
|
8941
|
-
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
|
|
9339
|
+
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback, context), context);
|
|
8942
9340
|
if (usages.length === 0) return;
|
|
8943
|
-
|
|
8944
|
-
|
|
9341
|
+
const firstUsage = findFirstUsageWithoutCleanup(callback, usages, context);
|
|
9342
|
+
if (!firstUsage) return;
|
|
8945
9343
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
9344
|
+
const hookName = getCalleeName$2(node) ?? "effect";
|
|
8946
9345
|
context.report({
|
|
8947
9346
|
node,
|
|
8948
|
-
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in
|
|
9347
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in ${hookName} without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
8949
9348
|
});
|
|
8950
9349
|
},
|
|
8951
9350
|
FunctionDeclaration(node) {
|
|
8952
9351
|
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8953
9352
|
},
|
|
8954
9353
|
ArrowFunctionExpression(node) {
|
|
8955
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9354
|
+
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
|
|
8956
9355
|
},
|
|
8957
9356
|
FunctionExpression(node) {
|
|
8958
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9357
|
+
if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
|
|
8959
9358
|
}
|
|
8960
9359
|
};
|
|
8961
9360
|
}
|
|
@@ -12339,20 +12738,6 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
12339
12738
|
return names;
|
|
12340
12739
|
};
|
|
12341
12740
|
//#endregion
|
|
12342
|
-
//#region src/plugin/utils/get-function-binding-name.ts
|
|
12343
|
-
const getFunctionBindingIdentifier = (functionNode) => {
|
|
12344
|
-
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
12345
|
-
const parent = functionNode.parent;
|
|
12346
|
-
if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
|
|
12347
|
-
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionNode && isNodeOfType(parent.left, "Identifier")) return parent.left;
|
|
12348
|
-
if (isNodeOfType(parent, "CallExpression")) {
|
|
12349
|
-
const callParent = parent.parent;
|
|
12350
|
-
if (isNodeOfType(callParent, "VariableDeclarator") && isNodeOfType(callParent.id, "Identifier")) return callParent.id;
|
|
12351
|
-
}
|
|
12352
|
-
return null;
|
|
12353
|
-
};
|
|
12354
|
-
const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(functionNode)?.name ?? null;
|
|
12355
|
-
//#endregion
|
|
12356
12741
|
//#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
|
|
12357
12742
|
const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
|
|
12358
12743
|
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
@@ -13497,7 +13882,7 @@ const jsHoistIntl = defineRule({
|
|
|
13497
13882
|
});
|
|
13498
13883
|
//#endregion
|
|
13499
13884
|
//#region src/plugin/utils/create-loop-aware-visitors.ts
|
|
13500
|
-
const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
13885
|
+
const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
|
|
13501
13886
|
"map",
|
|
13502
13887
|
"flatMap",
|
|
13503
13888
|
"forEach",
|
|
@@ -13516,7 +13901,7 @@ const isIteratorCallback = (node) => {
|
|
|
13516
13901
|
const parent = node.parent;
|
|
13517
13902
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
13518
13903
|
if (!parent.arguments.includes(node)) return false;
|
|
13519
|
-
return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(parent.callee.property.name);
|
|
13904
|
+
return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(parent.callee.property.name);
|
|
13520
13905
|
};
|
|
13521
13906
|
const createLoopAwareVisitors = (innerVisitors, options = {}) => {
|
|
13522
13907
|
let loopDepth = 0;
|
|
@@ -13745,7 +14130,7 @@ const findIndexedArrayObject = (callbackBody, indexParameterName) => {
|
|
|
13745
14130
|
});
|
|
13746
14131
|
return indexedArrayObject;
|
|
13747
14132
|
};
|
|
13748
|
-
const unwrapChainExpression$
|
|
14133
|
+
const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
13749
14134
|
const LENGTH_EQUALITY_OPERATORS = new Set(["===", "=="]);
|
|
13750
14135
|
const LENGTH_MISMATCH_OPERATORS = new Set([
|
|
13751
14136
|
"!==",
|
|
@@ -13757,17 +14142,17 @@ const LENGTH_MISMATCH_OPERATORS = new Set([
|
|
|
13757
14142
|
]);
|
|
13758
14143
|
const LENGTH_ANY_COMPARISON_OPERATORS = new Set([...LENGTH_EQUALITY_OPERATORS, ...LENGTH_MISMATCH_OPERATORS]);
|
|
13759
14144
|
const isLengthComparison = (candidate, receiverArray, indexedArray, operators) => {
|
|
13760
|
-
const binaryGuard = unwrapChainExpression$
|
|
14145
|
+
const binaryGuard = unwrapChainExpression$3(candidate);
|
|
13761
14146
|
if (!isNodeOfType(binaryGuard, "BinaryExpression")) return false;
|
|
13762
14147
|
if (!operators.has(binaryGuard.operator)) return false;
|
|
13763
|
-
const leftSide = unwrapChainExpression$
|
|
13764
|
-
const rightSide = unwrapChainExpression$
|
|
14148
|
+
const leftSide = unwrapChainExpression$3(binaryGuard.left);
|
|
14149
|
+
const rightSide = unwrapChainExpression$3(binaryGuard.right);
|
|
13765
14150
|
if (!isMemberProperty(leftSide, "length")) return false;
|
|
13766
14151
|
if (!isMemberProperty(rightSide, "length")) return false;
|
|
13767
|
-
const leftLengthObject = unwrapChainExpression$
|
|
13768
|
-
const rightLengthObject = unwrapChainExpression$
|
|
13769
|
-
const normalizedReceiver = unwrapChainExpression$
|
|
13770
|
-
const normalizedIndexed = unwrapChainExpression$
|
|
14152
|
+
const leftLengthObject = unwrapChainExpression$3(leftSide.object);
|
|
14153
|
+
const rightLengthObject = unwrapChainExpression$3(rightSide.object);
|
|
14154
|
+
const normalizedReceiver = unwrapChainExpression$3(receiverArray);
|
|
14155
|
+
const normalizedIndexed = unwrapChainExpression$3(indexedArray);
|
|
13771
14156
|
const matchesReceiverThenIndexed = areExpressionsStructurallyEqual(leftLengthObject, normalizedReceiver) && areExpressionsStructurallyEqual(rightLengthObject, normalizedIndexed);
|
|
13772
14157
|
const matchesIndexedThenReceiver = areExpressionsStructurallyEqual(leftLengthObject, normalizedIndexed) && areExpressionsStructurallyEqual(rightLengthObject, normalizedReceiver);
|
|
13773
14158
|
return matchesReceiverThenIndexed || matchesIndexedThenReceiver;
|
|
@@ -13854,18 +14239,18 @@ const LENGTH_PRESERVING_METHOD_NAMES = new Set([
|
|
|
13854
14239
|
"toReversed"
|
|
13855
14240
|
]);
|
|
13856
14241
|
const peelLengthPreservingDerivation = (expression) => {
|
|
13857
|
-
let current = unwrapChainExpression$
|
|
14242
|
+
let current = unwrapChainExpression$3(expression);
|
|
13858
14243
|
for (;;) {
|
|
13859
14244
|
if (isNodeOfType(current, "ArrayExpression") && current.elements?.length === 1 && isNodeOfType(current.elements[0], "SpreadElement")) {
|
|
13860
|
-
current = unwrapChainExpression$
|
|
14245
|
+
current = unwrapChainExpression$3(current.elements[0].argument);
|
|
13861
14246
|
continue;
|
|
13862
14247
|
}
|
|
13863
14248
|
if (isNodeOfType(current, "CallExpression")) {
|
|
13864
|
-
const callee = unwrapChainExpression$
|
|
14249
|
+
const callee = unwrapChainExpression$3(current.callee);
|
|
13865
14250
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
13866
|
-
const calleeObject = unwrapChainExpression$
|
|
14251
|
+
const calleeObject = unwrapChainExpression$3(callee.object);
|
|
13867
14252
|
if (isNodeOfType(calleeObject, "Identifier") && calleeObject.name === "Array" && callee.property.name === "from" && current.arguments?.length === 1) {
|
|
13868
|
-
current = unwrapChainExpression$
|
|
14253
|
+
current = unwrapChainExpression$3(current.arguments[0]);
|
|
13869
14254
|
continue;
|
|
13870
14255
|
}
|
|
13871
14256
|
if (LENGTH_PRESERVING_METHOD_NAMES.has(callee.property.name)) {
|
|
@@ -13910,7 +14295,7 @@ const resolveComparedArraySource = (expression, scopeNode) => {
|
|
|
13910
14295
|
const initializer = findConstInitializer(current.name, scopeNode);
|
|
13911
14296
|
if (!initializer) return current;
|
|
13912
14297
|
const peeledInitializer = peelLengthPreservingDerivation(initializer);
|
|
13913
|
-
if (peeledInitializer === unwrapChainExpression$
|
|
14298
|
+
if (peeledInitializer === unwrapChainExpression$3(initializer)) return current;
|
|
13914
14299
|
current = peeledInitializer;
|
|
13915
14300
|
}
|
|
13916
14301
|
return current;
|
|
@@ -19640,54 +20025,6 @@ const nextjsNoAElement = defineRule({
|
|
|
19640
20025
|
} })
|
|
19641
20026
|
});
|
|
19642
20027
|
//#endregion
|
|
19643
|
-
//#region src/plugin/utils/collect-effect-invoked-functions.ts
|
|
19644
|
-
const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
|
|
19645
|
-
"then",
|
|
19646
|
-
"catch",
|
|
19647
|
-
"finally"
|
|
19648
|
-
]);
|
|
19649
|
-
const collectEffectInvokedFunctions = (effectCallback) => {
|
|
19650
|
-
const invokedFunctions = new Set([effectCallback]);
|
|
19651
|
-
const localFunctionBindings = /* @__PURE__ */ new Map();
|
|
19652
|
-
const calledBindingNames = /* @__PURE__ */ new Set();
|
|
19653
|
-
const pendingFunctions = [effectCallback];
|
|
19654
|
-
const enqueue = (candidate) => {
|
|
19655
|
-
const strippedCandidate = candidate ? stripParenExpression(candidate) : candidate;
|
|
19656
|
-
if (!isFunctionLike$1(strippedCandidate) || invokedFunctions.has(strippedCandidate)) return;
|
|
19657
|
-
invokedFunctions.add(strippedCandidate);
|
|
19658
|
-
pendingFunctions.push(strippedCandidate);
|
|
19659
|
-
};
|
|
19660
|
-
const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
|
|
19661
|
-
while (pendingFunctions.length > 0) {
|
|
19662
|
-
const currentFunction = pendingFunctions.pop();
|
|
19663
|
-
if (!currentFunction) break;
|
|
19664
|
-
walkAst(currentFunction, (child) => {
|
|
19665
|
-
if (child !== currentFunction && isFunctionLike$1(child)) {
|
|
19666
|
-
if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) localFunctionBindings.set(child.id.name, child);
|
|
19667
|
-
return false;
|
|
19668
|
-
}
|
|
19669
|
-
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
19670
|
-
const initializer = child.init ? stripParenExpression(child.init) : null;
|
|
19671
|
-
if (isFunctionLike$1(initializer)) localFunctionBindings.set(child.id.name, initializer);
|
|
19672
|
-
return;
|
|
19673
|
-
}
|
|
19674
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
19675
|
-
const callee = stripParenExpression(child.callee);
|
|
19676
|
-
if (isFunctionLike$1(callee)) {
|
|
19677
|
-
enqueue(callee);
|
|
19678
|
-
return;
|
|
19679
|
-
}
|
|
19680
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
19681
|
-
calledBindingNames.add(callee.name);
|
|
19682
|
-
return;
|
|
19683
|
-
}
|
|
19684
|
-
if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
19685
|
-
});
|
|
19686
|
-
for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
|
|
19687
|
-
}
|
|
19688
|
-
return invokedFunctions;
|
|
19689
|
-
};
|
|
19690
|
-
//#endregion
|
|
19691
20028
|
//#region src/plugin/utils/contains-fetch-call.ts
|
|
19692
20029
|
const isFetchCall$1 = (node) => {
|
|
19693
20030
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
@@ -20671,6 +21008,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
20671
21008
|
continue;
|
|
20672
21009
|
}
|
|
20673
21010
|
if (isNodeOfType(statement, "ExportNamedDeclaration")) {
|
|
21011
|
+
if (statement.exportKind === "type") continue;
|
|
20674
21012
|
const declaration = statement.declaration;
|
|
20675
21013
|
if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
|
|
20676
21014
|
recordVariableDeclaration(declaration);
|
|
@@ -20685,6 +21023,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
20685
21023
|
}
|
|
20686
21024
|
for (const specifier of statement.specifiers ?? []) {
|
|
20687
21025
|
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
21026
|
+
if (specifier.exportKind === "type") continue;
|
|
20688
21027
|
const local = specifier.local;
|
|
20689
21028
|
const exported = specifier.exported;
|
|
20690
21029
|
if (!isNodeOfType(local, "Identifier")) continue;
|
|
@@ -20733,25 +21072,37 @@ const resolveImportedExportName = (importSpecifier) => {
|
|
|
20733
21072
|
if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
|
|
20734
21073
|
return null;
|
|
20735
21074
|
};
|
|
20736
|
-
const
|
|
21075
|
+
const findReExportTargetsForName = (programRoot, exportedName) => {
|
|
20737
21076
|
if (!isNodeOfType(programRoot, "Program")) return [];
|
|
20738
|
-
const
|
|
21077
|
+
const exportAllTargets = [];
|
|
20739
21078
|
for (const statement of programRoot.body ?? []) {
|
|
20740
21079
|
if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
|
|
21080
|
+
if (statement.exportKind === "type") continue;
|
|
20741
21081
|
const sourceValue = statement.source.value;
|
|
20742
21082
|
if (typeof sourceValue !== "string") continue;
|
|
20743
21083
|
for (const specifier of statement.specifiers ?? []) {
|
|
20744
21084
|
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
21085
|
+
if (specifier.exportKind === "type") continue;
|
|
20745
21086
|
const exported = specifier.exported;
|
|
20746
|
-
if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null)
|
|
21087
|
+
if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) !== exportedName) continue;
|
|
21088
|
+
const local = specifier.local;
|
|
21089
|
+
const importedName = isNodeOfType(local, "Identifier") ? local.name : isNodeOfType(local, "Literal") && typeof local.value === "string" ? local.value : null;
|
|
21090
|
+
if (importedName) return [{
|
|
21091
|
+
importedName,
|
|
21092
|
+
source: sourceValue
|
|
21093
|
+
}];
|
|
20747
21094
|
}
|
|
20748
21095
|
}
|
|
20749
21096
|
if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
|
|
21097
|
+
if (statement.exportKind === "type" || statement.exported) continue;
|
|
20750
21098
|
const sourceValue = statement.source.value;
|
|
20751
|
-
if (typeof sourceValue === "string")
|
|
21099
|
+
if (typeof sourceValue === "string") exportAllTargets.push({
|
|
21100
|
+
importedName: exportedName,
|
|
21101
|
+
source: sourceValue
|
|
21102
|
+
});
|
|
20752
21103
|
}
|
|
20753
21104
|
}
|
|
20754
|
-
return
|
|
21105
|
+
return exportAllTargets;
|
|
20755
21106
|
};
|
|
20756
21107
|
//#endregion
|
|
20757
21108
|
//#region src/plugin/utils/attach-parent-references.ts
|
|
@@ -20840,139 +21191,6 @@ const parseSourceFile = (absoluteFilePath) => {
|
|
|
20840
21191
|
return parsedProgram;
|
|
20841
21192
|
};
|
|
20842
21193
|
//#endregion
|
|
20843
|
-
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
20844
|
-
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
20845
|
-
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
20846
|
-
const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
20847
|
-
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
20848
|
-
const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
|
|
20849
|
-
const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
|
|
20850
|
-
const createNonBarrelInfo = () => ({
|
|
20851
|
-
isBarrel: false,
|
|
20852
|
-
exportsByName: /* @__PURE__ */ new Map(),
|
|
20853
|
-
starExportSources: []
|
|
20854
|
-
});
|
|
20855
|
-
const addImportedBinding = (importedBindings, binding) => {
|
|
20856
|
-
importedBindings.set(binding.localName, {
|
|
20857
|
-
...binding,
|
|
20858
|
-
didExport: false
|
|
20859
|
-
});
|
|
20860
|
-
};
|
|
20861
|
-
const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
|
|
20862
|
-
for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
|
|
20863
|
-
localName: specifier.exportedName,
|
|
20864
|
-
importedName: specifier.localName,
|
|
20865
|
-
source,
|
|
20866
|
-
isTypeOnly: specifier.isTypeOnly
|
|
20867
|
-
});
|
|
20868
|
-
};
|
|
20869
|
-
const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
|
|
20870
|
-
const trimmedImportClause = importClause.trim();
|
|
20871
|
-
const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
|
|
20872
|
-
if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
|
|
20873
|
-
localName: namespaceMatch[1],
|
|
20874
|
-
importedName: "*",
|
|
20875
|
-
source,
|
|
20876
|
-
isTypeOnly: declarationIsTypeOnly
|
|
20877
|
-
});
|
|
20878
|
-
const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
|
|
20879
|
-
if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
|
|
20880
|
-
const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
|
|
20881
|
-
if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
|
|
20882
|
-
localName: defaultImportName,
|
|
20883
|
-
importedName: "default",
|
|
20884
|
-
source,
|
|
20885
|
-
isTypeOnly: declarationIsTypeOnly
|
|
20886
|
-
});
|
|
20887
|
-
};
|
|
20888
|
-
const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
|
|
20889
|
-
let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
|
|
20890
|
-
collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
|
|
20891
|
-
return "";
|
|
20892
|
-
});
|
|
20893
|
-
withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
|
|
20894
|
-
const isTypeOnly = Boolean(typeKeyword);
|
|
20895
|
-
if (namespaceExportName) {
|
|
20896
|
-
exportsByName.set(namespaceExportName, {
|
|
20897
|
-
exportedName: namespaceExportName,
|
|
20898
|
-
importedName: "*",
|
|
20899
|
-
source,
|
|
20900
|
-
isTypeOnly
|
|
20901
|
-
});
|
|
20902
|
-
return "";
|
|
20903
|
-
}
|
|
20904
|
-
if (specifiersText) {
|
|
20905
|
-
for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
|
|
20906
|
-
exportedName: specifier.exportedName,
|
|
20907
|
-
importedName: specifier.localName,
|
|
20908
|
-
source,
|
|
20909
|
-
isTypeOnly: specifier.isTypeOnly
|
|
20910
|
-
});
|
|
20911
|
-
return "";
|
|
20912
|
-
}
|
|
20913
|
-
starExportSources.push(source);
|
|
20914
|
-
return "";
|
|
20915
|
-
});
|
|
20916
|
-
withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
|
|
20917
|
-
for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
|
|
20918
|
-
const importedBinding = importedBindings.get(specifier.localName);
|
|
20919
|
-
if (!importedBinding) return _match;
|
|
20920
|
-
importedBinding.didExport = true;
|
|
20921
|
-
exportsByName.set(specifier.exportedName, {
|
|
20922
|
-
exportedName: specifier.exportedName,
|
|
20923
|
-
importedName: importedBinding.importedName,
|
|
20924
|
-
source: importedBinding.source,
|
|
20925
|
-
isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
|
|
20926
|
-
});
|
|
20927
|
-
}
|
|
20928
|
-
return "";
|
|
20929
|
-
});
|
|
20930
|
-
return withoutKnownDeclarations;
|
|
20931
|
-
};
|
|
20932
|
-
const hasUnexportedRuntimeImport = (importedBindings) => {
|
|
20933
|
-
for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
|
|
20934
|
-
return false;
|
|
20935
|
-
};
|
|
20936
|
-
const classifyBarrelModule = (sourceText) => {
|
|
20937
|
-
const strippedSource = stripJsComments(sourceText).trim();
|
|
20938
|
-
if (!strippedSource) return createNonBarrelInfo();
|
|
20939
|
-
const importedBindings = /* @__PURE__ */ new Map();
|
|
20940
|
-
const exportsByName = /* @__PURE__ */ new Map();
|
|
20941
|
-
const starExportSources = [];
|
|
20942
|
-
if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
|
|
20943
|
-
return {
|
|
20944
|
-
isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
|
|
20945
|
-
exportsByName,
|
|
20946
|
-
starExportSources
|
|
20947
|
-
};
|
|
20948
|
-
};
|
|
20949
|
-
const getBarrelIndexModuleInfo = (filePath) => {
|
|
20950
|
-
if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
|
|
20951
|
-
recordContentProbe(filePath);
|
|
20952
|
-
let fileStat;
|
|
20953
|
-
try {
|
|
20954
|
-
fileStat = fs.statSync(filePath);
|
|
20955
|
-
} catch {
|
|
20956
|
-
fileStat = null;
|
|
20957
|
-
}
|
|
20958
|
-
const cachedResult = barrelIndexModuleInfoCache.get(filePath);
|
|
20959
|
-
if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
|
|
20960
|
-
if (fileStat === null) return createNonBarrelInfo();
|
|
20961
|
-
let moduleInfo = createNonBarrelInfo();
|
|
20962
|
-
try {
|
|
20963
|
-
moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
|
|
20964
|
-
} catch {
|
|
20965
|
-
moduleInfo = createNonBarrelInfo();
|
|
20966
|
-
}
|
|
20967
|
-
barrelIndexModuleInfoCache.set(filePath, {
|
|
20968
|
-
mtimeMs: fileStat.mtimeMs,
|
|
20969
|
-
size: fileStat.size,
|
|
20970
|
-
moduleInfo
|
|
20971
|
-
});
|
|
20972
|
-
return moduleInfo;
|
|
20973
|
-
};
|
|
20974
|
-
const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
|
|
20975
|
-
//#endregion
|
|
20976
21194
|
//#region src/plugin/utils/resolve-relative-import-path.ts
|
|
20977
21195
|
const MODULE_FILE_EXTENSIONS = [
|
|
20978
21196
|
".ts",
|
|
@@ -21089,35 +21307,6 @@ const resolveModuleFileFromAbsolutePath = (importPath) => {
|
|
|
21089
21307
|
};
|
|
21090
21308
|
const resolveRelativeImportPath = (filename, source) => resolveModuleFileFromAbsolutePath(path.resolve(path.dirname(filename), source));
|
|
21091
21309
|
//#endregion
|
|
21092
|
-
//#region src/plugin/utils/resolve-barrel-export-file-path.ts
|
|
21093
|
-
const getUniqueFilePath = (filePaths) => {
|
|
21094
|
-
const uniqueFilePaths = new Set(filePaths);
|
|
21095
|
-
if (uniqueFilePaths.size !== 1) return null;
|
|
21096
|
-
const [filePath] = uniqueFilePaths;
|
|
21097
|
-
return filePath ?? null;
|
|
21098
|
-
};
|
|
21099
|
-
const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
|
|
21100
|
-
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
|
|
21101
|
-
if (!resolvedTargetPath) return null;
|
|
21102
|
-
const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
|
|
21103
|
-
if (nestedTargetPath) return nestedTargetPath;
|
|
21104
|
-
return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
|
|
21105
|
-
};
|
|
21106
|
-
const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
|
|
21107
|
-
if (visitedFilePaths.has(barrelFilePath)) return null;
|
|
21108
|
-
visitedFilePaths.add(barrelFilePath);
|
|
21109
|
-
const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
|
|
21110
|
-
if (!moduleInfo.isBarrel) return null;
|
|
21111
|
-
const target = moduleInfo.exportsByName.get(exportedName);
|
|
21112
|
-
if (target) {
|
|
21113
|
-
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
|
|
21114
|
-
if (!resolvedTargetPath) return null;
|
|
21115
|
-
return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
|
|
21116
|
-
}
|
|
21117
|
-
if (exportedName === "default") return null;
|
|
21118
|
-
return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
|
|
21119
|
-
};
|
|
21120
|
-
//#endregion
|
|
21121
21310
|
//#region src/plugin/utils/resolve-tsconfig-alias.ts
|
|
21122
21311
|
const TSCONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
|
|
21123
21312
|
const isObjectRecord = (value) => typeof value === "object" && value !== null;
|
|
@@ -21296,25 +21485,29 @@ const resolveTsconfigAliasPath = (fromFilename, source) => {
|
|
|
21296
21485
|
};
|
|
21297
21486
|
//#endregion
|
|
21298
21487
|
//#region src/plugin/utils/resolve-module-path.ts
|
|
21299
|
-
const resolveModulePath = (fromFilename, source) =>
|
|
21488
|
+
const resolveModulePath = (fromFilename, source) => {
|
|
21489
|
+
if (path.isAbsolute(source)) return null;
|
|
21490
|
+
return source.startsWith(".") ? resolveRelativeImportPath(fromFilename, source) : resolveTsconfigAliasPath(fromFilename, source);
|
|
21491
|
+
};
|
|
21300
21492
|
//#endregion
|
|
21301
21493
|
//#region src/plugin/utils/resolve-cross-file-function-export.ts
|
|
21302
21494
|
const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
|
|
21303
21495
|
if (visitedFilePaths.size >= 4) return null;
|
|
21304
21496
|
if (visitedFilePaths.has(filePath)) return null;
|
|
21305
21497
|
visitedFilePaths.add(filePath);
|
|
21306
|
-
const
|
|
21307
|
-
const programRoot = parseSourceFile(actualFilePath);
|
|
21498
|
+
const programRoot = parseSourceFile(filePath);
|
|
21308
21499
|
if (!programRoot) return null;
|
|
21309
21500
|
const exported = findExportedFunctionBody(programRoot, exportedName);
|
|
21310
21501
|
if (exported) return exported;
|
|
21311
|
-
|
|
21312
|
-
|
|
21502
|
+
const resolvedCandidates = /* @__PURE__ */ new Set();
|
|
21503
|
+
for (const target of findReExportTargetsForName(programRoot, exportedName)) {
|
|
21504
|
+
const nextFilePath = resolveModulePath(filePath, target.source);
|
|
21313
21505
|
if (!nextFilePath) continue;
|
|
21314
|
-
const resolved = resolveFunctionExportInFile(nextFilePath,
|
|
21315
|
-
if (resolved)
|
|
21506
|
+
const resolved = resolveFunctionExportInFile(nextFilePath, target.importedName, new Set(visitedFilePaths));
|
|
21507
|
+
if (resolved) resolvedCandidates.add(resolved);
|
|
21316
21508
|
}
|
|
21317
|
-
return null;
|
|
21509
|
+
if (resolvedCandidates.size !== 1) return null;
|
|
21510
|
+
return resolvedCandidates.values().next().value ?? null;
|
|
21318
21511
|
};
|
|
21319
21512
|
const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
|
|
21320
21513
|
const resolvedFilePath = resolveModulePath(fromFilename, source);
|
|
@@ -22497,6 +22690,46 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
|
|
|
22497
22690
|
"parseInt"
|
|
22498
22691
|
]);
|
|
22499
22692
|
const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
|
|
22693
|
+
const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
|
|
22694
|
+
"isRawJSON",
|
|
22695
|
+
"parse",
|
|
22696
|
+
"rawJSON",
|
|
22697
|
+
"stringify"
|
|
22698
|
+
])], ["Math", new Set([
|
|
22699
|
+
"abs",
|
|
22700
|
+
"acos",
|
|
22701
|
+
"acosh",
|
|
22702
|
+
"asin",
|
|
22703
|
+
"asinh",
|
|
22704
|
+
"atan",
|
|
22705
|
+
"atan2",
|
|
22706
|
+
"atanh",
|
|
22707
|
+
"cbrt",
|
|
22708
|
+
"ceil",
|
|
22709
|
+
"clz32",
|
|
22710
|
+
"cos",
|
|
22711
|
+
"cosh",
|
|
22712
|
+
"exp",
|
|
22713
|
+
"floor",
|
|
22714
|
+
"fround",
|
|
22715
|
+
"hypot",
|
|
22716
|
+
"imul",
|
|
22717
|
+
"log",
|
|
22718
|
+
"log10",
|
|
22719
|
+
"log1p",
|
|
22720
|
+
"log2",
|
|
22721
|
+
"max",
|
|
22722
|
+
"min",
|
|
22723
|
+
"pow",
|
|
22724
|
+
"round",
|
|
22725
|
+
"sign",
|
|
22726
|
+
"sin",
|
|
22727
|
+
"sinh",
|
|
22728
|
+
"sqrt",
|
|
22729
|
+
"tan",
|
|
22730
|
+
"tanh",
|
|
22731
|
+
"trunc"
|
|
22732
|
+
])]]);
|
|
22500
22733
|
const PURE_MEMBER_TRANSFORM_NAMES = new Set([
|
|
22501
22734
|
"concat",
|
|
22502
22735
|
"filter",
|
|
@@ -22543,6 +22776,42 @@ const getParameterBindingIdentity = (analysis, functionNode, parameter) => {
|
|
|
22543
22776
|
}
|
|
22544
22777
|
return parameter;
|
|
22545
22778
|
};
|
|
22779
|
+
const isAsyncOrGeneratorFunction = (functionNode) => Boolean(functionNode.async === true || functionNode.generator === true);
|
|
22780
|
+
const isModuleFunction = (functionNode) => {
|
|
22781
|
+
let ancestor = functionNode.parent;
|
|
22782
|
+
while (ancestor) {
|
|
22783
|
+
if (isFunctionLike$1(ancestor)) return false;
|
|
22784
|
+
if (isNodeOfType(ancestor, "Program")) return true;
|
|
22785
|
+
ancestor = ancestor.parent;
|
|
22786
|
+
}
|
|
22787
|
+
return false;
|
|
22788
|
+
};
|
|
22789
|
+
const getFunctionBindingNames = (functionNode) => {
|
|
22790
|
+
const names = /* @__PURE__ */ new Set();
|
|
22791
|
+
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) names.add(functionNode.id.name);
|
|
22792
|
+
const parent = functionNode.parent;
|
|
22793
|
+
if (parent && isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) names.add(parent.id.name);
|
|
22794
|
+
return names;
|
|
22795
|
+
};
|
|
22796
|
+
const collectModuleBindingNames = (functionNode) => {
|
|
22797
|
+
let program = functionNode.parent;
|
|
22798
|
+
while (program && !isNodeOfType(program, "Program")) program = program.parent;
|
|
22799
|
+
const bindingNames = /* @__PURE__ */ new Set();
|
|
22800
|
+
if (!program || !isNodeOfType(program, "Program")) return bindingNames;
|
|
22801
|
+
for (const statement of program.body ?? []) {
|
|
22802
|
+
if (isNodeOfType(statement, "ImportDeclaration")) {
|
|
22803
|
+
for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier.local, "Identifier")) bindingNames.add(specifier.local.name);
|
|
22804
|
+
continue;
|
|
22805
|
+
}
|
|
22806
|
+
const declaration = isNodeOfType(statement, "ExportNamedDeclaration") || isNodeOfType(statement, "ExportDefaultDeclaration") ? statement.declaration : statement;
|
|
22807
|
+
if (isNodeOfType(declaration, "VariableDeclaration")) {
|
|
22808
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, bindingNames);
|
|
22809
|
+
continue;
|
|
22810
|
+
}
|
|
22811
|
+
if ((isNodeOfType(declaration, "FunctionDeclaration") || isNodeOfType(declaration, "ClassDeclaration")) && isNodeOfType(declaration.id, "Identifier")) bindingNames.add(declaration.id.name);
|
|
22812
|
+
}
|
|
22813
|
+
return bindingNames;
|
|
22814
|
+
};
|
|
22546
22815
|
const buildSubstitutions = (analysis, functionNode, argumentExpressions, parentFrame) => {
|
|
22547
22816
|
const substitutions = /* @__PURE__ */ new Map();
|
|
22548
22817
|
const parameters = getFunctionParameters(functionNode);
|
|
@@ -22673,7 +22942,7 @@ const collectIntroducedBindings = (analysis, functionNode) => {
|
|
|
22673
22942
|
for (const parameter of getFunctionParameters(functionNode)) if (isNodeOfType(parameter, "Identifier")) introducedBindings.add(getParameterBindingIdentity(analysis, functionNode, parameter));
|
|
22674
22943
|
return introducedBindings;
|
|
22675
22944
|
};
|
|
22676
|
-
const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
|
|
22945
|
+
const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilename) => {
|
|
22677
22946
|
const effectFunction = getEffectFn(analysis, effectNode);
|
|
22678
22947
|
if (!effectFunction || !isFunctionLike$1(effectFunction) || effectFunction.async === true) return [];
|
|
22679
22948
|
const invokedFunctionEvidence = collectEffectInvokedFunctions(effectFunction);
|
|
@@ -22682,7 +22951,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
|
|
|
22682
22951
|
invocation: null,
|
|
22683
22952
|
isDeferred: false,
|
|
22684
22953
|
introducedBindings: /* @__PURE__ */ new Set(),
|
|
22685
|
-
substitutions: /* @__PURE__ */ new Map()
|
|
22954
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
22955
|
+
currentFilename
|
|
22686
22956
|
};
|
|
22687
22957
|
const frames = [rootFrame];
|
|
22688
22958
|
walkAst(effectFunction, (child) => {
|
|
@@ -22703,7 +22973,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
|
|
|
22703
22973
|
invocation: child,
|
|
22704
22974
|
isDeferred,
|
|
22705
22975
|
introducedBindings,
|
|
22706
|
-
substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame)
|
|
22976
|
+
substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame),
|
|
22977
|
+
currentFilename
|
|
22707
22978
|
});
|
|
22708
22979
|
};
|
|
22709
22980
|
if (isFunctionLike$1(callee)) {
|
|
@@ -22756,6 +23027,174 @@ const isOpaqueHookCall = (callExpression) => {
|
|
|
22756
23027
|
const calleeName = getCallCalleeName$1(callExpression);
|
|
22757
23028
|
return Boolean(calleeName && /^use[A-Z0-9]/.test(calleeName) && calleeName !== "useMemo" && calleeName !== "useCallback" && calleeName !== "useEffectEvent");
|
|
22758
23029
|
};
|
|
23030
|
+
const analyzeHelperStatements = (statements, environment, usedParameterIndices, analyzeExpression) => {
|
|
23031
|
+
let canContinue = true;
|
|
23032
|
+
for (const statement of statements) {
|
|
23033
|
+
if (!canContinue) {
|
|
23034
|
+
if (!isNodeOfType(statement, "EmptyStatement")) return {
|
|
23035
|
+
canContinue: false,
|
|
23036
|
+
isValid: false
|
|
23037
|
+
};
|
|
23038
|
+
continue;
|
|
23039
|
+
}
|
|
23040
|
+
if (isNodeOfType(statement, "EmptyStatement")) continue;
|
|
23041
|
+
if (isNodeOfType(statement, "ReturnStatement")) {
|
|
23042
|
+
if (!statement.argument || !analyzeExpression(statement.argument, environment, usedParameterIndices)) return {
|
|
23043
|
+
canContinue: false,
|
|
23044
|
+
isValid: false
|
|
23045
|
+
};
|
|
23046
|
+
canContinue = false;
|
|
23047
|
+
continue;
|
|
23048
|
+
}
|
|
23049
|
+
if (isNodeOfType(statement, "BlockStatement")) {
|
|
23050
|
+
const blockSummary = analyzeHelperStatements(statement.body ?? [], environment, usedParameterIndices, analyzeExpression);
|
|
23051
|
+
if (!blockSummary.isValid) return blockSummary;
|
|
23052
|
+
canContinue = blockSummary.canContinue;
|
|
23053
|
+
continue;
|
|
23054
|
+
}
|
|
23055
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
23056
|
+
if (!analyzeExpression(statement.test, environment, usedParameterIndices)) return {
|
|
23057
|
+
canContinue: false,
|
|
23058
|
+
isValid: false
|
|
23059
|
+
};
|
|
23060
|
+
const consequentSummary = analyzeHelperStatements([statement.consequent], environment, usedParameterIndices, analyzeExpression);
|
|
23061
|
+
if (!consequentSummary.isValid) return consequentSummary;
|
|
23062
|
+
const alternateSummary = statement.alternate ? analyzeHelperStatements([statement.alternate], environment, usedParameterIndices, analyzeExpression) : {
|
|
23063
|
+
canContinue: true,
|
|
23064
|
+
isValid: true
|
|
23065
|
+
};
|
|
23066
|
+
if (!alternateSummary.isValid) return alternateSummary;
|
|
23067
|
+
canContinue = consequentSummary.canContinue || alternateSummary.canContinue;
|
|
23068
|
+
continue;
|
|
23069
|
+
}
|
|
23070
|
+
return {
|
|
23071
|
+
canContinue: false,
|
|
23072
|
+
isValid: false
|
|
23073
|
+
};
|
|
23074
|
+
}
|
|
23075
|
+
return {
|
|
23076
|
+
canContinue,
|
|
23077
|
+
isValid: true
|
|
23078
|
+
};
|
|
23079
|
+
};
|
|
23080
|
+
const analyzeHelperExpression = (expression, environment, usedParameterIndices) => {
|
|
23081
|
+
const node = stripParenExpression(expression);
|
|
23082
|
+
if (isNodeOfType(node, "Literal") || isNodeOfType(node, "TemplateElement")) return true;
|
|
23083
|
+
if (isNodeOfType(node, "Identifier")) {
|
|
23084
|
+
const parameterIndex = environment.parameterIndices.get(node.name);
|
|
23085
|
+
if (parameterIndex !== void 0) {
|
|
23086
|
+
if (parameterIndex !== null) usedParameterIndices.add(parameterIndex);
|
|
23087
|
+
return true;
|
|
23088
|
+
}
|
|
23089
|
+
return node.name === "undefined" || node.name === "NaN" || node.name === "Infinity";
|
|
23090
|
+
}
|
|
23091
|
+
if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => !element || analyzeHelperExpression(element, environment, usedParameterIndices));
|
|
23092
|
+
if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).every((property) => {
|
|
23093
|
+
if (isNodeOfType(property, "SpreadElement")) return analyzeHelperExpression(property.argument, environment, usedParameterIndices);
|
|
23094
|
+
if (!isNodeOfType(property, "Property") || property.kind !== "init" || property.method === true) return false;
|
|
23095
|
+
if (property.computed && !analyzeHelperExpression(property.key, environment, usedParameterIndices)) return false;
|
|
23096
|
+
return analyzeHelperExpression(property.value, environment, usedParameterIndices);
|
|
23097
|
+
});
|
|
23098
|
+
if (isNodeOfType(node, "TemplateLiteral")) return (node.expressions ?? []).every((templateExpression) => analyzeHelperExpression(templateExpression, environment, usedParameterIndices));
|
|
23099
|
+
if (isNodeOfType(node, "UnaryExpression")) return node.operator !== "delete" && analyzeHelperExpression(node.argument, environment, usedParameterIndices);
|
|
23100
|
+
if (isNodeOfType(node, "BinaryExpression") || isNodeOfType(node, "LogicalExpression")) return analyzeHelperExpression(node.left, environment, usedParameterIndices) && analyzeHelperExpression(node.right, environment, usedParameterIndices);
|
|
23101
|
+
if (isNodeOfType(node, "ConditionalExpression")) return analyzeHelperExpression(node.test, environment, usedParameterIndices) && analyzeHelperExpression(node.consequent, environment, usedParameterIndices) && analyzeHelperExpression(node.alternate, environment, usedParameterIndices);
|
|
23102
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
23103
|
+
if (!analyzeHelperExpression(node.object, environment, usedParameterIndices)) return false;
|
|
23104
|
+
return !node.computed || analyzeHelperExpression(node.property, environment, usedParameterIndices);
|
|
23105
|
+
}
|
|
23106
|
+
if (isFunctionLike$1(node)) {
|
|
23107
|
+
if (isAsyncOrGeneratorFunction(node)) return false;
|
|
23108
|
+
const callbackParameterIndices = new Map(environment.parameterIndices);
|
|
23109
|
+
for (const parameter of getFunctionParameters(node)) {
|
|
23110
|
+
if (!isNodeOfType(parameter, "Identifier")) return false;
|
|
23111
|
+
callbackParameterIndices.set(parameter.name, null);
|
|
23112
|
+
}
|
|
23113
|
+
const callbackEnvironment = {
|
|
23114
|
+
parameterIndices: callbackParameterIndices,
|
|
23115
|
+
recursiveNames: environment.recursiveNames,
|
|
23116
|
+
shadowedGlobalNames: environment.shadowedGlobalNames
|
|
23117
|
+
};
|
|
23118
|
+
if (!isNodeOfType(node.body, "BlockStatement")) return analyzeHelperExpression(node.body, callbackEnvironment, usedParameterIndices);
|
|
23119
|
+
const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
|
|
23120
|
+
return callbackSummary.isValid && !callbackSummary.canContinue;
|
|
23121
|
+
}
|
|
23122
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
23123
|
+
const callee = stripParenExpression(node.callee);
|
|
23124
|
+
const calleeRoot = getMemberRoot(callee);
|
|
23125
|
+
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);
|
|
23126
|
+
const namespaceName = isNodeOfType(calleeRoot, "Identifier") && !environment.parameterIndices.has(calleeRoot.name) && !environment.recursiveNames.has(calleeRoot.name) && !environment.shadowedGlobalNames.has(calleeRoot.name) ? calleeRoot.name : null;
|
|
23127
|
+
const namespaceMemberName = getStaticMemberName(callee);
|
|
23128
|
+
const isPureNamespaceCall = isNodeOfType(callee, "MemberExpression") && namespaceName !== null && namespaceMemberName !== null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(namespaceName)?.has(namespaceMemberName) === true;
|
|
23129
|
+
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
|
|
23130
|
+
if (!isPureGlobalCall && !isPureNamespaceCall && !isPureMemberTransform) return false;
|
|
23131
|
+
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression") && !analyzeHelperExpression(callee.object, environment, usedParameterIndices)) return false;
|
|
23132
|
+
return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
|
|
23133
|
+
}
|
|
23134
|
+
if (isNodeOfType(node, "SpreadElement")) return analyzeHelperExpression(node.argument, environment, usedParameterIndices);
|
|
23135
|
+
return false;
|
|
23136
|
+
};
|
|
23137
|
+
const helperSummaryCache = /* @__PURE__ */ new WeakMap();
|
|
23138
|
+
const summarizeHelperReturn = (functionNode) => {
|
|
23139
|
+
if (!isFunctionLike$1(functionNode) || !isModuleFunction(functionNode)) return null;
|
|
23140
|
+
if (helperSummaryCache.has(functionNode)) return helperSummaryCache.get(functionNode) ?? null;
|
|
23141
|
+
if (isAsyncOrGeneratorFunction(functionNode)) {
|
|
23142
|
+
helperSummaryCache.set(functionNode, null);
|
|
23143
|
+
return null;
|
|
23144
|
+
}
|
|
23145
|
+
const parameterIndices = /* @__PURE__ */ new Map();
|
|
23146
|
+
const parameters = getFunctionParameters(functionNode);
|
|
23147
|
+
for (let parameterIndex = 0; parameterIndex < parameters.length; parameterIndex += 1) {
|
|
23148
|
+
const parameter = parameters[parameterIndex];
|
|
23149
|
+
if (!parameter || !isNodeOfType(parameter, "Identifier") || parameterIndices.has(parameter.name)) {
|
|
23150
|
+
helperSummaryCache.set(functionNode, null);
|
|
23151
|
+
return null;
|
|
23152
|
+
}
|
|
23153
|
+
parameterIndices.set(parameter.name, parameterIndex);
|
|
23154
|
+
}
|
|
23155
|
+
const environment = {
|
|
23156
|
+
parameterIndices,
|
|
23157
|
+
recursiveNames: getFunctionBindingNames(functionNode),
|
|
23158
|
+
shadowedGlobalNames: collectModuleBindingNames(functionNode)
|
|
23159
|
+
};
|
|
23160
|
+
const usedParameterIndices = /* @__PURE__ */ new Set();
|
|
23161
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) {
|
|
23162
|
+
if (!analyzeHelperExpression(functionNode.body, environment, usedParameterIndices)) {
|
|
23163
|
+
helperSummaryCache.set(functionNode, null);
|
|
23164
|
+
return null;
|
|
23165
|
+
}
|
|
23166
|
+
} else {
|
|
23167
|
+
const controlFlowSummary = analyzeHelperStatements(functionNode.body.body ?? [], environment, usedParameterIndices, analyzeHelperExpression);
|
|
23168
|
+
if (!controlFlowSummary.isValid || controlFlowSummary.canContinue) {
|
|
23169
|
+
helperSummaryCache.set(functionNode, null);
|
|
23170
|
+
return null;
|
|
23171
|
+
}
|
|
23172
|
+
}
|
|
23173
|
+
const summary = { usedParameterIndices };
|
|
23174
|
+
helperSummaryCache.set(functionNode, summary);
|
|
23175
|
+
return summary;
|
|
23176
|
+
};
|
|
23177
|
+
const resolveValueHelperFunction = (analysis, callee, currentFilename) => {
|
|
23178
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
23179
|
+
const reference = getRef(analysis, callee);
|
|
23180
|
+
if (!reference?.resolved) return null;
|
|
23181
|
+
const importDefinition = reference.resolved.defs.find((definition) => definition.type === "ImportBinding");
|
|
23182
|
+
if (importDefinition) {
|
|
23183
|
+
if (!currentFilename) return null;
|
|
23184
|
+
const specifier = importDefinition.node;
|
|
23185
|
+
if (!isNodeOfType(specifier, "ImportSpecifier") && !isNodeOfType(specifier, "ImportDefaultSpecifier")) return null;
|
|
23186
|
+
const importDeclaration = specifier.parent;
|
|
23187
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
|
|
23188
|
+
if (importDeclaration.importKind === "type" || isNodeOfType(specifier, "ImportSpecifier") && specifier.importKind === "type") return null;
|
|
23189
|
+
const source = importDeclaration.source?.value;
|
|
23190
|
+
if (typeof source !== "string") return null;
|
|
23191
|
+
const exportedName = resolveImportedExportName(specifier);
|
|
23192
|
+
if (!exportedName) return null;
|
|
23193
|
+
return resolveCrossFileFunctionExport(currentFilename, source, exportedName);
|
|
23194
|
+
}
|
|
23195
|
+
const callable = resolveWrappedCallable(analysis, callee);
|
|
23196
|
+
return callable && isModuleFunction(callable) ? callable : null;
|
|
23197
|
+
};
|
|
22759
23198
|
const isLocallyConstructedObjectMember = (reference, memberExpression) => isNodeOfType(memberExpression, "MemberExpression") && reference.resolved?.defs.some((definition) => {
|
|
22760
23199
|
const definitionNode = definition.node;
|
|
22761
23200
|
return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
|
|
@@ -22841,38 +23280,55 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
22841
23280
|
const calleeRoot = getMemberRoot(callee);
|
|
22842
23281
|
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(calleeRoot, "Identifier") && PURE_GLOBAL_NAMESPACE_NAMES.has(calleeRoot.name) && getIdentifierBindingIdentity(analysis, calleeRoot) === null;
|
|
22843
23282
|
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
|
|
22844
|
-
if (isPureMemberTransform)
|
|
22845
|
-
|
|
22846
|
-
|
|
22847
|
-
|
|
22848
|
-
|
|
22849
|
-
|
|
22850
|
-
if (isNodeOfType(callee, "MemberExpression")) {
|
|
22851
|
-
evidence.hasUnknownSource = true;
|
|
23283
|
+
if (isPureGlobalCall || isPureMemberTransform) {
|
|
23284
|
+
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
23285
|
+
for (const argument of node.arguments ?? []) {
|
|
23286
|
+
if (isFunctionLike$1(argument)) continue;
|
|
23287
|
+
mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
23288
|
+
}
|
|
22852
23289
|
return evidence;
|
|
22853
23290
|
}
|
|
22854
23291
|
if (remainingCallFrames <= 0) {
|
|
22855
23292
|
evidence.hasUnknownSource = true;
|
|
22856
23293
|
return evidence;
|
|
22857
23294
|
}
|
|
22858
|
-
const
|
|
22859
|
-
if (
|
|
22860
|
-
|
|
23295
|
+
const localHelperFunction = resolveWrappedCallable(analysis, callee);
|
|
23296
|
+
if (localHelperFunction && !isModuleFunction(localHelperFunction)) {
|
|
23297
|
+
if (isAsyncOrGeneratorFunction(localHelperFunction) || functionInvokesItself(analysis, localHelperFunction)) {
|
|
23298
|
+
evidence.hasUnknownSource = true;
|
|
23299
|
+
return evidence;
|
|
23300
|
+
}
|
|
23301
|
+
const localHelperFrame = {
|
|
23302
|
+
functionNode: localHelperFunction,
|
|
23303
|
+
invocation: node,
|
|
23304
|
+
isDeferred: false,
|
|
23305
|
+
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23306
|
+
substitutions: buildSubstitutions(analysis, localHelperFunction, node.arguments ?? [], frame),
|
|
23307
|
+
currentFilename: frame.currentFilename
|
|
23308
|
+
};
|
|
23309
|
+
const returnedExpressions = getReturnedExpressions(localHelperFunction);
|
|
23310
|
+
if (returnedExpressions.length === 0) {
|
|
23311
|
+
evidence.hasUnknownSource = true;
|
|
23312
|
+
return evidence;
|
|
23313
|
+
}
|
|
23314
|
+
for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, localHelperFrame, remainingCallFrames - 1, new Set(visitedBindings)));
|
|
22861
23315
|
return evidence;
|
|
22862
23316
|
}
|
|
22863
|
-
const
|
|
22864
|
-
|
|
22865
|
-
|
|
22866
|
-
isDeferred: false,
|
|
22867
|
-
introducedBindings: /* @__PURE__ */ new Set(),
|
|
22868
|
-
substitutions: buildSubstitutions(analysis, callable, node.arguments ?? [], frame)
|
|
22869
|
-
};
|
|
22870
|
-
const returnedExpressions = getReturnedExpressions(callable);
|
|
22871
|
-
if (returnedExpressions.length === 0) {
|
|
23317
|
+
const helperFunction = resolveValueHelperFunction(analysis, callee, frame.currentFilename);
|
|
23318
|
+
const helperSummary = helperFunction ? summarizeHelperReturn(helperFunction) : null;
|
|
23319
|
+
if (!helperSummary) {
|
|
22872
23320
|
evidence.hasUnknownSource = true;
|
|
22873
23321
|
return evidence;
|
|
22874
23322
|
}
|
|
22875
|
-
|
|
23323
|
+
const argumentsForHelper = node.arguments ?? [];
|
|
23324
|
+
for (const parameterIndex of helperSummary.usedParameterIndices) {
|
|
23325
|
+
const argument = argumentsForHelper[parameterIndex];
|
|
23326
|
+
if (!argument) {
|
|
23327
|
+
evidence.hasUnknownSource = true;
|
|
23328
|
+
return evidence;
|
|
23329
|
+
}
|
|
23330
|
+
mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames - 1, new Set(visitedBindings)));
|
|
23331
|
+
}
|
|
22876
23332
|
return evidence;
|
|
22877
23333
|
}
|
|
22878
23334
|
if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
|
|
@@ -22952,8 +23408,8 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
|
|
|
22952
23408
|
}
|
|
22953
23409
|
return false;
|
|
22954
23410
|
};
|
|
22955
|
-
const collectEffectStateWriteFacts = (analysis, effectNode) => {
|
|
22956
|
-
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode);
|
|
23411
|
+
const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
|
|
23412
|
+
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
|
|
22957
23413
|
if (frames.length === 0) return [];
|
|
22958
23414
|
const effectHasCleanup = hasCleanup(analysis, effectNode);
|
|
22959
23415
|
const cleanupManagedStateDeclarators = /* @__PURE__ */ new Set();
|
|
@@ -22973,7 +23429,8 @@ const collectEffectStateWriteFacts = (analysis, effectNode) => {
|
|
|
22973
23429
|
invocation: callExpression,
|
|
22974
23430
|
isDeferred: frame.isDeferred,
|
|
22975
23431
|
introducedBindings: collectIntroducedBindings(analysis, writtenValue),
|
|
22976
|
-
substitutions: /* @__PURE__ */ new Map()
|
|
23432
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
23433
|
+
currentFilename
|
|
22977
23434
|
};
|
|
22978
23435
|
valueEvidence = emptyEvidence();
|
|
22979
23436
|
const returnedExpressions = getReturnedExpressions(writtenValue);
|
|
@@ -23022,7 +23479,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
23022
23479
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
23023
23480
|
if (!dependencyReferences) return;
|
|
23024
23481
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
23025
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
23482
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
23026
23483
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
23027
23484
|
context.report({
|
|
23028
23485
|
node: fact.callExpression,
|
|
@@ -24283,6 +24740,139 @@ const createRelativeImportSource = (filename, targetFilePath) => {
|
|
|
24283
24740
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
24284
24741
|
};
|
|
24285
24742
|
//#endregion
|
|
24743
|
+
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
24744
|
+
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
24745
|
+
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24746
|
+
const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24747
|
+
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24748
|
+
const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
|
|
24749
|
+
const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
|
|
24750
|
+
const createNonBarrelInfo = () => ({
|
|
24751
|
+
isBarrel: false,
|
|
24752
|
+
exportsByName: /* @__PURE__ */ new Map(),
|
|
24753
|
+
starExportSources: []
|
|
24754
|
+
});
|
|
24755
|
+
const addImportedBinding = (importedBindings, binding) => {
|
|
24756
|
+
importedBindings.set(binding.localName, {
|
|
24757
|
+
...binding,
|
|
24758
|
+
didExport: false
|
|
24759
|
+
});
|
|
24760
|
+
};
|
|
24761
|
+
const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
|
|
24762
|
+
for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
|
|
24763
|
+
localName: specifier.exportedName,
|
|
24764
|
+
importedName: specifier.localName,
|
|
24765
|
+
source,
|
|
24766
|
+
isTypeOnly: specifier.isTypeOnly
|
|
24767
|
+
});
|
|
24768
|
+
};
|
|
24769
|
+
const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
|
|
24770
|
+
const trimmedImportClause = importClause.trim();
|
|
24771
|
+
const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
|
|
24772
|
+
if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
|
|
24773
|
+
localName: namespaceMatch[1],
|
|
24774
|
+
importedName: "*",
|
|
24775
|
+
source,
|
|
24776
|
+
isTypeOnly: declarationIsTypeOnly
|
|
24777
|
+
});
|
|
24778
|
+
const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
|
|
24779
|
+
if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
|
|
24780
|
+
const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
|
|
24781
|
+
if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
|
|
24782
|
+
localName: defaultImportName,
|
|
24783
|
+
importedName: "default",
|
|
24784
|
+
source,
|
|
24785
|
+
isTypeOnly: declarationIsTypeOnly
|
|
24786
|
+
});
|
|
24787
|
+
};
|
|
24788
|
+
const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
|
|
24789
|
+
let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
|
|
24790
|
+
collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
|
|
24791
|
+
return "";
|
|
24792
|
+
});
|
|
24793
|
+
withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
|
|
24794
|
+
const isTypeOnly = Boolean(typeKeyword);
|
|
24795
|
+
if (namespaceExportName) {
|
|
24796
|
+
exportsByName.set(namespaceExportName, {
|
|
24797
|
+
exportedName: namespaceExportName,
|
|
24798
|
+
importedName: "*",
|
|
24799
|
+
source,
|
|
24800
|
+
isTypeOnly
|
|
24801
|
+
});
|
|
24802
|
+
return "";
|
|
24803
|
+
}
|
|
24804
|
+
if (specifiersText) {
|
|
24805
|
+
for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
|
|
24806
|
+
exportedName: specifier.exportedName,
|
|
24807
|
+
importedName: specifier.localName,
|
|
24808
|
+
source,
|
|
24809
|
+
isTypeOnly: specifier.isTypeOnly
|
|
24810
|
+
});
|
|
24811
|
+
return "";
|
|
24812
|
+
}
|
|
24813
|
+
starExportSources.push(source);
|
|
24814
|
+
return "";
|
|
24815
|
+
});
|
|
24816
|
+
withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
|
|
24817
|
+
for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
|
|
24818
|
+
const importedBinding = importedBindings.get(specifier.localName);
|
|
24819
|
+
if (!importedBinding) return _match;
|
|
24820
|
+
importedBinding.didExport = true;
|
|
24821
|
+
exportsByName.set(specifier.exportedName, {
|
|
24822
|
+
exportedName: specifier.exportedName,
|
|
24823
|
+
importedName: importedBinding.importedName,
|
|
24824
|
+
source: importedBinding.source,
|
|
24825
|
+
isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
|
|
24826
|
+
});
|
|
24827
|
+
}
|
|
24828
|
+
return "";
|
|
24829
|
+
});
|
|
24830
|
+
return withoutKnownDeclarations;
|
|
24831
|
+
};
|
|
24832
|
+
const hasUnexportedRuntimeImport = (importedBindings) => {
|
|
24833
|
+
for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
|
|
24834
|
+
return false;
|
|
24835
|
+
};
|
|
24836
|
+
const classifyBarrelModule = (sourceText) => {
|
|
24837
|
+
const strippedSource = stripJsComments(sourceText).trim();
|
|
24838
|
+
if (!strippedSource) return createNonBarrelInfo();
|
|
24839
|
+
const importedBindings = /* @__PURE__ */ new Map();
|
|
24840
|
+
const exportsByName = /* @__PURE__ */ new Map();
|
|
24841
|
+
const starExportSources = [];
|
|
24842
|
+
if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
|
|
24843
|
+
return {
|
|
24844
|
+
isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
|
|
24845
|
+
exportsByName,
|
|
24846
|
+
starExportSources
|
|
24847
|
+
};
|
|
24848
|
+
};
|
|
24849
|
+
const getBarrelIndexModuleInfo = (filePath) => {
|
|
24850
|
+
if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
|
|
24851
|
+
recordContentProbe(filePath);
|
|
24852
|
+
let fileStat;
|
|
24853
|
+
try {
|
|
24854
|
+
fileStat = fs.statSync(filePath);
|
|
24855
|
+
} catch {
|
|
24856
|
+
fileStat = null;
|
|
24857
|
+
}
|
|
24858
|
+
const cachedResult = barrelIndexModuleInfoCache.get(filePath);
|
|
24859
|
+
if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
|
|
24860
|
+
if (fileStat === null) return createNonBarrelInfo();
|
|
24861
|
+
let moduleInfo = createNonBarrelInfo();
|
|
24862
|
+
try {
|
|
24863
|
+
moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
|
|
24864
|
+
} catch {
|
|
24865
|
+
moduleInfo = createNonBarrelInfo();
|
|
24866
|
+
}
|
|
24867
|
+
barrelIndexModuleInfoCache.set(filePath, {
|
|
24868
|
+
mtimeMs: fileStat.mtimeMs,
|
|
24869
|
+
size: fileStat.size,
|
|
24870
|
+
moduleInfo
|
|
24871
|
+
});
|
|
24872
|
+
return moduleInfo;
|
|
24873
|
+
};
|
|
24874
|
+
const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
|
|
24875
|
+
//#endregion
|
|
24286
24876
|
//#region src/react-native-dependency-names.ts
|
|
24287
24877
|
const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
|
|
24288
24878
|
"expo",
|
|
@@ -24485,6 +25075,35 @@ const classifyReactNativeFileTarget = (context) => {
|
|
|
24485
25075
|
};
|
|
24486
25076
|
const isReactNativeFileActive = (context) => classifyReactNativeFileTarget(context) !== "web";
|
|
24487
25077
|
//#endregion
|
|
25078
|
+
//#region src/plugin/utils/resolve-barrel-export-file-path.ts
|
|
25079
|
+
const getUniqueFilePath = (filePaths) => {
|
|
25080
|
+
const uniqueFilePaths = new Set(filePaths);
|
|
25081
|
+
if (uniqueFilePaths.size !== 1) return null;
|
|
25082
|
+
const [filePath] = uniqueFilePaths;
|
|
25083
|
+
return filePath ?? null;
|
|
25084
|
+
};
|
|
25085
|
+
const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
|
|
25086
|
+
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
|
|
25087
|
+
if (!resolvedTargetPath) return null;
|
|
25088
|
+
const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
|
|
25089
|
+
if (nestedTargetPath) return nestedTargetPath;
|
|
25090
|
+
return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
|
|
25091
|
+
};
|
|
25092
|
+
const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
|
|
25093
|
+
if (visitedFilePaths.has(barrelFilePath)) return null;
|
|
25094
|
+
visitedFilePaths.add(barrelFilePath);
|
|
25095
|
+
const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
|
|
25096
|
+
if (!moduleInfo.isBarrel) return null;
|
|
25097
|
+
const target = moduleInfo.exportsByName.get(exportedName);
|
|
25098
|
+
if (target) {
|
|
25099
|
+
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
|
|
25100
|
+
if (!resolvedTargetPath) return null;
|
|
25101
|
+
return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
|
|
25102
|
+
}
|
|
25103
|
+
if (exportedName === "default") return null;
|
|
25104
|
+
return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
|
|
25105
|
+
};
|
|
25106
|
+
//#endregion
|
|
24488
25107
|
//#region src/plugin/rules/bundle-size/no-barrel-import.ts
|
|
24489
25108
|
const TYPE_DECLARATION_FILE_PATTERN = /\.d\.[cm]?ts$/;
|
|
24490
25109
|
const SERVER_ONLY_FILE_PATTERN = /\.server\.[cm]?[jt]sx?$/;
|
|
@@ -24814,7 +25433,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
24814
25433
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
24815
25434
|
return false;
|
|
24816
25435
|
};
|
|
24817
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES
|
|
25436
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
24818
25437
|
"forEach",
|
|
24819
25438
|
"map",
|
|
24820
25439
|
"filter",
|
|
@@ -24835,7 +25454,7 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
24835
25454
|
if (parent.callee === functionNode) return true;
|
|
24836
25455
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
24837
25456
|
const callee = parent.callee;
|
|
24838
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES
|
|
25457
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
24839
25458
|
};
|
|
24840
25459
|
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
24841
25460
|
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
@@ -25800,7 +26419,7 @@ const noDerivedState = defineRule({
|
|
|
25800
26419
|
if (!isUseEffect(node)) return;
|
|
25801
26420
|
const analysis = getProgramAnalysis(node);
|
|
25802
26421
|
if (!analysis) return;
|
|
25803
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
26422
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
25804
26423
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
25805
26424
|
const stateName = getStateName$1(fact.stateDeclarator);
|
|
25806
26425
|
context.report({
|
|
@@ -25822,7 +26441,7 @@ const noDerivedStateEffect = defineRule({
|
|
|
25822
26441
|
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
25823
26442
|
const analysis = getProgramAnalysis(node);
|
|
25824
26443
|
if (!analysis) return;
|
|
25825
|
-
if (!collectEffectStateWriteFacts(analysis, node).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
26444
|
+
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
25826
26445
|
context.report({
|
|
25827
26446
|
node,
|
|
25828
26447
|
message: "You pay an extra render for state you can derive from other values."
|
|
@@ -27468,14 +28087,14 @@ const hasDocumentClassListMutation = (node) => {
|
|
|
27468
28087
|
//#endregion
|
|
27469
28088
|
//#region src/plugin/rules/state-and-effects/no-effect-event-handler.ts
|
|
27470
28089
|
const hasEventLikeNode = (node) => findTriggeredSideEffectCalleeName(node) !== null || hasDocumentClassListMutation(node);
|
|
27471
|
-
const unwrapChainExpression$
|
|
28090
|
+
const unwrapChainExpression$2 = (node) => {
|
|
27472
28091
|
if (!node) return null;
|
|
27473
28092
|
if (isNodeOfType(node, "ChainExpression")) return node.expression;
|
|
27474
28093
|
return node;
|
|
27475
28094
|
};
|
|
27476
28095
|
const collectGuardExpressions = (node, into) => {
|
|
27477
28096
|
if (!node) return;
|
|
27478
|
-
const unwrappedNode = unwrapChainExpression$
|
|
28097
|
+
const unwrappedNode = unwrapChainExpression$2(node);
|
|
27479
28098
|
if (!unwrappedNode) return;
|
|
27480
28099
|
const rootIdentifierName = getRootIdentifierName(unwrappedNode);
|
|
27481
28100
|
if (rootIdentifierName) {
|
|
@@ -27506,7 +28125,7 @@ const isReturnOnlyStatement = (node) => {
|
|
|
27506
28125
|
};
|
|
27507
28126
|
const hasEventLikeRemainingStatements = (statements) => statements.some((statement) => !isNodeOfType(statement, "ReturnStatement") && hasEventLikeNode(statement));
|
|
27508
28127
|
const doesGuardMatchDependency = (guardExpression, dependencyExpression) => {
|
|
27509
|
-
const unwrappedDependencyExpression = unwrapChainExpression$
|
|
28128
|
+
const unwrappedDependencyExpression = unwrapChainExpression$2(dependencyExpression);
|
|
27510
28129
|
if (!unwrappedDependencyExpression) return false;
|
|
27511
28130
|
if (areExpressionsStructurallyEqual(guardExpression.expression, unwrappedDependencyExpression)) return true;
|
|
27512
28131
|
return isNodeOfType(unwrappedDependencyExpression, "Identifier") && unwrappedDependencyExpression.name === guardExpression.rootIdentifierName;
|
|
@@ -29565,6 +30184,405 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
29565
30184
|
} })
|
|
29566
30185
|
});
|
|
29567
30186
|
//#endregion
|
|
30187
|
+
//#region src/plugin/utils/find-enclosing-jsx-opening-element.ts
|
|
30188
|
+
const findEnclosingJsxOpeningElement = (node) => {
|
|
30189
|
+
let cursor = node.parent;
|
|
30190
|
+
while (cursor) {
|
|
30191
|
+
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
30192
|
+
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
30193
|
+
cursor = cursor.parent ?? null;
|
|
30194
|
+
}
|
|
30195
|
+
return null;
|
|
30196
|
+
};
|
|
30197
|
+
//#endregion
|
|
30198
|
+
//#region src/plugin/utils/has-client-render-evidence.ts
|
|
30199
|
+
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
30200
|
+
if (fileHasUseClientDirective) return true;
|
|
30201
|
+
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
30202
|
+
if (displayName && isReactHookName(displayName)) return true;
|
|
30203
|
+
let callsHook = false;
|
|
30204
|
+
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
30205
|
+
if (callsHook) return false;
|
|
30206
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
30207
|
+
callsHook = true;
|
|
30208
|
+
return false;
|
|
30209
|
+
}
|
|
30210
|
+
});
|
|
30211
|
+
return callsHook;
|
|
30212
|
+
};
|
|
30213
|
+
//#endregion
|
|
30214
|
+
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
30215
|
+
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
30216
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
30217
|
+
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
30218
|
+
return false;
|
|
30219
|
+
};
|
|
30220
|
+
//#endregion
|
|
30221
|
+
//#region src/plugin/utils/read-initial-state-boolean.ts
|
|
30222
|
+
const readLogicalResult = (operator, leftResult, rightResult) => {
|
|
30223
|
+
if (operator === "&&") {
|
|
30224
|
+
if (leftResult === false || rightResult === false) return false;
|
|
30225
|
+
if (leftResult === true && rightResult === true) return true;
|
|
30226
|
+
return null;
|
|
30227
|
+
}
|
|
30228
|
+
if (leftResult === true || rightResult === true) return true;
|
|
30229
|
+
if (leftResult === false && rightResult === false) return false;
|
|
30230
|
+
return null;
|
|
30231
|
+
};
|
|
30232
|
+
const readIdentifierInitialStateBoolean = (identifier, scopes, visitedBindings, allowLazyInitializer) => {
|
|
30233
|
+
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
30234
|
+
if (identifier.name === "undefined" && scopes.isGlobalReference(identifier)) return false;
|
|
30235
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
30236
|
+
if (!binding || visitedBindings.has(binding.bindingIdentifier)) return null;
|
|
30237
|
+
visitedBindings.add(binding.bindingIdentifier);
|
|
30238
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
30239
|
+
if (!declarator?.init || scopes.symbolFor(identifier)?.declarationNode !== declarator) return null;
|
|
30240
|
+
const initializer = stripParenExpression(declarator.init);
|
|
30241
|
+
if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements?.[0] === binding.bindingIdentifier && isReactApiCall(initializer, "useState", scopes, { allowGlobalReactNamespace: true }) && isNodeOfType(initializer, "CallExpression")) {
|
|
30242
|
+
const initialState = initializer.arguments?.[0];
|
|
30243
|
+
if (!initialState) return false;
|
|
30244
|
+
if (initialState.type === "SpreadElement") return null;
|
|
30245
|
+
return readInitialStateBooleanInternal(initialState, scopes, visitedBindings, true);
|
|
30246
|
+
}
|
|
30247
|
+
const declaration = declarator.parent;
|
|
30248
|
+
if (!isNodeOfType(declarator.id, "Identifier") || declarator.id !== binding.bindingIdentifier || !isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const") return null;
|
|
30249
|
+
return readInitialStateBooleanInternal(initializer, scopes, visitedBindings, allowLazyInitializer);
|
|
30250
|
+
};
|
|
30251
|
+
const readInitialStateBooleanInternal = (expression, scopes, visitedBindings, allowLazyInitializer) => {
|
|
30252
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
30253
|
+
if (isNodeOfType(unwrappedExpression, "Literal")) return Boolean(unwrappedExpression.value);
|
|
30254
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) return readIdentifierInitialStateBoolean(unwrappedExpression, scopes, visitedBindings, allowLazyInitializer);
|
|
30255
|
+
if (allowLazyInitializer && (isNodeOfType(unwrappedExpression, "ArrowFunctionExpression") || isNodeOfType(unwrappedExpression, "FunctionExpression"))) {
|
|
30256
|
+
if (unwrappedExpression.async || isNodeOfType(unwrappedExpression, "FunctionExpression") && unwrappedExpression.generator) return null;
|
|
30257
|
+
if (!isNodeOfType(unwrappedExpression.body, "BlockStatement")) return readInitialStateBooleanInternal(unwrappedExpression.body, scopes, visitedBindings, false);
|
|
30258
|
+
if (unwrappedExpression.body.body.length !== 1) return null;
|
|
30259
|
+
const returnStatement = unwrappedExpression.body.body[0];
|
|
30260
|
+
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return null;
|
|
30261
|
+
return readInitialStateBooleanInternal(returnStatement.argument, scopes, visitedBindings, false);
|
|
30262
|
+
}
|
|
30263
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
30264
|
+
const argumentResult = readInitialStateBooleanInternal(unwrappedExpression.argument, scopes, visitedBindings, false);
|
|
30265
|
+
return argumentResult === null ? null : !argumentResult;
|
|
30266
|
+
}
|
|
30267
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression") && (unwrappedExpression.operator === "&&" || unwrappedExpression.operator === "||")) return readLogicalResult(unwrappedExpression.operator, readInitialStateBooleanInternal(unwrappedExpression.left, scopes, new Set(visitedBindings), false), readInitialStateBooleanInternal(unwrappedExpression.right, scopes, new Set(visitedBindings), false));
|
|
30268
|
+
return null;
|
|
30269
|
+
};
|
|
30270
|
+
const readInitialStateBoolean = (expression, scopes) => readInitialStateBooleanInternal(expression, scopes, /* @__PURE__ */ new Set(), false);
|
|
30271
|
+
//#endregion
|
|
30272
|
+
//#region src/plugin/utils/is-after-client-only-early-return.ts
|
|
30273
|
+
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode, scopes) => {
|
|
30274
|
+
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
30275
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
30276
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
30277
|
+
let currentNode = node;
|
|
30278
|
+
while (currentNode) {
|
|
30279
|
+
ancestors.add(currentNode);
|
|
30280
|
+
currentNode = currentNode.parent ?? null;
|
|
30281
|
+
}
|
|
30282
|
+
for (const statement of body.body ?? []) {
|
|
30283
|
+
if (ancestors.has(statement)) return false;
|
|
30284
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
30285
|
+
const initialConditionResult = readInitialStateBoolean(statement.test, scopes);
|
|
30286
|
+
if (initialConditionResult === true && statementAlwaysExits(statement.consequent)) return true;
|
|
30287
|
+
if (initialConditionResult === false && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
|
|
30288
|
+
}
|
|
30289
|
+
return false;
|
|
30290
|
+
};
|
|
30291
|
+
//#endregion
|
|
30292
|
+
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
30293
|
+
const isGatedByFalsyInitialState = (node, scopes) => {
|
|
30294
|
+
let cursor = node;
|
|
30295
|
+
let parent = node.parent;
|
|
30296
|
+
while (parent) {
|
|
30297
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === cursor && (parent.operator === "&&" && readInitialStateBoolean(parent.left, scopes) === false || parent.operator === "||" && readInitialStateBoolean(parent.left, scopes) === true)) return true;
|
|
30298
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor && readInitialStateBoolean(parent.test, scopes) === false || parent.alternate === cursor && readInitialStateBoolean(parent.test, scopes) === true)) return true;
|
|
30299
|
+
if (isNodeOfType(parent, "IfStatement") && (parent.consequent === cursor && readInitialStateBoolean(parent.test, scopes) === false || parent.alternate === cursor && readInitialStateBoolean(parent.test, scopes) === true)) return true;
|
|
30300
|
+
cursor = parent;
|
|
30301
|
+
parent = parent.parent ?? null;
|
|
30302
|
+
}
|
|
30303
|
+
return false;
|
|
30304
|
+
};
|
|
30305
|
+
//#endregion
|
|
30306
|
+
//#region src/plugin/rules/performance/no-hydration-branch-on-browser-global.ts
|
|
30307
|
+
const evaluateEquality = (operator, left, right) => {
|
|
30308
|
+
if (operator === "===" || operator === "==") return left === right;
|
|
30309
|
+
if (operator === "!==" || operator === "!=") return left !== right;
|
|
30310
|
+
return null;
|
|
30311
|
+
};
|
|
30312
|
+
const readTypeofBrowserGlobal = (expression, context) => {
|
|
30313
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
30314
|
+
if (!isNodeOfType(unwrappedExpression, "UnaryExpression") || unwrappedExpression.operator !== "typeof") return null;
|
|
30315
|
+
const argument = stripParenExpression(unwrappedExpression.argument);
|
|
30316
|
+
if (isNodeOfType(argument, "Identifier")) return (argument.name === "window" || argument.name === "document") && context.scopes.isGlobalReference(argument) ? argument.name : null;
|
|
30317
|
+
if (!isNodeOfType(argument, "MemberExpression") || argument.computed || !isNodeOfType(argument.object, "Identifier") || argument.object.name !== "globalThis" || !context.scopes.isGlobalReference(argument.object) || !isNodeOfType(argument.property, "Identifier") || argument.property.name !== "window" && argument.property.name !== "document") return null;
|
|
30318
|
+
return argument.property.name;
|
|
30319
|
+
};
|
|
30320
|
+
const matchBrowserPredicate = (expression, context) => {
|
|
30321
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
30322
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
30323
|
+
const innerMatch = matchBrowserPredicate(unwrappedExpression.argument, context);
|
|
30324
|
+
return innerMatch ? {
|
|
30325
|
+
browserGlobalName: innerMatch.browserGlobalName,
|
|
30326
|
+
clientResult: !innerMatch.clientResult,
|
|
30327
|
+
serverResult: !innerMatch.serverResult
|
|
30328
|
+
} : null;
|
|
30329
|
+
}
|
|
30330
|
+
if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
|
|
30331
|
+
const leftGlobalName = readTypeofBrowserGlobal(unwrappedExpression.left, context);
|
|
30332
|
+
const rightGlobalName = readTypeofBrowserGlobal(unwrappedExpression.right, context);
|
|
30333
|
+
const leftString = isNodeOfType(unwrappedExpression.left, "Literal") ? unwrappedExpression.left.value : null;
|
|
30334
|
+
const rightString = isNodeOfType(unwrappedExpression.right, "Literal") ? unwrappedExpression.right.value : null;
|
|
30335
|
+
const browserGlobalName = leftGlobalName && typeof rightString === "string" ? leftGlobalName : rightGlobalName && typeof leftString === "string" ? rightGlobalName : null;
|
|
30336
|
+
const comparedType = leftGlobalName && typeof rightString === "string" ? rightString : rightGlobalName && typeof leftString === "string" ? leftString : null;
|
|
30337
|
+
if (!browserGlobalName || !comparedType) return null;
|
|
30338
|
+
const clientResult = evaluateEquality(unwrappedExpression.operator, "object", comparedType);
|
|
30339
|
+
const serverResult = evaluateEquality(unwrappedExpression.operator, "undefined", comparedType);
|
|
30340
|
+
if (clientResult === null || serverResult === null || clientResult === serverResult) return null;
|
|
30341
|
+
return {
|
|
30342
|
+
browserGlobalName,
|
|
30343
|
+
clientResult,
|
|
30344
|
+
serverResult
|
|
30345
|
+
};
|
|
30346
|
+
};
|
|
30347
|
+
const readLogicalConditionResult = (operator, leftResult, rightResult) => {
|
|
30348
|
+
if (operator === "&&") {
|
|
30349
|
+
if (leftResult === false || rightResult === false) return false;
|
|
30350
|
+
if (leftResult === true && rightResult === true) return true;
|
|
30351
|
+
return null;
|
|
30352
|
+
}
|
|
30353
|
+
if (leftResult === true || rightResult === true) return true;
|
|
30354
|
+
if (leftResult === false && rightResult === false) return false;
|
|
30355
|
+
return null;
|
|
30356
|
+
};
|
|
30357
|
+
const readHydrationConditionResult = (expression, context, runtime) => {
|
|
30358
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
30359
|
+
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
30360
|
+
if (predicateMatch) return predicateMatch[`${runtime}Result`];
|
|
30361
|
+
const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
|
|
30362
|
+
if (staticResult !== null) return staticResult;
|
|
30363
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
30364
|
+
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
|
|
30365
|
+
return argumentResult === null ? null : !argumentResult;
|
|
30366
|
+
}
|
|
30367
|
+
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
30368
|
+
return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime), readHydrationConditionResult(unwrappedExpression.right, context, runtime));
|
|
30369
|
+
};
|
|
30370
|
+
const matchHydrationCondition = (expression, context) => {
|
|
30371
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
30372
|
+
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
30373
|
+
if (predicateMatch) return {
|
|
30374
|
+
predicateMatch,
|
|
30375
|
+
predicateNode: unwrappedExpression
|
|
30376
|
+
};
|
|
30377
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationCondition(unwrappedExpression.argument, context);
|
|
30378
|
+
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
30379
|
+
const leftMatch = matchHydrationCondition(unwrappedExpression.left, context);
|
|
30380
|
+
const rightMatch = matchHydrationCondition(unwrappedExpression.right, context);
|
|
30381
|
+
if (leftMatch && rightMatch) {
|
|
30382
|
+
const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client");
|
|
30383
|
+
const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server");
|
|
30384
|
+
return clientResult !== null && serverResult !== null && clientResult !== serverResult ? leftMatch : null;
|
|
30385
|
+
}
|
|
30386
|
+
const nestedMatch = leftMatch ?? rightMatch;
|
|
30387
|
+
if (!nestedMatch) return null;
|
|
30388
|
+
const otherResult = readInitialStateBoolean(leftMatch ? unwrappedExpression.right : unwrappedExpression.left, context.scopes);
|
|
30389
|
+
if (unwrappedExpression.operator === "&&" && otherResult === false || unwrappedExpression.operator === "||" && otherResult === true) return null;
|
|
30390
|
+
return nestedMatch;
|
|
30391
|
+
};
|
|
30392
|
+
const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
|
|
30393
|
+
const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
|
|
30394
|
+
if (!leftNode || !rightNode) return leftNode === rightNode;
|
|
30395
|
+
const left = stripParenExpression(leftNode);
|
|
30396
|
+
const right = stripParenExpression(rightNode);
|
|
30397
|
+
if (areExpressionsStructurallyEqual(left, right)) return true;
|
|
30398
|
+
if (left.type !== right.type) return false;
|
|
30399
|
+
if (isNodeOfType(left, "JSXText") && isNodeOfType(right, "JSXText")) return left.value === right.value;
|
|
30400
|
+
if (isNodeOfType(left, "JSXExpressionContainer") && isNodeOfType(right, "JSXExpressionContainer")) {
|
|
30401
|
+
if (!isAstNode(left.expression) || !isAstNode(right.expression)) return left.expression.type === right.expression.type;
|
|
30402
|
+
return areRenderedBranchesEquivalent(left.expression, right.expression);
|
|
30403
|
+
}
|
|
30404
|
+
if (isNodeOfType(left, "JSXElement") && isNodeOfType(right, "JSXElement")) {
|
|
30405
|
+
if (flattenJsxName$1(left.openingElement.name) !== flattenJsxName$1(right.openingElement.name)) return false;
|
|
30406
|
+
if (!areNodeArraysEquivalent(left.openingElement.attributes, right.openingElement.attributes)) return false;
|
|
30407
|
+
return areNodeArraysEquivalent(left.children, right.children);
|
|
30408
|
+
}
|
|
30409
|
+
if (isNodeOfType(left, "JSXFragment") && isNodeOfType(right, "JSXFragment")) return areNodeArraysEquivalent(left.children, right.children);
|
|
30410
|
+
if (isNodeOfType(left, "JSXAttribute") && isNodeOfType(right, "JSXAttribute")) {
|
|
30411
|
+
if (flattenJsxName$1(left.name) !== flattenJsxName$1(right.name)) return false;
|
|
30412
|
+
return areRenderedBranchesEquivalent(left.value, right.value);
|
|
30413
|
+
}
|
|
30414
|
+
if (isNodeOfType(left, "JSXSpreadAttribute") && isNodeOfType(right, "JSXSpreadAttribute")) return areRenderedBranchesEquivalent(left.argument, right.argument);
|
|
30415
|
+
if (isNodeOfType(left, "TemplateLiteral") && isNodeOfType(right, "TemplateLiteral")) {
|
|
30416
|
+
if (left.quasis.length !== right.quasis.length) return false;
|
|
30417
|
+
if (!left.quasis.every((quasi, index) => quasi.value.cooked === right.quasis[index]?.value.cooked && quasi.value.raw === right.quasis[index]?.value.raw)) return false;
|
|
30418
|
+
return areNodeArraysEquivalent(left.expressions, right.expressions);
|
|
30419
|
+
}
|
|
30420
|
+
return false;
|
|
30421
|
+
};
|
|
30422
|
+
const isRenderedValue = (node) => {
|
|
30423
|
+
const unwrappedNode = stripParenExpression(node);
|
|
30424
|
+
if (isNodeOfType(unwrappedNode, "Literal")) return unwrappedNode.value !== null && unwrappedNode.value !== true && unwrappedNode.value !== false && unwrappedNode.value !== "";
|
|
30425
|
+
if (isNodeOfType(unwrappedNode, "TemplateLiteral")) return unwrappedNode.expressions.length > 0 || unwrappedNode.quasis[0]?.value.cooked !== "";
|
|
30426
|
+
return isNodeOfType(unwrappedNode, "JSXElement") || isNodeOfType(unwrappedNode, "JSXFragment");
|
|
30427
|
+
};
|
|
30428
|
+
const findRenderedValueInAndBranch = (node) => {
|
|
30429
|
+
const unwrappedNode = stripParenExpression(node);
|
|
30430
|
+
if (isRenderedValue(unwrappedNode)) return unwrappedNode;
|
|
30431
|
+
if (!isNodeOfType(unwrappedNode, "LogicalExpression") || unwrappedNode.operator !== "&&") return null;
|
|
30432
|
+
return findRenderedValueInAndBranch(unwrappedNode.right);
|
|
30433
|
+
};
|
|
30434
|
+
const findEnclosingJsxAttribute = (node) => {
|
|
30435
|
+
let currentNode = node.parent;
|
|
30436
|
+
while (currentNode) {
|
|
30437
|
+
if (isNodeOfType(currentNode, "JSXAttribute")) return currentNode;
|
|
30438
|
+
if (isNodeOfType(currentNode, "JSXElement") || isNodeOfType(currentNode, "JSXFragment") || isFunctionLike$1(currentNode)) return null;
|
|
30439
|
+
currentNode = currentNode.parent;
|
|
30440
|
+
}
|
|
30441
|
+
return null;
|
|
30442
|
+
};
|
|
30443
|
+
const isInRenderedOutput = (node, componentOrHookNode, scopes) => {
|
|
30444
|
+
let currentNode = node;
|
|
30445
|
+
let parentNode = currentNode.parent;
|
|
30446
|
+
while (parentNode) {
|
|
30447
|
+
if (isNodeOfType(parentNode, "JSXExpressionContainer")) {
|
|
30448
|
+
const attribute = findEnclosingJsxAttribute(parentNode);
|
|
30449
|
+
return attribute ? !isEventHandlerAttribute(attribute) : true;
|
|
30450
|
+
}
|
|
30451
|
+
if (isNodeOfType(parentNode, "ReturnStatement")) {
|
|
30452
|
+
if (findEnclosingFunction(parentNode) === componentOrHookNode) return true;
|
|
30453
|
+
}
|
|
30454
|
+
if (parentNode === componentOrHookNode) return isFunctionLike$1(componentOrHookNode) && !isNodeOfType(componentOrHookNode.body, "BlockStatement") && componentOrHookNode.body === currentNode;
|
|
30455
|
+
if (isFunctionLike$1(parentNode) && !executesDuringRender(parentNode, scopes)) return false;
|
|
30456
|
+
currentNode = parentNode;
|
|
30457
|
+
parentNode = currentNode.parent;
|
|
30458
|
+
}
|
|
30459
|
+
return false;
|
|
30460
|
+
};
|
|
30461
|
+
const getReturnedValues = (statement) => {
|
|
30462
|
+
if (!statement) return [];
|
|
30463
|
+
if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? [statement.argument] : [];
|
|
30464
|
+
if (isNodeOfType(statement, "IfStatement")) return [...getReturnedValues(statement.consequent), ...getReturnedValues(statement.alternate)];
|
|
30465
|
+
if (!isNodeOfType(statement, "BlockStatement")) return [];
|
|
30466
|
+
const returnedValues = [];
|
|
30467
|
+
for (const childStatement of statement.body) {
|
|
30468
|
+
returnedValues.push(...getReturnedValues(childStatement));
|
|
30469
|
+
if (statementAlwaysExits(childStatement)) break;
|
|
30470
|
+
}
|
|
30471
|
+
return returnedValues;
|
|
30472
|
+
};
|
|
30473
|
+
const findFollowingReturnedValues = (ifStatement) => {
|
|
30474
|
+
const parentNode = ifStatement.parent;
|
|
30475
|
+
if (!isNodeOfType(parentNode, "BlockStatement")) return [];
|
|
30476
|
+
const statementIndex = parentNode.body.findIndex((statement) => statement === ifStatement);
|
|
30477
|
+
if (statementIndex < 0) return [];
|
|
30478
|
+
const returnedValues = [];
|
|
30479
|
+
for (const statement of parentNode.body.slice(statementIndex + 1)) {
|
|
30480
|
+
returnedValues.push(...getReturnedValues(statement));
|
|
30481
|
+
if (statementAlwaysExits(statement)) break;
|
|
30482
|
+
}
|
|
30483
|
+
return returnedValues;
|
|
30484
|
+
};
|
|
30485
|
+
const areConditionExpressionsEquivalent = (leftExpression, rightExpression) => {
|
|
30486
|
+
const left = stripParenExpression(leftExpression);
|
|
30487
|
+
const right = stripParenExpression(rightExpression);
|
|
30488
|
+
if (areExpressionsStructurallyEqual(left, right)) return true;
|
|
30489
|
+
if (left.type !== right.type) return false;
|
|
30490
|
+
if (isNodeOfType(left, "UnaryExpression") && isNodeOfType(right, "UnaryExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.argument, right.argument);
|
|
30491
|
+
if (isNodeOfType(left, "LogicalExpression") && isNodeOfType(right, "LogicalExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.left, right.left) && areConditionExpressionsEquivalent(left.right, right.right);
|
|
30492
|
+
if (isNodeOfType(left, "BinaryExpression") && isNodeOfType(right, "BinaryExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.left, right.left) && areConditionExpressionsEquivalent(left.right, right.right);
|
|
30493
|
+
return false;
|
|
30494
|
+
};
|
|
30495
|
+
const areReturnTreesEquivalent = (leftStatement, rightStatement) => {
|
|
30496
|
+
if (!leftStatement || !rightStatement) return leftStatement === rightStatement;
|
|
30497
|
+
if (isNodeOfType(leftStatement, "ReturnStatement") && isNodeOfType(rightStatement, "ReturnStatement")) return areRenderedBranchesEquivalent(leftStatement.argument, rightStatement.argument);
|
|
30498
|
+
if (isNodeOfType(leftStatement, "IfStatement") && isNodeOfType(rightStatement, "IfStatement")) return areConditionExpressionsEquivalent(leftStatement.test, rightStatement.test) && areReturnTreesEquivalent(leftStatement.consequent, rightStatement.consequent) && areReturnTreesEquivalent(leftStatement.alternate, rightStatement.alternate);
|
|
30499
|
+
if (!isNodeOfType(leftStatement, "BlockStatement") || !isNodeOfType(rightStatement, "BlockStatement")) return false;
|
|
30500
|
+
const leftReturningStatements = leftStatement.body.filter((statement) => getReturnedValues(statement).length > 0);
|
|
30501
|
+
const rightReturningStatements = rightStatement.body.filter((statement) => getReturnedValues(statement).length > 0);
|
|
30502
|
+
return leftReturningStatements.length === rightReturningStatements.length && leftReturningStatements.every((statement, index) => areReturnTreesEquivalent(statement, rightReturningStatements[index]));
|
|
30503
|
+
};
|
|
30504
|
+
const isStructuralRenderedValue = (node) => {
|
|
30505
|
+
if (!node) return false;
|
|
30506
|
+
const unwrappedNode = stripParenExpression(node);
|
|
30507
|
+
return isNodeOfType(unwrappedNode, "JSXElement") || isNodeOfType(unwrappedNode, "JSXFragment");
|
|
30508
|
+
};
|
|
30509
|
+
const branchRootsSuppressSameElement = (leftBranch, rightBranch) => {
|
|
30510
|
+
if (!rightBranch) return false;
|
|
30511
|
+
const left = stripParenExpression(leftBranch);
|
|
30512
|
+
const right = stripParenExpression(rightBranch);
|
|
30513
|
+
return isNodeOfType(left, "JSXElement") && isNodeOfType(right, "JSXElement") && flattenJsxName$1(left.openingElement.name) === flattenJsxName$1(right.openingElement.name) && hasSuppressHydrationWarningAttribute(left.openingElement) && hasSuppressHydrationWarningAttribute(right.openingElement);
|
|
30514
|
+
};
|
|
30515
|
+
const noHydrationBranchOnBrowserGlobal = defineRule({
|
|
30516
|
+
id: "no-hydration-branch-on-browser-global",
|
|
30517
|
+
title: "Server and client render different branches",
|
|
30518
|
+
severity: "error",
|
|
30519
|
+
category: "Correctness",
|
|
30520
|
+
requires: ["ssr"],
|
|
30521
|
+
recommendation: "Render the same initial output on the server and client, then switch after mount or use useSyncExternalStore with a stable server snapshot.",
|
|
30522
|
+
create: (context) => {
|
|
30523
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
30524
|
+
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
30525
|
+
let fileHasUseClientDirective = false;
|
|
30526
|
+
let fileIsEmailTemplate = false;
|
|
30527
|
+
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30528
|
+
const reportHydrationBranch = (conditionNode, leftBranch, rightBranch, requiresRenderedContext) => {
|
|
30529
|
+
const conditionMatch = matchHydrationCondition(conditionNode, context);
|
|
30530
|
+
if (!conditionMatch) return;
|
|
30531
|
+
const { predicateMatch, predicateNode } = conditionMatch;
|
|
30532
|
+
if (reportedNodes.has(predicateNode)) return;
|
|
30533
|
+
if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
|
|
30534
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(predicateNode, context.scopes);
|
|
30535
|
+
if (!componentOrHookNode) return;
|
|
30536
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30537
|
+
if (requiresRenderedContext && !isInRenderedOutput(predicateNode, componentOrHookNode, context.scopes)) return;
|
|
30538
|
+
if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
|
|
30539
|
+
const attribute = findEnclosingJsxAttribute(predicateNode);
|
|
30540
|
+
if (!attribute || isEventHandlerAttribute(attribute)) return;
|
|
30541
|
+
}
|
|
30542
|
+
if (fileIsEmailTemplate || isGatedByFalsyInitialState(predicateNode, context.scopes)) return;
|
|
30543
|
+
if (isAfterClientOnlyEarlyReturn(predicateNode, componentOrHookNode, context.scopes)) return;
|
|
30544
|
+
const openingElement = findEnclosingJsxOpeningElement(predicateNode);
|
|
30545
|
+
if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
|
|
30546
|
+
if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
|
|
30547
|
+
if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
|
|
30548
|
+
reportedNodes.add(predicateNode);
|
|
30549
|
+
context.report({
|
|
30550
|
+
node: predicateNode,
|
|
30551
|
+
message: `\`typeof ${predicateMatch.browserGlobalName}\` selects different rendered output on the server and during hydration. Render the same initial output, then switch after mount.`
|
|
30552
|
+
});
|
|
30553
|
+
};
|
|
30554
|
+
return {
|
|
30555
|
+
Program(node) {
|
|
30556
|
+
fileHasUseClientDirective = hasDirective(node, "use client");
|
|
30557
|
+
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
30558
|
+
},
|
|
30559
|
+
ConditionalExpression(node) {
|
|
30560
|
+
reportHydrationBranch(node.test, node.consequent, node.alternate, true);
|
|
30561
|
+
},
|
|
30562
|
+
LogicalExpression(node) {
|
|
30563
|
+
if (node.operator !== "&&" && node.operator !== "||") return;
|
|
30564
|
+
const renderedValue = node.operator === "&&" ? findRenderedValueInAndBranch(node.right) : isRenderedValue(node.right) ? node.right : null;
|
|
30565
|
+
if (!renderedValue) return;
|
|
30566
|
+
reportHydrationBranch(node, renderedValue, null, true);
|
|
30567
|
+
},
|
|
30568
|
+
IfStatement(node) {
|
|
30569
|
+
if (node.alternate && areReturnTreesEquivalent(node.consequent, node.alternate)) return;
|
|
30570
|
+
const consequentValues = getReturnedValues(node.consequent);
|
|
30571
|
+
const alternateValues = node.alternate ? getReturnedValues(node.alternate) : findFollowingReturnedValues(node);
|
|
30572
|
+
if (consequentValues.length === 0 || alternateValues.length === 0) return;
|
|
30573
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node.test, context.scopes);
|
|
30574
|
+
if (!componentOrHookNode) return;
|
|
30575
|
+
const enclosingFunction = findEnclosingFunction(node);
|
|
30576
|
+
if (enclosingFunction !== componentOrHookNode && (!enclosingFunction || !isInRenderedOutput(enclosingFunction, componentOrHookNode, context.scopes))) return;
|
|
30577
|
+
for (const consequentValue of consequentValues) for (const alternateValue of alternateValues) {
|
|
30578
|
+
if (!isRenderedValue(consequentValue) && !isRenderedValue(alternateValue)) continue;
|
|
30579
|
+
reportHydrationBranch(node.test, consequentValue, alternateValue, false);
|
|
30580
|
+
}
|
|
30581
|
+
}
|
|
30582
|
+
};
|
|
30583
|
+
}
|
|
30584
|
+
});
|
|
30585
|
+
//#endregion
|
|
29568
30586
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
29569
30587
|
const MESSAGE$22 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
|
|
29570
30588
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
@@ -29585,6 +30603,131 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
29585
30603
|
} })
|
|
29586
30604
|
});
|
|
29587
30605
|
//#endregion
|
|
30606
|
+
//#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
|
|
30607
|
+
const TIMER_FUNCTION_NAMES = new Set([
|
|
30608
|
+
"cancelAnimationFrame",
|
|
30609
|
+
"clearInterval",
|
|
30610
|
+
"clearTimeout",
|
|
30611
|
+
"queueMicrotask",
|
|
30612
|
+
"requestAnimationFrame",
|
|
30613
|
+
"setInterval",
|
|
30614
|
+
"setTimeout"
|
|
30615
|
+
]);
|
|
30616
|
+
const STORAGE_MUTATION_METHOD_NAMES = new Set([
|
|
30617
|
+
"clear",
|
|
30618
|
+
"removeItem",
|
|
30619
|
+
"setItem"
|
|
30620
|
+
]);
|
|
30621
|
+
const STORAGE_RECEIVER_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
30622
|
+
const EXTERNAL_READ_METHOD_NAMES = new Set(["getBoundingClientRect", "getClientRects"]);
|
|
30623
|
+
const NOTIFICATION_RECEIVER_NAMES = new Set([
|
|
30624
|
+
"message",
|
|
30625
|
+
"notification",
|
|
30626
|
+
"toast"
|
|
30627
|
+
]);
|
|
30628
|
+
const NOTIFICATION_METHOD_NAMES = new Set([
|
|
30629
|
+
"error",
|
|
30630
|
+
"info",
|
|
30631
|
+
"loading",
|
|
30632
|
+
"open",
|
|
30633
|
+
"show",
|
|
30634
|
+
"success",
|
|
30635
|
+
"warning"
|
|
30636
|
+
]);
|
|
30637
|
+
const getMemberCall = (node) => {
|
|
30638
|
+
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
30639
|
+
if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
|
|
30640
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
30641
|
+
return {
|
|
30642
|
+
methodName: node.callee.property.name,
|
|
30643
|
+
receiver: stripParenExpression(node.callee.object)
|
|
30644
|
+
};
|
|
30645
|
+
};
|
|
30646
|
+
const isNotificationReceiver = (receiver, scopes) => {
|
|
30647
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
30648
|
+
if (!NOTIFICATION_RECEIVER_NAMES.has(receiver.name)) return false;
|
|
30649
|
+
const symbol = scopes.symbolFor(receiver);
|
|
30650
|
+
if (symbol?.kind === "import") return true;
|
|
30651
|
+
if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
|
|
30652
|
+
const callee = symbol.initializer.callee;
|
|
30653
|
+
return isNodeOfType(callee, "Identifier") && /^use(?:Message|Notification|Toast)$/.test(callee.name);
|
|
30654
|
+
};
|
|
30655
|
+
const getKnownImpureCall = (callExpression, scopes) => {
|
|
30656
|
+
if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
|
|
30657
|
+
const memberCall = getMemberCall(callExpression);
|
|
30658
|
+
if (!memberCall) return null;
|
|
30659
|
+
const { methodName, receiver } = memberCall;
|
|
30660
|
+
if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && STORAGE_RECEIVER_NAMES.has(receiver.name) && scopes.isGlobalReference(receiver)) return `${receiver.name}.${methodName}()`;
|
|
30661
|
+
if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
|
|
30662
|
+
if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && isNotificationReceiver(receiver, scopes)) return `${receiver.name}.${methodName}()`;
|
|
30663
|
+
return null;
|
|
30664
|
+
};
|
|
30665
|
+
const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
|
|
30666
|
+
let rootIdentifier = null;
|
|
30667
|
+
if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
|
|
30668
|
+
else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
|
|
30669
|
+
if (!rootIdentifier) return null;
|
|
30670
|
+
const updaterScope = scopes.ownScopeFor(updater);
|
|
30671
|
+
if (!updaterScope) return null;
|
|
30672
|
+
const symbol = scopes.symbolFor(rootIdentifier);
|
|
30673
|
+
if (!symbol) return `the external value "${rootIdentifier.name}"`;
|
|
30674
|
+
if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
|
|
30675
|
+
return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
|
|
30676
|
+
};
|
|
30677
|
+
const findImpureUpdaterOperation = (updater, scopes) => {
|
|
30678
|
+
const analysis = getProgramAnalysis(updater);
|
|
30679
|
+
let operation = null;
|
|
30680
|
+
walkAst(updater, (child) => {
|
|
30681
|
+
if (operation) return false;
|
|
30682
|
+
if (child !== updater && isFunctionLike$1(child) && !executesDuringRender(child, scopes)) return false;
|
|
30683
|
+
if (isNodeOfType(child, "CallExpression")) {
|
|
30684
|
+
if (isNodeOfType(child.callee, "Identifier") && analysis) {
|
|
30685
|
+
const calleeReference = getRef(analysis, child.callee);
|
|
30686
|
+
if (calleeReference && isStateSetterCall(analysis, calleeReference)) {
|
|
30687
|
+
operation = `the nested state update "${child.callee.name}()"`;
|
|
30688
|
+
return false;
|
|
30689
|
+
}
|
|
30690
|
+
}
|
|
30691
|
+
const impureCall = getKnownImpureCall(child, scopes);
|
|
30692
|
+
if (impureCall) {
|
|
30693
|
+
operation = impureCall;
|
|
30694
|
+
return false;
|
|
30695
|
+
}
|
|
30696
|
+
}
|
|
30697
|
+
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
30698
|
+
operation = getExternalAssignmentDescription(child.left, updater, scopes);
|
|
30699
|
+
if (operation) return false;
|
|
30700
|
+
}
|
|
30701
|
+
if (isNodeOfType(child, "UpdateExpression")) {
|
|
30702
|
+
operation = getExternalAssignmentDescription(child.argument, updater, scopes);
|
|
30703
|
+
if (operation) return false;
|
|
30704
|
+
}
|
|
30705
|
+
});
|
|
30706
|
+
return operation;
|
|
30707
|
+
};
|
|
30708
|
+
const noImpureStateUpdater = defineRule({
|
|
30709
|
+
id: "no-impure-state-updater",
|
|
30710
|
+
title: "State updater has side effects",
|
|
30711
|
+
severity: "error",
|
|
30712
|
+
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.",
|
|
30713
|
+
create: (context) => ({ CallExpression(node) {
|
|
30714
|
+
const updater = node.arguments?.[0];
|
|
30715
|
+
if (!updater || !isFunctionLike$1(updater)) return;
|
|
30716
|
+
const analysis = getProgramAnalysis(node);
|
|
30717
|
+
if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
|
|
30718
|
+
const calleeReference = getRef(analysis, node.callee);
|
|
30719
|
+
if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
|
|
30720
|
+
const stateDeclarator = getUseStateDecl(analysis, calleeReference);
|
|
30721
|
+
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
|
|
30722
|
+
const operation = findImpureUpdaterOperation(updater, context.scopes);
|
|
30723
|
+
if (!operation) return;
|
|
30724
|
+
context.report({
|
|
30725
|
+
node: updater,
|
|
30726
|
+
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.`
|
|
30727
|
+
});
|
|
30728
|
+
} })
|
|
30729
|
+
});
|
|
30730
|
+
//#endregion
|
|
29588
30731
|
//#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
|
|
29589
30732
|
const MESSAGE$21 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
|
|
29590
30733
|
const REACT_USE_REF_OPTIONS = {
|
|
@@ -29751,7 +30894,7 @@ const noInitializeState = defineRule({
|
|
|
29751
30894
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
29752
30895
|
const analysis = getProgramAnalysis(node);
|
|
29753
30896
|
if (!analysis) return;
|
|
29754
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
30897
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
29755
30898
|
if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
|
|
29756
30899
|
const stateName = getStateName(fact.stateDeclarator);
|
|
29757
30900
|
context.report({
|
|
@@ -30410,111 +31553,6 @@ const noLegacyContextApi = defineRule({
|
|
|
30410
31553
|
}
|
|
30411
31554
|
});
|
|
30412
31555
|
//#endregion
|
|
30413
|
-
//#region src/plugin/utils/executes-during-render.ts
|
|
30414
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
30415
|
-
"map",
|
|
30416
|
-
"filter",
|
|
30417
|
-
"forEach",
|
|
30418
|
-
"flatMap",
|
|
30419
|
-
"reduce",
|
|
30420
|
-
"reduceRight",
|
|
30421
|
-
"some",
|
|
30422
|
-
"every",
|
|
30423
|
-
"find",
|
|
30424
|
-
"findIndex",
|
|
30425
|
-
"findLast",
|
|
30426
|
-
"findLastIndex",
|
|
30427
|
-
"sort",
|
|
30428
|
-
"toSorted"
|
|
30429
|
-
]);
|
|
30430
|
-
const executesDuringRender = (functionNode) => {
|
|
30431
|
-
const parent = functionNode.parent;
|
|
30432
|
-
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
30433
|
-
if (parent.callee === functionNode) return true;
|
|
30434
|
-
if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
|
|
30435
|
-
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
|
|
30436
|
-
};
|
|
30437
|
-
//#endregion
|
|
30438
|
-
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
30439
|
-
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
30440
|
-
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
30441
|
-
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
30442
|
-
return false;
|
|
30443
|
-
};
|
|
30444
|
-
//#endregion
|
|
30445
|
-
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
30446
|
-
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
30447
|
-
let ancestor = bindingIdentifier.parent;
|
|
30448
|
-
while (ancestor) {
|
|
30449
|
-
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
30450
|
-
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
30451
|
-
ancestor = ancestor.parent ?? null;
|
|
30452
|
-
}
|
|
30453
|
-
return null;
|
|
30454
|
-
};
|
|
30455
|
-
//#endregion
|
|
30456
|
-
//#region src/plugin/utils/references-falsy-initial-state.ts
|
|
30457
|
-
const isFalsyLiteral = (node) => {
|
|
30458
|
-
if (!node) return true;
|
|
30459
|
-
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
30460
|
-
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
30461
|
-
};
|
|
30462
|
-
const isFalsyInitialStateBinding = (identifier) => {
|
|
30463
|
-
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
30464
|
-
const binding = findVariableInitializer(identifier, identifier.name);
|
|
30465
|
-
if (!binding) return false;
|
|
30466
|
-
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
30467
|
-
if (!declarator?.init) return false;
|
|
30468
|
-
const init = stripParenExpression(declarator.init);
|
|
30469
|
-
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
30470
|
-
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
30471
|
-
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
30472
|
-
return isFalsyLiteral(init.arguments?.[0]);
|
|
30473
|
-
};
|
|
30474
|
-
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
30475
|
-
//#endregion
|
|
30476
|
-
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
30477
|
-
const isGatedByFalsyInitialState = (node) => {
|
|
30478
|
-
let cursor = node;
|
|
30479
|
-
let parent = node.parent;
|
|
30480
|
-
while (parent) {
|
|
30481
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
30482
|
-
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
30483
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
30484
|
-
cursor = parent;
|
|
30485
|
-
parent = parent.parent ?? null;
|
|
30486
|
-
}
|
|
30487
|
-
return false;
|
|
30488
|
-
};
|
|
30489
|
-
//#endregion
|
|
30490
|
-
//#region src/plugin/utils/references-client-only-flag.ts
|
|
30491
|
-
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
30492
|
-
const referencesClientOnlyFlag = (expression) => {
|
|
30493
|
-
const unwrapped = stripParenExpression(expression);
|
|
30494
|
-
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
30495
|
-
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
30496
|
-
const property = unwrapped.property;
|
|
30497
|
-
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
30498
|
-
}
|
|
30499
|
-
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
30500
|
-
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
30501
|
-
return false;
|
|
30502
|
-
};
|
|
30503
|
-
//#endregion
|
|
30504
|
-
//#region src/plugin/utils/is-inside-client-only-guard.ts
|
|
30505
|
-
const isInsideClientOnlyGuard = (node) => {
|
|
30506
|
-
let cursor = node;
|
|
30507
|
-
let parent = node.parent;
|
|
30508
|
-
while (parent) {
|
|
30509
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
30510
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
30511
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
30512
|
-
cursor = parent;
|
|
30513
|
-
parent = parent.parent ?? null;
|
|
30514
|
-
}
|
|
30515
|
-
return false;
|
|
30516
|
-
};
|
|
30517
|
-
//#endregion
|
|
30518
31556
|
//#region src/plugin/rules/performance/no-locale-format-in-render.ts
|
|
30519
31557
|
const LOCALE_FORMAT_METHOD_NAMES = new Set([
|
|
30520
31558
|
"toLocaleString",
|
|
@@ -30644,75 +31682,12 @@ const matchDateDefaultStringification = (node) => {
|
|
|
30644
31682
|
}
|
|
30645
31683
|
return null;
|
|
30646
31684
|
};
|
|
30647
|
-
const findRenderPhaseComponentOrHook = (node) => {
|
|
30648
|
-
let functionNode = findEnclosingFunction(node);
|
|
30649
|
-
while (functionNode) {
|
|
30650
|
-
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
30651
|
-
if (!executesDuringRender(functionNode)) return null;
|
|
30652
|
-
functionNode = findEnclosingFunction(functionNode);
|
|
30653
|
-
}
|
|
30654
|
-
return null;
|
|
30655
|
-
};
|
|
30656
|
-
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
30657
|
-
if (fileHasUseClientDirective) return true;
|
|
30658
|
-
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
30659
|
-
if (displayName && isReactHookName(displayName)) return true;
|
|
30660
|
-
let callsHook = false;
|
|
30661
|
-
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
30662
|
-
if (callsHook) return false;
|
|
30663
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
30664
|
-
callsHook = true;
|
|
30665
|
-
return false;
|
|
30666
|
-
}
|
|
30667
|
-
});
|
|
30668
|
-
return callsHook;
|
|
30669
|
-
};
|
|
30670
|
-
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
|
|
30671
|
-
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
30672
|
-
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
30673
|
-
const ancestors = /* @__PURE__ */ new Set();
|
|
30674
|
-
let cursor = node;
|
|
30675
|
-
while (cursor) {
|
|
30676
|
-
ancestors.add(cursor);
|
|
30677
|
-
cursor = cursor.parent ?? null;
|
|
30678
|
-
}
|
|
30679
|
-
for (const statement of body.body ?? []) {
|
|
30680
|
-
if (ancestors.has(statement)) return false;
|
|
30681
|
-
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
30682
|
-
if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
|
|
30683
|
-
let returnsEarly = false;
|
|
30684
|
-
walkAst(statement.consequent, (child) => {
|
|
30685
|
-
if (isFunctionLike$1(child)) return false;
|
|
30686
|
-
if (isNodeOfType(child, "ReturnStatement")) {
|
|
30687
|
-
returnsEarly = true;
|
|
30688
|
-
return false;
|
|
30689
|
-
}
|
|
30690
|
-
});
|
|
30691
|
-
if (returnsEarly) return true;
|
|
30692
|
-
}
|
|
30693
|
-
return false;
|
|
30694
|
-
};
|
|
30695
|
-
const findEnclosingJsxOpeningElement = (node) => {
|
|
30696
|
-
let cursor = node.parent;
|
|
30697
|
-
while (cursor) {
|
|
30698
|
-
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
30699
|
-
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
30700
|
-
cursor = cursor.parent ?? null;
|
|
30701
|
-
}
|
|
30702
|
-
return null;
|
|
30703
|
-
};
|
|
30704
31685
|
const noLocaleFormatInRender = defineRule({
|
|
30705
31686
|
id: "no-locale-format-in-render",
|
|
30706
31687
|
title: "Locale/timezone formatting during render",
|
|
30707
31688
|
severity: "warn",
|
|
30708
31689
|
category: "Correctness",
|
|
30709
|
-
|
|
30710
|
-
"vite",
|
|
30711
|
-
"cra",
|
|
30712
|
-
"expo",
|
|
30713
|
-
"react-native",
|
|
30714
|
-
"unknown"
|
|
30715
|
-
],
|
|
31690
|
+
requires: ["ssr"],
|
|
30716
31691
|
recommendation: "Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.",
|
|
30717
31692
|
create: (context) => {
|
|
30718
31693
|
if (isTestlikeFilename(context.filename)) return {};
|
|
@@ -30722,13 +31697,12 @@ const noLocaleFormatInRender = defineRule({
|
|
|
30722
31697
|
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30723
31698
|
const reportIfRenderPhase = (match) => {
|
|
30724
31699
|
if (reportedNodes.has(match.node)) return;
|
|
30725
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
|
|
31700
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node, context.scopes);
|
|
30726
31701
|
if (!componentOrHookNode) return;
|
|
30727
31702
|
if (fileIsEmailTemplate) return;
|
|
30728
31703
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30729
|
-
if (
|
|
30730
|
-
if (
|
|
30731
|
-
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
|
|
31704
|
+
if (isGatedByFalsyInitialState(match.node, context.scopes)) return;
|
|
31705
|
+
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode, context.scopes)) return;
|
|
30732
31706
|
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
|
|
30733
31707
|
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
|
|
30734
31708
|
reportedNodes.add(match.node);
|
|
@@ -30755,7 +31729,7 @@ const noLocaleFormatInRender = defineRule({
|
|
|
30755
31729
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
30756
31730
|
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
30757
31731
|
const helperName = expression.callee.name;
|
|
30758
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(node);
|
|
31732
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node, context.scopes);
|
|
30759
31733
|
if (!componentOrHookNode) return;
|
|
30760
31734
|
const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
|
|
30761
31735
|
if (!helperNode || !isFunctionLike$1(helperNode)) return;
|
|
@@ -30767,9 +31741,8 @@ const noLocaleFormatInRender = defineRule({
|
|
|
30767
31741
|
if (!match || reportedNodes.has(match.node)) return;
|
|
30768
31742
|
if (fileIsEmailTemplate) return;
|
|
30769
31743
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30770
|
-
if (
|
|
30771
|
-
if (
|
|
30772
|
-
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
|
|
31744
|
+
if (isGatedByFalsyInitialState(node, context.scopes)) return;
|
|
31745
|
+
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode, context.scopes)) return;
|
|
30773
31746
|
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
|
|
30774
31747
|
reportedNodes.add(match.node);
|
|
30775
31748
|
context.report({
|
|
@@ -30871,9 +31844,6 @@ const noLongTransitionDuration = defineRule({
|
|
|
30871
31844
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
30872
31845
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
30873
31846
|
//#endregion
|
|
30874
|
-
//#region src/plugin/utils/is-event-handler-attribute.ts
|
|
30875
|
-
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
|
|
30876
|
-
//#endregion
|
|
30877
31847
|
//#region src/plugin/rules/architecture/no-many-boolean-props.ts
|
|
30878
31848
|
const IMPERATIVE_CALLBACK_PREFIX_PATTERN = /^(?:show|hide|enable|disable)[A-Z]/;
|
|
30879
31849
|
const collectCallbackUsedNames = (componentBody, propsParam, scopes) => {
|
|
@@ -31005,7 +31975,7 @@ const noMatchMediaInStateInitializer = defineRule({
|
|
|
31005
31975
|
title: "matchMedia in state initializer",
|
|
31006
31976
|
severity: "warn",
|
|
31007
31977
|
category: "Correctness",
|
|
31008
|
-
|
|
31978
|
+
requires: ["ssr"],
|
|
31009
31979
|
recommendation: "Prefer CSS media queries for layout, or subscribe with `useSyncExternalStore` and provide a stable server snapshot.",
|
|
31010
31980
|
create: (context) => {
|
|
31011
31981
|
if (isTestlikeFilename(context.filename)) return {};
|
|
@@ -31556,6 +32526,41 @@ const noMutableInDeps = defineRule({
|
|
|
31556
32526
|
}
|
|
31557
32527
|
});
|
|
31558
32528
|
//#endregion
|
|
32529
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
32530
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
32531
|
+
let node = callExpression;
|
|
32532
|
+
let parent = node.parent;
|
|
32533
|
+
while (parent) {
|
|
32534
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
32535
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
32536
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
32537
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
32538
|
+
node = parent;
|
|
32539
|
+
parent = node.parent;
|
|
32540
|
+
continue;
|
|
32541
|
+
}
|
|
32542
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
32543
|
+
node = parent;
|
|
32544
|
+
parent = node.parent;
|
|
32545
|
+
continue;
|
|
32546
|
+
}
|
|
32547
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
32548
|
+
node = parent;
|
|
32549
|
+
parent = node.parent;
|
|
32550
|
+
continue;
|
|
32551
|
+
}
|
|
32552
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
32553
|
+
const expressions = parent.expressions ?? [];
|
|
32554
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
32555
|
+
node = parent;
|
|
32556
|
+
parent = node.parent;
|
|
32557
|
+
continue;
|
|
32558
|
+
}
|
|
32559
|
+
return false;
|
|
32560
|
+
}
|
|
32561
|
+
return false;
|
|
32562
|
+
};
|
|
32563
|
+
//#endregion
|
|
31559
32564
|
//#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
|
|
31560
32565
|
const LODASH_MUTATOR_NAMES = new Set([
|
|
31561
32566
|
"set",
|
|
@@ -32846,7 +33851,7 @@ const isUseRefIdentifier = (identifier) => {
|
|
|
32846
33851
|
};
|
|
32847
33852
|
const COMMAND_PROP_NAME_PATTERN = /^(fetch|load|refetch|dispatch|register|render)([A-Z_]|$)/;
|
|
32848
33853
|
const SETTER_NAMED_PROP_PATTERN = /^set[A-Z]/;
|
|
32849
|
-
const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
33854
|
+
const unwrapChainExpression$1 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
32850
33855
|
const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
|
|
32851
33856
|
"useCallback",
|
|
32852
33857
|
"useMemo",
|
|
@@ -32881,7 +33886,7 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
32881
33886
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
32882
33887
|
const node = def.node;
|
|
32883
33888
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
32884
|
-
const initializer = unwrapChainExpression(node.init);
|
|
33889
|
+
const initializer = unwrapChainExpression$1(node.init);
|
|
32885
33890
|
const wrappedFunction = getWrapperHookWrappedFunction(initializer);
|
|
32886
33891
|
if (wrappedFunction) {
|
|
32887
33892
|
if (wrappedFunction.async) return false;
|
|
@@ -32891,10 +33896,170 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
32891
33896
|
return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
|
|
32892
33897
|
}));
|
|
32893
33898
|
};
|
|
33899
|
+
const getDeclarationKind = (declarator) => {
|
|
33900
|
+
const declaration = declarator.parent;
|
|
33901
|
+
return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
|
|
33902
|
+
};
|
|
33903
|
+
const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
33904
|
+
const getDestructuredPropertyName = (bindingIdentifier) => {
|
|
33905
|
+
const property = bindingIdentifier.parent;
|
|
33906
|
+
if (!property || !isNodeOfType(property, "Property")) return null;
|
|
33907
|
+
if (property.computed) return isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" ? String(property.key.value) : null;
|
|
33908
|
+
return isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
33909
|
+
};
|
|
33910
|
+
const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
|
|
33911
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
33912
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
33913
|
+
const callbackReference = getRef(analysis, unwrappedExpression);
|
|
33914
|
+
const callbackVariable = callbackReference?.resolved;
|
|
33915
|
+
if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable)) return null;
|
|
33916
|
+
if (isProp(analysis, callbackReference)) {
|
|
33917
|
+
if (isWholePropsObjectReference(analysis, callbackReference)) return null;
|
|
33918
|
+
const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
|
|
33919
|
+
return (bindingIdentifier && getDestructuredPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
|
|
33920
|
+
}
|
|
33921
|
+
if (hasMutableBindingWrite(callbackReference)) return null;
|
|
33922
|
+
const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
33923
|
+
if (definitions.length !== 1) return null;
|
|
33924
|
+
const declarator = definitions[0];
|
|
33925
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || getDeclarationKind(declarator) !== "const" || !declarator.init) return null;
|
|
33926
|
+
visitedVariables.add(callbackVariable);
|
|
33927
|
+
if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
33928
|
+
const bindingIdentifier = callbackVariable.defs[0]?.name;
|
|
33929
|
+
const propertyName = bindingIdentifier ? getDestructuredPropertyName(bindingIdentifier) : null;
|
|
33930
|
+
const propsReference = getDownstreamRefs(analysis, declarator.init).find((candidateReference) => isWholePropsObjectReference(analysis, candidateReference));
|
|
33931
|
+
return propertyName && propsReference ? propertyName : null;
|
|
33932
|
+
}
|
|
33933
|
+
if (!isNodeOfType(declarator.id, "Identifier")) return null;
|
|
33934
|
+
return getParentCallbackPropName(analysis, declarator.init, visitedVariables);
|
|
33935
|
+
}
|
|
33936
|
+
if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
|
|
33937
|
+
const callbackName = getStaticMemberPropertyName(unwrappedExpression);
|
|
33938
|
+
if (!callbackName) return null;
|
|
33939
|
+
const receiver = stripParenExpression(unwrappedExpression.object);
|
|
33940
|
+
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
33941
|
+
const receiverReference = getRef(analysis, receiver);
|
|
33942
|
+
if (!receiverReference || !isWholePropsObjectReference(analysis, receiverReference)) return null;
|
|
33943
|
+
return callbackName;
|
|
33944
|
+
};
|
|
33945
|
+
const isNullishRefInitializer = (initializer) => {
|
|
33946
|
+
if (!initializer) return true;
|
|
33947
|
+
const unwrappedInitializer = stripParenExpression(initializer);
|
|
33948
|
+
if (isNodeOfType(unwrappedInitializer, "Literal")) return unwrappedInitializer.value === null;
|
|
33949
|
+
if (!isNodeOfType(unwrappedInitializer, "Identifier")) return false;
|
|
33950
|
+
return unwrappedInitializer.name === "undefined";
|
|
33951
|
+
};
|
|
33952
|
+
const getDirectComponentBodyStatement = (node, componentBody) => {
|
|
33953
|
+
let current = node;
|
|
33954
|
+
while (current?.parent && current.parent !== componentBody) current = current.parent;
|
|
33955
|
+
return current?.parent === componentBody ? current : null;
|
|
33956
|
+
};
|
|
33957
|
+
const getVariableForDeclarator = (analysis, declarator) => {
|
|
33958
|
+
for (const scope of analysis.scopeManager.scopes) {
|
|
33959
|
+
const variable = scope.variables.find((candidateVariable) => candidateVariable.defs.some((definition) => definition.node === declarator));
|
|
33960
|
+
if (variable) return variable;
|
|
33961
|
+
}
|
|
33962
|
+
return null;
|
|
33963
|
+
};
|
|
33964
|
+
const getRefMember = (identifier) => {
|
|
33965
|
+
const receiver = findTransparentExpressionRoot(identifier);
|
|
33966
|
+
const member = receiver.parent;
|
|
33967
|
+
if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== receiver) return null;
|
|
33968
|
+
return member;
|
|
33969
|
+
};
|
|
33970
|
+
const getRefMemberAssignment = (identifier) => {
|
|
33971
|
+
const member = getRefMember(identifier);
|
|
33972
|
+
if (!member) return null;
|
|
33973
|
+
const memberRoot = findTransparentExpressionRoot(member);
|
|
33974
|
+
const assignment = memberRoot.parent;
|
|
33975
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== memberRoot) return null;
|
|
33976
|
+
return assignment;
|
|
33977
|
+
};
|
|
33978
|
+
const getRefAliasDeclarator = (identifier) => {
|
|
33979
|
+
const initializer = findTransparentExpressionRoot(identifier);
|
|
33980
|
+
const declarator = initializer.parent;
|
|
33981
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== initializer || !isNodeOfType(declarator.id, "Identifier") || getDeclarationKind(declarator) !== "const") return null;
|
|
33982
|
+
return declarator;
|
|
33983
|
+
};
|
|
33984
|
+
const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
|
|
33985
|
+
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
33986
|
+
const receiverReference = getRef(analysis, receiver);
|
|
33987
|
+
if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
|
|
33988
|
+
const variables = /* @__PURE__ */ new Set();
|
|
33989
|
+
let currentVariable = receiverReference.resolved;
|
|
33990
|
+
let refCall = null;
|
|
33991
|
+
while (currentVariable && !variables.has(currentVariable)) {
|
|
33992
|
+
variables.add(currentVariable);
|
|
33993
|
+
const definitions = currentVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
33994
|
+
if (definitions.length !== 1) return null;
|
|
33995
|
+
const declarator = definitions[0];
|
|
33996
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) return null;
|
|
33997
|
+
if (isNodeOfType(declarator.init, "CallExpression") && isReactUseRefCall(declarator.init)) {
|
|
33998
|
+
refCall = declarator.init;
|
|
33999
|
+
break;
|
|
34000
|
+
}
|
|
34001
|
+
if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
|
|
34002
|
+
const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
|
|
34003
|
+
if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
|
|
34004
|
+
currentVariable = upstreamReference.resolved;
|
|
34005
|
+
}
|
|
34006
|
+
if (!refCall) return null;
|
|
34007
|
+
const pendingVariables = [...variables];
|
|
34008
|
+
while (pendingVariables.length > 0) {
|
|
34009
|
+
const variable = pendingVariables.pop();
|
|
34010
|
+
if (!variable) continue;
|
|
34011
|
+
for (const candidateReference of variable.references) {
|
|
34012
|
+
const aliasDeclarator = getRefAliasDeclarator(candidateReference.identifier);
|
|
34013
|
+
if (!aliasDeclarator) continue;
|
|
34014
|
+
const aliasVariable = getVariableForDeclarator(analysis, aliasDeclarator);
|
|
34015
|
+
if (!aliasVariable || variables.has(aliasVariable)) continue;
|
|
34016
|
+
if (aliasVariable.references.some((aliasReference) => aliasReference.isWrite() && !aliasReference.init)) return null;
|
|
34017
|
+
variables.add(aliasVariable);
|
|
34018
|
+
pendingVariables.push(aliasVariable);
|
|
34019
|
+
}
|
|
34020
|
+
}
|
|
34021
|
+
return {
|
|
34022
|
+
refCall,
|
|
34023
|
+
variables
|
|
34024
|
+
};
|
|
34025
|
+
};
|
|
34026
|
+
const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactUseRefCall) => {
|
|
34027
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
34028
|
+
if (!isNodeOfType(callee, "MemberExpression") || getStaticMemberPropertyName(callee) !== "current") return null;
|
|
34029
|
+
const bindingProvenance = getRefBindingProvenance(analysis, stripParenExpression(callee.object), isReactUseRefCall);
|
|
34030
|
+
if (!bindingProvenance) return null;
|
|
34031
|
+
const { refCall, variables } = bindingProvenance;
|
|
34032
|
+
const componentFunction = findEnclosingFunction(effectCall);
|
|
34033
|
+
if (!componentFunction || !isFunctionLike$1(componentFunction) || !isComponentFunction$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
|
|
34034
|
+
if (!getDirectComponentBodyStatement(effectCall, componentFunction.body)) return null;
|
|
34035
|
+
const callbackPropNames = /* @__PURE__ */ new Set();
|
|
34036
|
+
const initializer = refCall.arguments?.[0];
|
|
34037
|
+
const initializerCallbackName = initializer ? getParentCallbackPropName(analysis, initializer) : null;
|
|
34038
|
+
if (initializerCallbackName) callbackPropNames.add(initializerCallbackName);
|
|
34039
|
+
else if (!isNullishRefInitializer(initializer)) return null;
|
|
34040
|
+
for (const variable of variables) for (const candidateReference of variable.references) {
|
|
34041
|
+
if (candidateReference.init) continue;
|
|
34042
|
+
if (candidateReference.isWrite()) return null;
|
|
34043
|
+
const identifier = candidateReference.identifier;
|
|
34044
|
+
if (getRefAliasDeclarator(identifier)) continue;
|
|
34045
|
+
const member = getRefMember(identifier);
|
|
34046
|
+
if (!member || getStaticMemberPropertyName(member) !== "current") return null;
|
|
34047
|
+
const memberParent = findTransparentExpressionRoot(member).parent;
|
|
34048
|
+
if (memberParent && (isNodeOfType(memberParent, "UpdateExpression") || isNodeOfType(memberParent, "UnaryExpression") && memberParent.operator === "delete")) return null;
|
|
34049
|
+
const assignment = getRefMemberAssignment(identifier);
|
|
34050
|
+
if (!assignment) continue;
|
|
34051
|
+
const assignmentStatement = findTransparentExpressionRoot(assignment).parent;
|
|
34052
|
+
if (assignment.operator !== "=" || !assignmentStatement || !isNodeOfType(assignmentStatement, "ExpressionStatement") || assignmentStatement.parent !== componentFunction.body) return null;
|
|
34053
|
+
const assignedCallbackName = getParentCallbackPropName(analysis, assignment.right);
|
|
34054
|
+
if (!assignedCallbackName) return null;
|
|
34055
|
+
callbackPropNames.add(assignedCallbackName);
|
|
34056
|
+
}
|
|
34057
|
+
return callbackPropNames.size > 0 ? { callbackPropNames } : null;
|
|
34058
|
+
};
|
|
32894
34059
|
const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
32895
34060
|
const node = def.node;
|
|
32896
34061
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
32897
|
-
return getWrapperHookWrappedFunction(unwrapChainExpression(node.init)) !== null;
|
|
34062
|
+
return getWrapperHookWrappedFunction(unwrapChainExpression$1(node.init)) !== null;
|
|
32898
34063
|
}));
|
|
32899
34064
|
const isHandlerBagArgument = (analysis, argument) => {
|
|
32900
34065
|
if (!isNodeOfType(argument, "ObjectExpression")) return false;
|
|
@@ -32916,7 +34081,7 @@ const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
|
|
|
32916
34081
|
const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
32917
34082
|
const node = def.node;
|
|
32918
34083
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
32919
|
-
const init = unwrapChainExpression(node.init);
|
|
34084
|
+
const init = unwrapChainExpression$1(node.init);
|
|
32920
34085
|
if (!isNodeOfType(init, "CallExpression")) return false;
|
|
32921
34086
|
const callee = init.callee;
|
|
32922
34087
|
if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
|
|
@@ -32946,71 +34111,79 @@ const noPassDataToParent = defineRule({
|
|
|
32946
34111
|
severity: "warn",
|
|
32947
34112
|
tags: ["test-noise"],
|
|
32948
34113
|
recommendation: "Fetch the data in the parent and pass it down as a prop (or return it from the hook), instead of handing it back up through a prop callback in a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#passing-data-to-the-parent",
|
|
32949
|
-
create: (context) =>
|
|
32950
|
-
|
|
32951
|
-
|
|
32952
|
-
|
|
32953
|
-
|
|
32954
|
-
|
|
32955
|
-
|
|
32956
|
-
|
|
32957
|
-
|
|
32958
|
-
|
|
32959
|
-
const
|
|
32960
|
-
if (!
|
|
32961
|
-
|
|
32962
|
-
if (!
|
|
32963
|
-
const
|
|
32964
|
-
|
|
32965
|
-
|
|
32966
|
-
|
|
32967
|
-
if (
|
|
32968
|
-
|
|
32969
|
-
|
|
32970
|
-
|
|
32971
|
-
|
|
32972
|
-
|
|
32973
|
-
|
|
32974
|
-
|
|
32975
|
-
|
|
32976
|
-
|
|
32977
|
-
|
|
32978
|
-
|
|
32979
|
-
|
|
32980
|
-
|
|
32981
|
-
|
|
32982
|
-
|
|
32983
|
-
|
|
32984
|
-
|
|
32985
|
-
|
|
32986
|
-
|
|
32987
|
-
|
|
32988
|
-
|
|
32989
|
-
|
|
32990
|
-
|
|
32991
|
-
|
|
32992
|
-
|
|
32993
|
-
|
|
32994
|
-
|
|
32995
|
-
|
|
32996
|
-
|
|
32997
|
-
|
|
32998
|
-
|
|
32999
|
-
|
|
33000
|
-
if (
|
|
33001
|
-
if (
|
|
33002
|
-
|
|
33003
|
-
|
|
33004
|
-
|
|
33005
|
-
|
|
33006
|
-
|
|
33007
|
-
|
|
33008
|
-
|
|
33009
|
-
|
|
33010
|
-
|
|
33011
|
-
|
|
33012
|
-
|
|
33013
|
-
|
|
34114
|
+
create: (context) => {
|
|
34115
|
+
const isReactUseRefCall = (node) => isReactApiCall(node, "useRef", context.scopes, {
|
|
34116
|
+
allowGlobalReactNamespace: true,
|
|
34117
|
+
allowUnboundBareCalls: true
|
|
34118
|
+
});
|
|
34119
|
+
return { CallExpression(node) {
|
|
34120
|
+
if (!isUseEffect(node)) return;
|
|
34121
|
+
const analysis = getProgramAnalysis(node);
|
|
34122
|
+
if (!analysis) return;
|
|
34123
|
+
if (hasCleanup(analysis, node)) return;
|
|
34124
|
+
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
34125
|
+
if (!effectFnRefs) return;
|
|
34126
|
+
const effectFn = getEffectFn(analysis, node);
|
|
34127
|
+
if (!effectFn) return;
|
|
34128
|
+
for (const ref of effectFnRefs) {
|
|
34129
|
+
const callExpr = getCallExpr(ref);
|
|
34130
|
+
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
34131
|
+
const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall);
|
|
34132
|
+
if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
|
|
34133
|
+
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
34134
|
+
const calleeNode = unwrapChainExpression$1(callExpr.callee);
|
|
34135
|
+
const identifier = ref.identifier;
|
|
34136
|
+
if (callbackRefProvenance) {
|
|
34137
|
+
if ([...callbackRefProvenance.callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
|
|
34138
|
+
} else if (calleeNode === identifier) {
|
|
34139
|
+
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
34140
|
+
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
34141
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
34142
|
+
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
34143
|
+
if (isCustomHookParameter(ref)) continue;
|
|
34144
|
+
} else continue;
|
|
34145
|
+
const methodName = getCallMethodName(calleeNode);
|
|
34146
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
34147
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
34148
|
+
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
34149
|
+
if (!callbackRefProvenance && isNamespacedApiCallee(calleeNode)) continue;
|
|
34150
|
+
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) ?? ""));
|
|
34151
|
+
const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
|
|
34152
|
+
const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
|
|
34153
|
+
if (isFunctionLike$1(argument)) {
|
|
34154
|
+
if (!isSetterNamedCallee) return [];
|
|
34155
|
+
return getFunctionalUpdaterDataRefs(analysis, argument);
|
|
34156
|
+
}
|
|
34157
|
+
if (isHandlerBagArgument(analysis, argument)) return [];
|
|
34158
|
+
if (isParentWiredHookResultArgument(analysis, argument)) return [];
|
|
34159
|
+
if (isNodeOfType(argument, "Identifier")) {
|
|
34160
|
+
const argumentRef = getRef(analysis, argument);
|
|
34161
|
+
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
34162
|
+
}
|
|
34163
|
+
return getDownstreamRefs(analysis, argument);
|
|
34164
|
+
}).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
34165
|
+
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
34166
|
+
if (!argsUpstreamRefs.some((argRef) => {
|
|
34167
|
+
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
34168
|
+
if (isProp(analysis, argRef)) return false;
|
|
34169
|
+
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
34170
|
+
if (isRefCurrent(argRef)) return false;
|
|
34171
|
+
if (isConstant(argRef)) return false;
|
|
34172
|
+
if (isParentWiredHookResultRef(analysis, argRef)) return false;
|
|
34173
|
+
if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
|
|
34174
|
+
if (resolvesToFunctionBinding(argRef)) return false;
|
|
34175
|
+
const argIdentifier = argRef.identifier;
|
|
34176
|
+
if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
|
|
34177
|
+
if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
|
|
34178
|
+
return true;
|
|
34179
|
+
})) continue;
|
|
34180
|
+
context.report({
|
|
34181
|
+
node: callExpr,
|
|
34182
|
+
message: "Handing data back to a parent from a useEffect costs your users an extra render."
|
|
34183
|
+
});
|
|
34184
|
+
}
|
|
34185
|
+
} };
|
|
34186
|
+
}
|
|
33014
34187
|
});
|
|
33015
34188
|
//#endregion
|
|
33016
34189
|
//#region src/plugin/utils/is-call-result-consumed-as-argument.ts
|
|
@@ -36272,6 +37445,140 @@ const noUnescapedEntities = defineRule({
|
|
|
36272
37445
|
} })
|
|
36273
37446
|
});
|
|
36274
37447
|
//#endregion
|
|
37448
|
+
//#region src/plugin/rules/performance/no-unguarded-browser-global-in-render-or-hook-init.ts
|
|
37449
|
+
const BROWSER_GLOBAL_NAMES = new Set([
|
|
37450
|
+
"window",
|
|
37451
|
+
"document",
|
|
37452
|
+
"localStorage",
|
|
37453
|
+
"sessionStorage",
|
|
37454
|
+
"navigator",
|
|
37455
|
+
"matchMedia"
|
|
37456
|
+
]);
|
|
37457
|
+
const getTypeofBrowserGlobalName = (expression, context) => {
|
|
37458
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
37459
|
+
if (!isNodeOfType(unwrappedExpression, "UnaryExpression") || unwrappedExpression.operator !== "typeof") return null;
|
|
37460
|
+
const argument = stripParenExpression(unwrappedExpression.argument);
|
|
37461
|
+
if (isNodeOfType(argument, "Identifier")) return BROWSER_GLOBAL_NAMES.has(argument.name) && context.scopes.isGlobalReference(argument) ? argument.name : null;
|
|
37462
|
+
if (!isNodeOfType(argument, "MemberExpression") || argument.computed || !isNodeOfType(argument.object, "Identifier") || argument.object.name !== "globalThis" || !context.scopes.isGlobalReference(argument.object) || !isNodeOfType(argument.property, "Identifier") || !BROWSER_GLOBAL_NAMES.has(argument.property.name)) return null;
|
|
37463
|
+
return argument.property.name;
|
|
37464
|
+
};
|
|
37465
|
+
const browserGuardCoversGlobal = (guardName, browserGlobalName) => guardName === browserGlobalName || guardName === "window" || guardName === "document";
|
|
37466
|
+
const mergeAvailability = (leftAvailability, rightAvailability) => {
|
|
37467
|
+
if (leftAvailability === null) return rightAvailability;
|
|
37468
|
+
if (rightAvailability === null) return leftAvailability;
|
|
37469
|
+
return leftAvailability === rightAvailability ? leftAvailability : null;
|
|
37470
|
+
};
|
|
37471
|
+
const readAvailabilityWhenPredicate = (expression, browserGlobalName, context, predicateResult) => {
|
|
37472
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
37473
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return readAvailabilityWhenPredicate(unwrappedExpression.argument, browserGlobalName, context, !predicateResult);
|
|
37474
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
|
|
37475
|
+
if (unwrappedExpression.operator === "&&" && predicateResult) return mergeAvailability(readAvailabilityWhenPredicate(unwrappedExpression.left, browserGlobalName, context, true), readAvailabilityWhenPredicate(unwrappedExpression.right, browserGlobalName, context, true));
|
|
37476
|
+
if (unwrappedExpression.operator === "||" && !predicateResult) return mergeAvailability(readAvailabilityWhenPredicate(unwrappedExpression.left, browserGlobalName, context, false), readAvailabilityWhenPredicate(unwrappedExpression.right, browserGlobalName, context, false));
|
|
37477
|
+
return null;
|
|
37478
|
+
}
|
|
37479
|
+
if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
|
|
37480
|
+
const leftTypeofName = getTypeofBrowserGlobalName(unwrappedExpression.left, context);
|
|
37481
|
+
const rightTypeofName = getTypeofBrowserGlobalName(unwrappedExpression.right, context);
|
|
37482
|
+
const leftComparedType = isNodeOfType(unwrappedExpression.left, "Literal") && typeof unwrappedExpression.left.value === "string" ? unwrappedExpression.left.value : null;
|
|
37483
|
+
const rightComparedType = isNodeOfType(unwrappedExpression.right, "Literal") && typeof unwrappedExpression.right.value === "string" ? unwrappedExpression.right.value : null;
|
|
37484
|
+
const guardName = leftTypeofName && rightComparedType ? leftTypeofName : rightTypeofName && leftComparedType ? rightTypeofName : null;
|
|
37485
|
+
const comparedType = leftTypeofName && rightComparedType ? rightComparedType : rightTypeofName && leftComparedType ? leftComparedType : null;
|
|
37486
|
+
if (!guardName || !browserGuardCoversGlobal(guardName, browserGlobalName)) return null;
|
|
37487
|
+
if (!comparedType) return null;
|
|
37488
|
+
const isEquality = unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==";
|
|
37489
|
+
const isInequality = unwrappedExpression.operator === "!==" || unwrappedExpression.operator === "!=";
|
|
37490
|
+
if (!isEquality && !isInequality) return null;
|
|
37491
|
+
const browserType = guardName === "matchMedia" ? "function" : "object";
|
|
37492
|
+
const browserResult = isEquality ? browserType === comparedType : browserType !== comparedType;
|
|
37493
|
+
if (browserResult === (isEquality ? comparedType === "undefined" : comparedType !== "undefined")) return null;
|
|
37494
|
+
return predicateResult === browserResult;
|
|
37495
|
+
};
|
|
37496
|
+
const isInsideAvailabilityGuard = (node, browserGlobalName, context) => {
|
|
37497
|
+
let currentNode = node;
|
|
37498
|
+
let parentNode = currentNode.parent;
|
|
37499
|
+
while (parentNode) {
|
|
37500
|
+
if (isFunctionLike$1(parentNode) && !executesDuringRender(parentNode, context.scopes)) break;
|
|
37501
|
+
if (isNodeOfType(parentNode, "LogicalExpression") && (parentNode.operator === "&&" || parentNode.operator === "||") && parentNode.right === currentNode && readAvailabilityWhenPredicate(parentNode.left, browserGlobalName, context, parentNode.operator === "&&") === true) return true;
|
|
37502
|
+
if (isNodeOfType(parentNode, "ConditionalExpression")) {
|
|
37503
|
+
if (parentNode.consequent === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, true) === true || parentNode.alternate === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, false) === true) return true;
|
|
37504
|
+
}
|
|
37505
|
+
if (isNodeOfType(parentNode, "IfStatement")) {
|
|
37506
|
+
if (parentNode.consequent === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, true) === true || parentNode.alternate === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, false) === true) return true;
|
|
37507
|
+
}
|
|
37508
|
+
currentNode = parentNode;
|
|
37509
|
+
parentNode = currentNode.parent;
|
|
37510
|
+
}
|
|
37511
|
+
return false;
|
|
37512
|
+
};
|
|
37513
|
+
const isAfterAvailabilityEarlyExit = (node, componentOrHookNode, browserGlobalName, context) => {
|
|
37514
|
+
const enclosingFunction = findEnclosingFunction(node);
|
|
37515
|
+
if (!enclosingFunction || enclosingFunction !== componentOrHookNode && !executesDuringRender(enclosingFunction, context.scopes) || !isFunctionLike$1(enclosingFunction) || !isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
|
|
37516
|
+
let currentNode = node;
|
|
37517
|
+
while (currentNode !== enclosingFunction) {
|
|
37518
|
+
const parentNode = currentNode.parent;
|
|
37519
|
+
if (!parentNode) return false;
|
|
37520
|
+
if (isNodeOfType(parentNode, "BlockStatement")) for (const statement of parentNode.body) {
|
|
37521
|
+
if (statement === currentNode) break;
|
|
37522
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
37523
|
+
if (readAvailabilityWhenPredicate(statement.test, browserGlobalName, context, false) === true && statementAlwaysExits(statement.consequent)) return true;
|
|
37524
|
+
if (readAvailabilityWhenPredicate(statement.test, browserGlobalName, context, true) === true && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
|
|
37525
|
+
}
|
|
37526
|
+
currentNode = parentNode;
|
|
37527
|
+
}
|
|
37528
|
+
return false;
|
|
37529
|
+
};
|
|
37530
|
+
const isTypeofProbe = (node) => {
|
|
37531
|
+
const expressionRoot = findTransparentExpressionRoot(node);
|
|
37532
|
+
const parentNode = expressionRoot.parent;
|
|
37533
|
+
return isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "typeof" && parentNode.argument === expressionRoot;
|
|
37534
|
+
};
|
|
37535
|
+
const noUnguardedBrowserGlobalInRenderOrHookInit = defineRule({
|
|
37536
|
+
id: "no-unguarded-browser-global-in-render-or-hook-init",
|
|
37537
|
+
title: "Browser global read during server render",
|
|
37538
|
+
severity: "error",
|
|
37539
|
+
category: "Correctness",
|
|
37540
|
+
requires: ["ssr"],
|
|
37541
|
+
recommendation: "Move browser-only reads into an effect or event, guard them behind a client-only render path, or use useSyncExternalStore with a stable server snapshot.",
|
|
37542
|
+
create: (context) => {
|
|
37543
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
37544
|
+
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
37545
|
+
let fileIsEmailTemplate = false;
|
|
37546
|
+
const reportedNodes = /* @__PURE__ */ new Set();
|
|
37547
|
+
const reportBrowserRead = (node, browserGlobalName) => {
|
|
37548
|
+
if (reportedNodes.has(node) || isTypeofProbe(node)) return;
|
|
37549
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node, context.scopes);
|
|
37550
|
+
if (!componentOrHookNode) return;
|
|
37551
|
+
if (fileIsEmailTemplate) return;
|
|
37552
|
+
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(node) ?? node)) return;
|
|
37553
|
+
if (isGatedByFalsyInitialState(node, context.scopes)) return;
|
|
37554
|
+
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode, context.scopes)) return;
|
|
37555
|
+
if (isInsideAvailabilityGuard(node, browserGlobalName, context)) return;
|
|
37556
|
+
if (isAfterAvailabilityEarlyExit(node, componentOrHookNode, browserGlobalName, context)) return;
|
|
37557
|
+
reportedNodes.add(node);
|
|
37558
|
+
context.report({
|
|
37559
|
+
node,
|
|
37560
|
+
message: `\`${browserGlobalName}\` is read while React is rendering on the server, where browser globals are unavailable. Move the read into an effect or event, or provide a stable server snapshot.`
|
|
37561
|
+
});
|
|
37562
|
+
};
|
|
37563
|
+
return {
|
|
37564
|
+
Program(node) {
|
|
37565
|
+
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
37566
|
+
},
|
|
37567
|
+
Identifier(node) {
|
|
37568
|
+
if (!BROWSER_GLOBAL_NAMES.has(node.name)) return;
|
|
37569
|
+
if (!context.scopes.isGlobalReference(node)) return;
|
|
37570
|
+
reportBrowserRead(node, node.name);
|
|
37571
|
+
},
|
|
37572
|
+
MemberExpression(node) {
|
|
37573
|
+
if (node.computed) return;
|
|
37574
|
+
const objectNode = stripParenExpression(node.object);
|
|
37575
|
+
if (!isNodeOfType(objectNode, "Identifier") || objectNode.name !== "globalThis" || !context.scopes.isGlobalReference(objectNode) || !isNodeOfType(node.property, "Identifier") || !BROWSER_GLOBAL_NAMES.has(node.property.name)) return;
|
|
37576
|
+
reportBrowserRead(node, node.property.name);
|
|
37577
|
+
}
|
|
37578
|
+
};
|
|
37579
|
+
}
|
|
37580
|
+
});
|
|
37581
|
+
//#endregion
|
|
36275
37582
|
//#region src/plugin/constants/dom-aria-properties.ts
|
|
36276
37583
|
const ARIA_PROPERTY_NAMES = new Set([
|
|
36277
37584
|
"activedescendant",
|
|
@@ -40114,6 +41421,123 @@ const preferUseEffectEvent = defineRule({
|
|
|
40114
41421
|
}
|
|
40115
41422
|
});
|
|
40116
41423
|
//#endregion
|
|
41424
|
+
//#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
|
|
41425
|
+
const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
41426
|
+
"each",
|
|
41427
|
+
"every",
|
|
41428
|
+
"filter",
|
|
41429
|
+
"find",
|
|
41430
|
+
"findIndex",
|
|
41431
|
+
"findLast",
|
|
41432
|
+
"findLastIndex",
|
|
41433
|
+
"flatMap",
|
|
41434
|
+
"forEach",
|
|
41435
|
+
"map",
|
|
41436
|
+
"reduce",
|
|
41437
|
+
"reduceRight",
|
|
41438
|
+
"some",
|
|
41439
|
+
"sort",
|
|
41440
|
+
"toSorted"
|
|
41441
|
+
]);
|
|
41442
|
+
const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
41443
|
+
"from",
|
|
41444
|
+
"fromAsync",
|
|
41445
|
+
"groupBy"
|
|
41446
|
+
]);
|
|
41447
|
+
const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
41448
|
+
const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
|
|
41449
|
+
const isListenerRemovalViaNullHandler = (callNode) => {
|
|
41450
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
41451
|
+
const callee = unwrapChainExpression(callNode.callee);
|
|
41452
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
|
|
41453
|
+
};
|
|
41454
|
+
const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
|
|
41455
|
+
"off",
|
|
41456
|
+
"removeEventListener",
|
|
41457
|
+
"removeListener"
|
|
41458
|
+
]);
|
|
41459
|
+
const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
|
|
41460
|
+
if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
|
|
41461
|
+
const handlerArgument = callNode.arguments?.[1];
|
|
41462
|
+
return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
|
|
41463
|
+
};
|
|
41464
|
+
const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
41465
|
+
const callNode = unwrapChainExpression(node);
|
|
41466
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
41467
|
+
if (isListenerRemovalViaNullHandler(callNode)) return true;
|
|
41468
|
+
const callee = unwrapChainExpression(callNode.callee);
|
|
41469
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
41470
|
+
if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
|
|
41471
|
+
if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
|
|
41472
|
+
if (knownCleanupFunctionNames.has(callee.name)) return true;
|
|
41473
|
+
return false;
|
|
41474
|
+
}
|
|
41475
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
41476
|
+
if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
|
|
41477
|
+
if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
|
|
41478
|
+
return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
|
|
41479
|
+
}
|
|
41480
|
+
return false;
|
|
41481
|
+
};
|
|
41482
|
+
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);
|
|
41483
|
+
const isIteratorCallbackArgument = (node) => {
|
|
41484
|
+
const parentNode = node.parent;
|
|
41485
|
+
if (!isNodeOfType(parentNode, "CallExpression")) return false;
|
|
41486
|
+
if (!parentNode.arguments?.some((argument) => argument === node)) return false;
|
|
41487
|
+
const callee = unwrapChainExpression(parentNode.callee);
|
|
41488
|
+
if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
|
|
41489
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
|
|
41490
|
+
};
|
|
41491
|
+
const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
41492
|
+
let didFindRelease = false;
|
|
41493
|
+
walkAst(node, (child) => {
|
|
41494
|
+
if (didFindRelease) return false;
|
|
41495
|
+
if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
|
|
41496
|
+
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
41497
|
+
didFindRelease = true;
|
|
41498
|
+
return false;
|
|
41499
|
+
}
|
|
41500
|
+
});
|
|
41501
|
+
return didFindRelease;
|
|
41502
|
+
};
|
|
41503
|
+
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
41504
|
+
if (!isFunctionLike$1(node)) return false;
|
|
41505
|
+
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
41506
|
+
};
|
|
41507
|
+
const isProvablyNoOpCleanupFunction = (node) => {
|
|
41508
|
+
if (!isFunctionLike$1(node)) return false;
|
|
41509
|
+
let sawNoOpRemoval = false;
|
|
41510
|
+
let sawOtherCall = false;
|
|
41511
|
+
walkAst(node.body, (child) => {
|
|
41512
|
+
if (sawOtherCall) return false;
|
|
41513
|
+
if (isFunctionLike$1(child)) return false;
|
|
41514
|
+
const callNode = unwrapChainExpression(child);
|
|
41515
|
+
if (!isNodeOfType(callNode, "CallExpression")) return;
|
|
41516
|
+
const callee = unwrapChainExpression(callNode.callee);
|
|
41517
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
|
|
41518
|
+
sawNoOpRemoval = true;
|
|
41519
|
+
return;
|
|
41520
|
+
}
|
|
41521
|
+
sawOtherCall = true;
|
|
41522
|
+
});
|
|
41523
|
+
return sawNoOpRemoval && !sawOtherCall;
|
|
41524
|
+
};
|
|
41525
|
+
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
|
|
41526
|
+
if (!returnedValue) return false;
|
|
41527
|
+
const unwrappedValue = unwrapChainExpression(returnedValue);
|
|
41528
|
+
if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
|
|
41529
|
+
if (isNodeOfType(unwrappedValue, "Identifier")) {
|
|
41530
|
+
if (unwrappedValue.name === "undefined") return false;
|
|
41531
|
+
if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
|
|
41532
|
+
return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
|
|
41533
|
+
}
|
|
41534
|
+
if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
41535
|
+
if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
|
|
41536
|
+
if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
41537
|
+
if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
|
|
41538
|
+
return false;
|
|
41539
|
+
};
|
|
41540
|
+
//#endregion
|
|
40117
41541
|
//#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
|
|
40118
41542
|
const findUseEffectsInComponent = (componentBody) => {
|
|
40119
41543
|
const effectCalls = [];
|
|
@@ -41582,7 +43006,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
41582
43006
|
title: "Time or random value in JSX",
|
|
41583
43007
|
severity: "warn",
|
|
41584
43008
|
category: "Correctness",
|
|
41585
|
-
|
|
43009
|
+
requires: ["ssr"],
|
|
41586
43010
|
recommendation: "Move time or random values into useEffect+useState so they only run in the browser, or add suppressHydrationWarning to the parent if it's intentional",
|
|
41587
43011
|
create: (context) => {
|
|
41588
43012
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
@@ -41596,8 +43020,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
41596
43020
|
const matched = NONDETERMINISTIC_RENDER_PATTERNS.find((pattern) => pattern.matches(node.expression));
|
|
41597
43021
|
if (matched) {
|
|
41598
43022
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
41599
|
-
if (
|
|
41600
|
-
if (isGatedByFalsyInitialState(node)) return;
|
|
43023
|
+
if (isGatedByFalsyInitialState(node, context.scopes)) return;
|
|
41601
43024
|
if (isInsideMotionTransitionAttribute(node)) return;
|
|
41602
43025
|
context.report({
|
|
41603
43026
|
node,
|
|
@@ -41606,11 +43029,10 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
41606
43029
|
return;
|
|
41607
43030
|
}
|
|
41608
43031
|
walkAst(node.expression, (child) => {
|
|
41609
|
-
if (isFunctionLike$1(child) && !executesDuringRender(child)) return false;
|
|
43032
|
+
if (isFunctionLike$1(child) && !executesDuringRender(child, context.scopes)) return false;
|
|
41610
43033
|
for (const pattern of NONDETERMINISTIC_RENDER_PATTERNS) if (pattern.matches(child)) {
|
|
41611
43034
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
41612
|
-
if (
|
|
41613
|
-
if (isGatedByFalsyInitialState(child)) return;
|
|
43035
|
+
if (isGatedByFalsyInitialState(child, context.scopes)) return;
|
|
41614
43036
|
if (isInsideMotionTransitionAttribute(child)) return;
|
|
41615
43037
|
if (pattern.display === "new Date()" && isYearOnlyDateRead(child)) return;
|
|
41616
43038
|
context.report({
|
|
@@ -54947,6 +56369,18 @@ const reactDoctorRules = [
|
|
|
54947
56369
|
category: "Accessibility"
|
|
54948
56370
|
}
|
|
54949
56371
|
},
|
|
56372
|
+
{
|
|
56373
|
+
key: "react-doctor/no-hydration-branch-on-browser-global",
|
|
56374
|
+
id: "no-hydration-branch-on-browser-global",
|
|
56375
|
+
source: "react-doctor",
|
|
56376
|
+
originallyExternal: false,
|
|
56377
|
+
rule: {
|
|
56378
|
+
...noHydrationBranchOnBrowserGlobal,
|
|
56379
|
+
framework: "global",
|
|
56380
|
+
category: "Bugs",
|
|
56381
|
+
requires: [...new Set(["react", ...noHydrationBranchOnBrowserGlobal.requires ?? []])]
|
|
56382
|
+
}
|
|
56383
|
+
},
|
|
54950
56384
|
{
|
|
54951
56385
|
key: "react-doctor/no-img-lazy-with-high-fetchpriority",
|
|
54952
56386
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
@@ -54959,6 +56393,18 @@ const reactDoctorRules = [
|
|
|
54959
56393
|
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
54960
56394
|
}
|
|
54961
56395
|
},
|
|
56396
|
+
{
|
|
56397
|
+
key: "react-doctor/no-impure-state-updater",
|
|
56398
|
+
id: "no-impure-state-updater",
|
|
56399
|
+
source: "react-doctor",
|
|
56400
|
+
originallyExternal: false,
|
|
56401
|
+
rule: {
|
|
56402
|
+
...noImpureStateUpdater,
|
|
56403
|
+
framework: "global",
|
|
56404
|
+
category: "Bugs",
|
|
56405
|
+
requires: [...new Set(["react", ...noImpureStateUpdater.requires ?? []])]
|
|
56406
|
+
}
|
|
56407
|
+
},
|
|
54962
56408
|
{
|
|
54963
56409
|
key: "react-doctor/no-indeterminate-attribute",
|
|
54964
56410
|
id: "no-indeterminate-attribute",
|
|
@@ -55710,6 +57156,18 @@ const reactDoctorRules = [
|
|
|
55710
57156
|
requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
|
|
55711
57157
|
}
|
|
55712
57158
|
},
|
|
57159
|
+
{
|
|
57160
|
+
key: "react-doctor/no-unguarded-browser-global-in-render-or-hook-init",
|
|
57161
|
+
id: "no-unguarded-browser-global-in-render-or-hook-init",
|
|
57162
|
+
source: "react-doctor",
|
|
57163
|
+
originallyExternal: false,
|
|
57164
|
+
rule: {
|
|
57165
|
+
...noUnguardedBrowserGlobalInRenderOrHookInit,
|
|
57166
|
+
framework: "global",
|
|
57167
|
+
category: "Bugs",
|
|
57168
|
+
requires: [...new Set(["react", ...noUnguardedBrowserGlobalInRenderOrHookInit.requires ?? []])]
|
|
57169
|
+
}
|
|
57170
|
+
},
|
|
55713
57171
|
{
|
|
55714
57172
|
key: "react-doctor/no-unknown-property",
|
|
55715
57173
|
id: "no-unknown-property",
|
|
@@ -58042,10 +59500,17 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
58042
59500
|
"nextjs-no-use-search-params-without-suspense",
|
|
58043
59501
|
"no-dynamic-import-path",
|
|
58044
59502
|
"no-full-lodash-import",
|
|
59503
|
+
"no-hydration-branch-on-browser-global",
|
|
58045
59504
|
"no-indeterminate-attribute",
|
|
58046
59505
|
"no-locale-format-in-render",
|
|
58047
59506
|
"no-match-media-in-state-initializer",
|
|
59507
|
+
"no-adjust-state-on-prop-change",
|
|
59508
|
+
"no-derived-state",
|
|
59509
|
+
"no-derived-state-effect",
|
|
59510
|
+
"no-event-handler",
|
|
59511
|
+
"no-initialize-state",
|
|
58048
59512
|
"no-mutating-reducer-state",
|
|
59513
|
+
"no-unguarded-browser-global-in-render-or-hook-init",
|
|
58049
59514
|
"prefer-dynamic-import",
|
|
58050
59515
|
"rendering-hydration-mismatch-time",
|
|
58051
59516
|
"rn-no-legacy-shadow-styles",
|
|
@@ -58097,6 +59562,9 @@ const collectNextjsSearchParamsDependencies = ({ absoluteFilePath, staticImports
|
|
|
58097
59562
|
if (hasAncestorSuspenseLayout(absoluteFilePath)) return;
|
|
58098
59563
|
for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
|
|
58099
59564
|
};
|
|
59565
|
+
const collectEffectValueHelperDependencies = ({ absoluteFilePath, staticImports }) => {
|
|
59566
|
+
for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
|
|
59567
|
+
};
|
|
58100
59568
|
const collectMutatingReducerDependencies = ({ absoluteFilePath, sourceText, staticImports, program }) => {
|
|
58101
59569
|
const namedUseReducerLocals = /* @__PURE__ */ new Set();
|
|
58102
59570
|
const reactObjectLocals = /* @__PURE__ */ new Set();
|
|
@@ -58179,10 +59647,17 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
58179
59647
|
["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
|
|
58180
59648
|
["no-dynamic-import-path", collectNearestManifestDependencies],
|
|
58181
59649
|
["no-full-lodash-import", collectNearestManifestDependencies],
|
|
59650
|
+
["no-hydration-branch-on-browser-global", collectNearestManifestDependencies],
|
|
58182
59651
|
["no-indeterminate-attribute", collectNearestManifestDependencies],
|
|
58183
59652
|
["no-locale-format-in-render", collectNearestManifestDependencies],
|
|
58184
59653
|
["no-match-media-in-state-initializer", collectNearestManifestDependencies],
|
|
59654
|
+
["no-adjust-state-on-prop-change", collectEffectValueHelperDependencies],
|
|
59655
|
+
["no-derived-state", collectEffectValueHelperDependencies],
|
|
59656
|
+
["no-derived-state-effect", collectEffectValueHelperDependencies],
|
|
59657
|
+
["no-event-handler", collectEffectValueHelperDependencies],
|
|
59658
|
+
["no-initialize-state", collectEffectValueHelperDependencies],
|
|
58185
59659
|
["no-mutating-reducer-state", collectMutatingReducerDependencies],
|
|
59660
|
+
["no-unguarded-browser-global-in-render-or-hook-init", collectNearestManifestDependencies],
|
|
58186
59661
|
["prefer-dynamic-import", collectNearestManifestDependencies],
|
|
58187
59662
|
["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
|
|
58188
59663
|
["rn-no-legacy-shadow-styles", collectLegacyArchDependencies],
|