oxlint-plugin-react-doctor 0.7.4-dev.63fc41f → 0.7.4-dev.70b5d99
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 +1 -231
- package/dist/index.js +920 -2827
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1915,10 +1915,7 @@ const isGeneratedImageRenderContext = (context, node) => {
|
|
|
1915
1915
|
if (!node) return false;
|
|
1916
1916
|
const programRoot = findProgramRoot(node);
|
|
1917
1917
|
if (!programRoot) return false;
|
|
1918
|
-
|
|
1919
|
-
if (generatedImageJsxNodes.has(node)) return true;
|
|
1920
|
-
if (isNodeOfType(node, "JSXElement")) return generatedImageJsxNodes.has(node.openingElement);
|
|
1921
|
-
return false;
|
|
1918
|
+
return collectGeneratedImageJsxNodes(programRoot).has(node);
|
|
1922
1919
|
};
|
|
1923
1920
|
//#endregion
|
|
1924
1921
|
//#region src/plugin/utils/object-has-accessible-child.ts
|
|
@@ -8406,54 +8403,6 @@ const effectListenerCleanupMismatch = defineRule({
|
|
|
8406
8403
|
} })
|
|
8407
8404
|
});
|
|
8408
8405
|
//#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
|
|
8457
8406
|
//#region src/plugin/utils/is-react-hook-name.ts
|
|
8458
8407
|
const isReactHookName = (name) => {
|
|
8459
8408
|
if (!name.startsWith("use")) return false;
|
|
@@ -8509,90 +8458,46 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
8509
8458
|
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
8510
8459
|
};
|
|
8511
8460
|
//#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
|
|
8588
8461
|
//#region src/plugin/utils/get-range-start.ts
|
|
8589
8462
|
const getRangeStart = (node) => {
|
|
8590
8463
|
const rangeStart = node.range?.[0];
|
|
8591
8464
|
return typeof rangeStart === "number" ? rangeStart : null;
|
|
8592
8465
|
};
|
|
8593
8466
|
//#endregion
|
|
8594
|
-
//#region src/plugin/utils/is-
|
|
8595
|
-
const
|
|
8467
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
8468
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
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
|
+
};
|
|
8596
8501
|
//#endregion
|
|
8597
8502
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
8598
8503
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
@@ -8627,9 +8532,125 @@ const isCleanupReturningSubscribeLikeCallExpression = (node) => {
|
|
|
8627
8532
|
return true;
|
|
8628
8533
|
};
|
|
8629
8534
|
//#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
|
|
8630
8652
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
8631
8653
|
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
8632
|
-
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
8633
8654
|
const RESOURCE_NOUN_BY_KIND = {
|
|
8634
8655
|
subscribe: "subscription",
|
|
8635
8656
|
timer: "timer",
|
|
@@ -8640,66 +8661,7 @@ const isSubscribeOrObserveCall = (node) => {
|
|
|
8640
8661
|
if (isSubscribeLikeCallExpression(node)) return true;
|
|
8641
8662
|
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
8642
8663
|
};
|
|
8643
|
-
const
|
|
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) => {
|
|
8664
|
+
const findSubscribeLikeUsages = (callback) => {
|
|
8703
8665
|
const usages = [];
|
|
8704
8666
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8705
8667
|
let cleanupArgument = null;
|
|
@@ -8708,22 +8670,13 @@ const findSubscribeLikeUsages = (callback, context) => {
|
|
|
8708
8670
|
const lastCallbackStatement = callbackStatements[callbackStatements.length - 1];
|
|
8709
8671
|
if (isNodeOfType(lastCallbackStatement, "ReturnStatement") && lastCallbackStatement.argument) cleanupArgument = lastCallbackStatement.argument;
|
|
8710
8672
|
}
|
|
8711
|
-
const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
|
|
8712
8673
|
walkAst(callback, (child) => {
|
|
8713
|
-
if (child
|
|
8714
|
-
if (child === cleanupArgument) return false;
|
|
8715
|
-
if (!effectInvokedFunctions.has(child) && !isSynchronousIteratorCallback(child)) return false;
|
|
8716
|
-
}
|
|
8674
|
+
if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
|
|
8717
8675
|
if (isSocketConstruction(child)) {
|
|
8718
8676
|
usages.push({
|
|
8719
8677
|
kind: "socket",
|
|
8720
8678
|
node: child,
|
|
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
|
|
8679
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8727
8680
|
});
|
|
8728
8681
|
return;
|
|
8729
8682
|
}
|
|
@@ -8732,348 +8685,118 @@ const findSubscribeLikeUsages = (callback, context) => {
|
|
|
8732
8685
|
usages.push({
|
|
8733
8686
|
kind: "timer",
|
|
8734
8687
|
node: child,
|
|
8735
|
-
resourceName: child.callee.name
|
|
8736
|
-
handleKey: findAssignedResourceKey(child, context),
|
|
8737
|
-
receiverKey: null,
|
|
8738
|
-
registrationVerbName: child.callee.name,
|
|
8739
|
-
eventKey: null,
|
|
8740
|
-
handlerKey: null
|
|
8688
|
+
resourceName: child.callee.name
|
|
8741
8689
|
});
|
|
8742
8690
|
return;
|
|
8743
8691
|
}
|
|
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
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
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)) usages.push({
|
|
8693
|
+
kind: "subscribe",
|
|
8694
|
+
node: child,
|
|
8695
|
+
resourceName: child.callee.property.name
|
|
8696
|
+
});
|
|
8697
|
+
});
|
|
8698
|
+
return usages;
|
|
8699
|
+
};
|
|
8700
|
+
const collectCleanupBindings = (effectCallback) => {
|
|
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
|
+
}
|
|
8753
8732
|
}
|
|
8754
8733
|
});
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
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) => {
|
|
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);
|
|
8746
|
+
}
|
|
8747
|
+
});
|
|
8748
|
+
walkAst(effectCallback.body, (child) => {
|
|
8749
|
+
if (isNodeOfType(child, "AssignmentExpression") && isNodeOfType(child.left, "Identifier") && bindings.effectScopeVariableNames.has(child.left.name) && isCleanupFunctionLike(child.right, bindings.cleanupFunctionNames, bindings.subscriptionNames)) bindings.cleanupFunctionNames.add(child.left.name);
|
|
8750
|
+
});
|
|
8751
|
+
return bindings;
|
|
8752
|
+
};
|
|
8753
|
+
const removeSynchronouslyReleasedUsages = (callback, usages) => {
|
|
8820
8754
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8821
8755
|
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
8822
|
-
const
|
|
8756
|
+
const releaseStarts = [];
|
|
8823
8757
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8824
|
-
if (!
|
|
8825
|
-
|
|
8758
|
+
if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
|
|
8759
|
+
const releaseStart = getRangeStart(child);
|
|
8760
|
+
if (releaseStart !== null) releaseStarts.push(releaseStart);
|
|
8826
8761
|
});
|
|
8827
|
-
if (
|
|
8762
|
+
if (releaseStarts.length === 0) return usages;
|
|
8828
8763
|
return usages.filter((usage) => {
|
|
8829
8764
|
const usageStart = getRangeStart(usage.node);
|
|
8830
8765
|
if (usageStart === null) return true;
|
|
8831
|
-
|
|
8832
|
-
const releaseStart = getRangeStart(releaseCall);
|
|
8833
|
-
return releaseStart !== null && releaseStart > usageStart && doesReleaseCallMatchUsage(releaseCall, usage, context);
|
|
8834
|
-
});
|
|
8835
|
-
return !doMatchingNodesCoverEveryPathAfterUsage(usage.node, matchingReleaseCalls, context);
|
|
8766
|
+
return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
|
|
8836
8767
|
});
|
|
8837
8768
|
};
|
|
8838
|
-
const
|
|
8839
|
-
if (
|
|
8840
|
-
const
|
|
8841
|
-
if (
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
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
|
-
}
|
|
8769
|
+
const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
|
|
8770
|
+
if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
|
|
8771
|
+
const returnStart = getRangeStart(returnStatement);
|
|
8772
|
+
if (returnStart === null) return true;
|
|
8773
|
+
return usages.some((usage) => {
|
|
8774
|
+
const usageStart = getRangeStart(usage.node);
|
|
8775
|
+
return usageStart === null || usageStart < returnStart;
|
|
9016
8776
|
});
|
|
9017
|
-
return didCleanupFunctionMatch;
|
|
9018
8777
|
};
|
|
9019
|
-
const
|
|
8778
|
+
const effectHasCleanupReturn = (callback, usages) => {
|
|
9020
8779
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
9021
|
-
if (
|
|
9022
|
-
|
|
9023
|
-
|
|
8780
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return isCleanupReturningSubscribeLikeCallExpression(callback.body);
|
|
8781
|
+
const cleanupBindings = collectCleanupBindings(callback);
|
|
8782
|
+
let didFindCleanupReturn = false;
|
|
9024
8783
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8784
|
+
if (didFindCleanupReturn) return;
|
|
9025
8785
|
if (!isNodeOfType(child, "ReturnStatement")) return;
|
|
9026
|
-
|
|
9027
|
-
|
|
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);
|
|
8786
|
+
if (!cleanupReturnRunsAfterUsage(child, usages)) return;
|
|
8787
|
+
if (isCleanupReturn(child.argument, cleanupBindings.cleanupFunctionNames, cleanupBindings.subscriptionNames, { allowOpaqueReturn: true })) didFindCleanupReturn = true;
|
|
9048
8788
|
});
|
|
9049
|
-
return
|
|
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));
|
|
8789
|
+
return didFindCleanupReturn;
|
|
9068
8790
|
};
|
|
9069
|
-
const
|
|
8791
|
+
const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
|
|
8792
|
+
const isSelfReleasingListenerOptionProperty = (property) => {
|
|
9070
8793
|
if (!isNodeOfType(property, "Property")) return false;
|
|
9071
8794
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
|
|
9072
|
-
if (keyName === "signal") return
|
|
8795
|
+
if (keyName === "signal") return true;
|
|
9073
8796
|
if (keyName !== "once") return false;
|
|
9074
8797
|
return isNodeOfType(property.value, "Literal") && property.value.value === true;
|
|
9075
8798
|
};
|
|
9076
|
-
const hasSelfReleasingListenerOptions = (node
|
|
8799
|
+
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
|
|
9077
8800
|
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
9078
8801
|
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
9079
8802
|
["addListener", new Set([
|
|
@@ -9099,185 +8822,85 @@ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
|
9099
8822
|
"teardown"
|
|
9100
8823
|
]);
|
|
9101
8824
|
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8825
|
+
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
9102
8826
|
const getReleaseVerbName = (node) => {
|
|
8827
|
+
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
9103
8828
|
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
9104
8829
|
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
9105
8830
|
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
9106
|
-
if (isNodeOfType(callee, "Identifier")) return
|
|
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
|
-
}
|
|
8831
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8832
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
9111
8833
|
return null;
|
|
9112
8834
|
};
|
|
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
|
-
};
|
|
9153
8835
|
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
9154
|
-
const
|
|
9155
|
-
let
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
const effectCallback = findEnclosingFunction(parentNode);
|
|
9163
|
-
const effectCall = effectCallback?.parent;
|
|
9164
|
-
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isHookCall$2(effectCall, CLEANUP_EFFECT_HOOK_NAMES));
|
|
9165
|
-
};
|
|
9166
|
-
const isPotentiallyReachableFunction = (functionNode, context) => {
|
|
9167
|
-
if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode)) return true;
|
|
9168
|
-
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
9169
|
-
if (!bindingIdentifier) return false;
|
|
9170
|
-
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
9171
|
-
if (!symbol) return false;
|
|
9172
|
-
return symbol.references.some((reference) => findEnclosingFunction(reference.identifier) !== functionNode);
|
|
9173
|
-
};
|
|
9174
|
-
const isReleaseReachableForUsage = (releaseNode, usage, context) => {
|
|
9175
|
-
if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
|
|
9176
|
-
const releaseFunction = findEnclosingFunction(releaseNode);
|
|
9177
|
-
if (!releaseFunction) return true;
|
|
9178
|
-
if (releaseFunction === findEnclosingFunction(usage.node)) return true;
|
|
9179
|
-
return isPotentiallyReachableFunction(releaseFunction, context);
|
|
9180
|
-
};
|
|
9181
|
-
const fileContainsReleaseForUsage = (usage, context) => {
|
|
9182
|
-
let programNode = usage.node;
|
|
9183
|
-
while (programNode.parent) programNode = programNode.parent;
|
|
9184
|
-
let didFindRelease = false;
|
|
9185
|
-
walkAst(programNode, (child) => {
|
|
9186
|
-
if (didFindRelease) return false;
|
|
9187
|
-
if (doesReleaseCallMatchUsage(child, usage, context) && isReleaseReachableForUsage(child, usage, context)) {
|
|
9188
|
-
didFindRelease = true;
|
|
8836
|
+
const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
|
|
8837
|
+
let didFindPairedRelease = false;
|
|
8838
|
+
walkAst(body, (child) => {
|
|
8839
|
+
if (didFindPairedRelease) return false;
|
|
8840
|
+
if (child !== body && isFunctionLike$1(child)) return false;
|
|
8841
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8842
|
+
if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
|
|
8843
|
+
didFindPairedRelease = true;
|
|
9189
8844
|
return false;
|
|
9190
8845
|
}
|
|
9191
8846
|
});
|
|
9192
|
-
return
|
|
8847
|
+
return didFindPairedRelease;
|
|
9193
8848
|
};
|
|
9194
|
-
const
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
const
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
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);
|
|
8849
|
+
const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
|
|
8850
|
+
const collectFileReleaseVerbNames = (anyNode) => {
|
|
8851
|
+
let programNode = anyNode;
|
|
8852
|
+
while (programNode.parent) programNode = programNode.parent;
|
|
8853
|
+
const cached = fileReleaseVerbNamesCache.get(programNode);
|
|
8854
|
+
if (cached) return cached;
|
|
8855
|
+
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
8856
|
+
walkAst(programNode, (child) => {
|
|
8857
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8858
|
+
if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
|
|
8859
|
+
});
|
|
8860
|
+
fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
|
|
8861
|
+
return releaseVerbNames;
|
|
9211
8862
|
};
|
|
9212
|
-
const
|
|
9213
|
-
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
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
|
-
}
|
|
8863
|
+
const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
|
|
8864
|
+
const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
|
|
8865
|
+
const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
|
|
8866
|
+
if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
|
|
8867
|
+
for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
|
|
9225
8868
|
return false;
|
|
9226
8869
|
};
|
|
9227
|
-
const findRetainedFunctionLeak = (retainedFunction
|
|
8870
|
+
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
9228
8871
|
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
9229
8872
|
const body = retainedFunction.body;
|
|
9230
8873
|
if (!body) return null;
|
|
8874
|
+
const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
|
|
9231
8875
|
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);
|
|
9235
8876
|
walkAst(body, (child) => {
|
|
9236
8877
|
if (leak !== null) return false;
|
|
9237
8878
|
if (isFunctionLike$1(child)) return false;
|
|
9238
|
-
if (
|
|
9239
|
-
|
|
8879
|
+
if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
|
|
8880
|
+
if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
|
|
8881
|
+
leak = {
|
|
9240
8882
|
kind: "socket",
|
|
9241
8883
|
node: child,
|
|
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
|
|
8884
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
9248
8885
|
};
|
|
9249
|
-
|
|
9250
|
-
leak = socketUsage;
|
|
9251
|
-
return false;
|
|
9252
|
-
}
|
|
8886
|
+
return false;
|
|
9253
8887
|
}
|
|
9254
8888
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
9255
|
-
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && !
|
|
9256
|
-
|
|
8889
|
+
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
|
|
8890
|
+
leak = {
|
|
9257
8891
|
kind: "timer",
|
|
9258
8892
|
node: child,
|
|
9259
|
-
resourceName: "setInterval"
|
|
9260
|
-
handleKey: findAssignedResourceKey(child, context),
|
|
9261
|
-
receiverKey: null,
|
|
9262
|
-
registrationVerbName: "setInterval",
|
|
9263
|
-
eventKey: null,
|
|
9264
|
-
handlerKey: null
|
|
8893
|
+
resourceName: "setInterval"
|
|
9265
8894
|
};
|
|
9266
|
-
|
|
9267
|
-
leak = timerUsage;
|
|
9268
|
-
return false;
|
|
9269
|
-
}
|
|
8895
|
+
return false;
|
|
9270
8896
|
}
|
|
9271
|
-
if (isSubscribeOrObserveCall(child) &&
|
|
9272
|
-
const
|
|
9273
|
-
|
|
8897
|
+
if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
|
|
8898
|
+
const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
|
|
8899
|
+
if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
|
|
9274
8900
|
kind: "subscribe",
|
|
9275
8901
|
node: child,
|
|
9276
|
-
resourceName:
|
|
9277
|
-
handleKey: findAssignedResourceKey(child, context),
|
|
9278
|
-
...registrationDetails
|
|
8902
|
+
resourceName: registrationVerbName
|
|
9279
8903
|
};
|
|
9280
|
-
if (!hasSelfReleasingListenerOptions(child, context) && !hasReleaseForUsage(subscriptionUsage)) leak = subscriptionUsage;
|
|
9281
8904
|
return false;
|
|
9282
8905
|
}
|
|
9283
8906
|
});
|
|
@@ -9289,26 +8912,6 @@ const isRetainedComponentScopeFunction = (functionNode) => {
|
|
|
9289
8912
|
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
9290
8913
|
return enclosingComponentOrHookName(functionNode) !== null;
|
|
9291
8914
|
};
|
|
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
|
-
};
|
|
9312
8915
|
const effectNeedsCleanup = defineRule({
|
|
9313
8916
|
id: "effect-needs-cleanup",
|
|
9314
8917
|
title: "Effect subscription or timer never cleaned up",
|
|
@@ -9317,8 +8920,7 @@ const effectNeedsCleanup = defineRule({
|
|
|
9317
8920
|
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.",
|
|
9318
8921
|
create: (context) => {
|
|
9319
8922
|
const reportRetainedLeak = (retainedFunction) => {
|
|
9320
|
-
|
|
9321
|
-
const leak = findRetainedFunctionLeak(retainedFunction, context);
|
|
8923
|
+
const leak = findRetainedFunctionLeak(retainedFunction);
|
|
9322
8924
|
if (!leak) return;
|
|
9323
8925
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
9324
8926
|
context.report({
|
|
@@ -9330,31 +8932,30 @@ const effectNeedsCleanup = defineRule({
|
|
|
9330
8932
|
CallExpression(node) {
|
|
9331
8933
|
if (isHookCall$2(node, "useCallback")) {
|
|
9332
8934
|
const retainedCallback = getEffectCallback(node);
|
|
9333
|
-
if (retainedCallback
|
|
8935
|
+
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
9334
8936
|
return;
|
|
9335
8937
|
}
|
|
9336
|
-
if (!isHookCall$2(node,
|
|
8938
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
9337
8939
|
const callback = getEffectCallback(node);
|
|
9338
8940
|
if (!callback) return;
|
|
9339
|
-
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback
|
|
8941
|
+
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
|
|
9340
8942
|
if (usages.length === 0) return;
|
|
9341
|
-
|
|
9342
|
-
|
|
8943
|
+
if (effectHasCleanupReturn(callback, usages)) return;
|
|
8944
|
+
const firstUsage = usages[0];
|
|
9343
8945
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
9344
|
-
const hookName = getCalleeName$2(node) ?? "effect";
|
|
9345
8946
|
context.report({
|
|
9346
8947
|
node,
|
|
9347
|
-
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in
|
|
8948
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
9348
8949
|
});
|
|
9349
8950
|
},
|
|
9350
8951
|
FunctionDeclaration(node) {
|
|
9351
8952
|
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9352
8953
|
},
|
|
9353
8954
|
ArrowFunctionExpression(node) {
|
|
9354
|
-
if (isRetainedComponentScopeFunction(node)
|
|
8955
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9355
8956
|
},
|
|
9356
8957
|
FunctionExpression(node) {
|
|
9357
|
-
if (isRetainedComponentScopeFunction(node)
|
|
8958
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9358
8959
|
}
|
|
9359
8960
|
};
|
|
9360
8961
|
}
|
|
@@ -9968,18 +9569,13 @@ const unwrapExpression$3 = (node) => {
|
|
|
9968
9569
|
return current;
|
|
9969
9570
|
};
|
|
9970
9571
|
/**
|
|
9971
|
-
* Get the hook name from a
|
|
9972
|
-
*
|
|
9572
|
+
* Get the hook name from a call expression's callee, regardless of
|
|
9573
|
+
* whether the hook is called as `useFoo()` (Identifier) or
|
|
9574
|
+
* `React.useFoo()` (MemberExpression).
|
|
9973
9575
|
*/
|
|
9974
|
-
const getHookName = (callee
|
|
9975
|
-
|
|
9976
|
-
if (isNodeOfType(
|
|
9977
|
-
const resolvedSymbol = scopes ? resolveConstIdentifierAlias(strippedCallee, scopes) : null;
|
|
9978
|
-
const importDeclaration = resolvedSymbol?.declarationNode.parent;
|
|
9979
|
-
if (resolvedSymbol?.kind === "import" && importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "react") return getImportedName(resolvedSymbol.declarationNode) ?? strippedCallee.name;
|
|
9980
|
-
return strippedCallee.name;
|
|
9981
|
-
}
|
|
9982
|
-
if (isNodeOfType(strippedCallee, "MemberExpression") && !strippedCallee.computed && isNodeOfType(strippedCallee.property, "Identifier")) return strippedCallee.property.name;
|
|
9576
|
+
const getHookName = (callee) => {
|
|
9577
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
9578
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
9983
9579
|
return null;
|
|
9984
9580
|
};
|
|
9985
9581
|
const FUNCTION_SCOPE_KINDS = new Set([
|
|
@@ -10164,7 +9760,7 @@ const STABLE_IDENTITY_WRAPPER_HOOK_NAMES = new Set([
|
|
|
10164
9760
|
* - primitive-literal local consts (the value never changes
|
|
10165
9761
|
* between renders unless the literal does)
|
|
10166
9762
|
*/
|
|
10167
|
-
const symbolHasStableHookOrigin = (symbol
|
|
9763
|
+
const symbolHasStableHookOrigin = (symbol) => {
|
|
10168
9764
|
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
10169
9765
|
let declarator = symbol.declarationNode;
|
|
10170
9766
|
while (declarator && declarator.type !== "VariableDeclarator") declarator = declarator.parent ?? null;
|
|
@@ -10177,7 +9773,7 @@ const symbolHasStableHookOrigin = (symbol, scopes) => {
|
|
|
10177
9773
|
if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
|
|
10178
9774
|
}
|
|
10179
9775
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
10180
|
-
const initializerHookName = getHookName(initializer.callee
|
|
9776
|
+
const initializerHookName = getHookName(initializer.callee);
|
|
10181
9777
|
if (!initializerHookName) return false;
|
|
10182
9778
|
if (initializerHookName === "useRef") return true;
|
|
10183
9779
|
if (initializerHookName === "useEffectEvent") return true;
|
|
@@ -10212,7 +9808,7 @@ const symbolHasStableMemoizedOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
10212
9808
|
if (!declarator.init) return false;
|
|
10213
9809
|
const initializer = unwrapExpression$3(declarator.init);
|
|
10214
9810
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
10215
|
-
const initializerHookName = getHookName(initializer.callee
|
|
9811
|
+
const initializerHookName = getHookName(initializer.callee);
|
|
10216
9812
|
if (!initializerHookName || !MEMOIZING_HOOK_NAMES.has(initializerHookName)) return false;
|
|
10217
9813
|
const depsArgument = initializer.arguments[1];
|
|
10218
9814
|
if (!depsArgument || !isAstNode(depsArgument)) return false;
|
|
@@ -10242,7 +9838,7 @@ const getObjectPropertyValue = (objectExpression, propertyName) => {
|
|
|
10242
9838
|
}
|
|
10243
9839
|
return null;
|
|
10244
9840
|
};
|
|
10245
|
-
const isStableRefContainerCapture = (symbol, depKey
|
|
9841
|
+
const isStableRefContainerCapture = (symbol, depKey) => {
|
|
10246
9842
|
if (symbol.kind !== "const") return false;
|
|
10247
9843
|
if (!depKey.startsWith(`${symbol.name}.`)) return false;
|
|
10248
9844
|
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
@@ -10256,7 +9852,7 @@ const isStableRefContainerCapture = (symbol, depKey, scopes) => {
|
|
|
10256
9852
|
if (!propertyValue) return false;
|
|
10257
9853
|
currentValue = propertyValue;
|
|
10258
9854
|
}
|
|
10259
|
-
return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee
|
|
9855
|
+
return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee) === "useRef";
|
|
10260
9856
|
};
|
|
10261
9857
|
const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
10262
9858
|
if (visitedSymbolIds.has(symbol.id)) return true;
|
|
@@ -10274,13 +9870,7 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
10274
9870
|
}
|
|
10275
9871
|
return true;
|
|
10276
9872
|
};
|
|
10277
|
-
const
|
|
10278
|
-
if (symbol.kind !== "const") return false;
|
|
10279
|
-
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
10280
|
-
const resolvedSymbol = resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes);
|
|
10281
|
-
return resolvedSymbol !== null && resolvedSymbol !== symbol && resolvedSymbol.kind === "import";
|
|
10282
|
-
};
|
|
10283
|
-
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol, scopes) || symbolHasStableImportedAlias(symbol, scopes) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
9873
|
+
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
10284
9874
|
//#endregion
|
|
10285
9875
|
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
10286
9876
|
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
@@ -10468,18 +10058,10 @@ const collectCaptureDepKeys = (callback, scopes) => {
|
|
|
10468
10058
|
}
|
|
10469
10059
|
const depKey = computeDepKey(reference);
|
|
10470
10060
|
if (!depKey) continue;
|
|
10471
|
-
if (isStableRefContainerCapture(symbol, depKey
|
|
10061
|
+
if (isStableRefContainerCapture(symbol, depKey)) {
|
|
10472
10062
|
stableCapturedNames.add(depKey);
|
|
10473
10063
|
continue;
|
|
10474
10064
|
}
|
|
10475
|
-
if (depKey === symbol.name) {
|
|
10476
|
-
const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
10477
|
-
if (identitySourceKeys) {
|
|
10478
|
-
if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
|
|
10479
|
-
for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
|
|
10480
|
-
continue;
|
|
10481
|
-
}
|
|
10482
|
-
}
|
|
10483
10065
|
keys.add(depKey);
|
|
10484
10066
|
}
|
|
10485
10067
|
return {
|
|
@@ -10508,60 +10090,12 @@ const hasComputedMemberExpression = (node) => {
|
|
|
10508
10090
|
if (stripped.computed) return true;
|
|
10509
10091
|
return hasComputedMemberExpression(stripped.object);
|
|
10510
10092
|
};
|
|
10511
|
-
const mergeIdentitySourceKeys = (expressions, scopes, visitedSymbolIds) => {
|
|
10512
|
-
const identitySourceKeys = /* @__PURE__ */ new Set();
|
|
10513
|
-
for (const expression of expressions) {
|
|
10514
|
-
const expressionSourceKeys = resolveIdentitySourceKeysFromExpression(expression, scopes, visitedSymbolIds);
|
|
10515
|
-
if (!expressionSourceKeys) return null;
|
|
10516
|
-
for (const expressionSourceKey of expressionSourceKeys) identitySourceKeys.add(expressionSourceKey);
|
|
10517
|
-
}
|
|
10518
|
-
return identitySourceKeys;
|
|
10519
|
-
};
|
|
10520
|
-
const resolveIdentitySourceKeysFromExpression = (expression, scopes, visitedSymbolIds) => {
|
|
10521
|
-
const stripped = unwrapExpression$3(expression);
|
|
10522
|
-
if (isNodeOfType(stripped, "Literal") && (stripped.value === null || typeof stripped.value === "string" || typeof stripped.value === "number" || typeof stripped.value === "boolean") || isNodeOfType(stripped, "TemplateLiteral") && getStaticTemplateLiteralValue(stripped) !== null) return /* @__PURE__ */ new Set();
|
|
10523
|
-
if (isNodeOfType(stripped, "Identifier")) {
|
|
10524
|
-
const sourceSymbol = scopes.symbolFor(stripped);
|
|
10525
|
-
if (!sourceSymbol) return null;
|
|
10526
|
-
if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
10527
|
-
if (sourceSymbol.kind === "const" && sourceSymbol.initializer && isNodeOfType(sourceSymbol.declarationNode, "VariableDeclarator") && sourceSymbol.declarationNode.id === sourceSymbol.bindingIdentifier && sourceSymbol.references.every((reference) => reference.flag === "read")) {
|
|
10528
|
-
if (visitedSymbolIds.has(sourceSymbol.id)) return null;
|
|
10529
|
-
visitedSymbolIds.add(sourceSymbol.id);
|
|
10530
|
-
const sourceKeys = resolveIdentitySourceKeysFromExpression(sourceSymbol.initializer, scopes, visitedSymbolIds);
|
|
10531
|
-
visitedSymbolIds.delete(sourceSymbol.id);
|
|
10532
|
-
if (sourceKeys) return sourceKeys;
|
|
10533
|
-
}
|
|
10534
|
-
return new Set([sourceSymbol.name]);
|
|
10535
|
-
}
|
|
10536
|
-
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
10537
|
-
if (hasComputedMemberExpression(stripped)) return null;
|
|
10538
|
-
const sourceKey = stringifyMemberChain(stripped);
|
|
10539
|
-
const rootIdentifier = getMemberRootIdentifier(stripped);
|
|
10540
|
-
const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
10541
|
-
if (!sourceKey || !rootSymbol) return null;
|
|
10542
|
-
if (isOutsideAllFunctions(rootSymbol)) return /* @__PURE__ */ new Set();
|
|
10543
|
-
if (isStableRefContainerCapture(rootSymbol, sourceKey, scopes)) return /* @__PURE__ */ new Set();
|
|
10544
|
-
if (symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
10545
|
-
return new Set([sourceKey]);
|
|
10546
|
-
}
|
|
10547
|
-
if (isNodeOfType(stripped, "LogicalExpression")) return mergeIdentitySourceKeys([stripped.left, stripped.right], scopes, visitedSymbolIds);
|
|
10548
|
-
if (isNodeOfType(stripped, "ConditionalExpression")) return mergeIdentitySourceKeys([
|
|
10549
|
-
stripped.test,
|
|
10550
|
-
stripped.consequent,
|
|
10551
|
-
stripped.alternate
|
|
10552
|
-
], scopes, visitedSymbolIds);
|
|
10553
|
-
return null;
|
|
10554
|
-
};
|
|
10555
|
-
const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
|
|
10556
|
-
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
|
|
10557
|
-
return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
|
|
10558
|
-
};
|
|
10559
10093
|
const isUseCallbackResultDep = (node, scopes) => {
|
|
10560
10094
|
const rootSymbol = getRootSymbol(node, scopes);
|
|
10561
10095
|
const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
|
|
10562
|
-
return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee
|
|
10096
|
+
return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee) === "useCallback");
|
|
10563
10097
|
};
|
|
10564
|
-
const
|
|
10098
|
+
const isExtraEffectDepAllowed = (node, scopes) => {
|
|
10565
10099
|
const rootIdentifier = getMemberRootIdentifier(node);
|
|
10566
10100
|
if (!rootIdentifier) return false;
|
|
10567
10101
|
const symbol = scopes.symbolFor(rootIdentifier);
|
|
@@ -10589,17 +10123,10 @@ const isUnstableInitializer = (node) => {
|
|
|
10589
10123
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
10590
10124
|
return isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "ArrayExpression") || isNodeOfType(stripped, "ClassExpression") || isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment") || isNodeOfType(stripped, "AssignmentExpression") || isNodeOfType(stripped, "NewExpression");
|
|
10591
10125
|
};
|
|
10592
|
-
const isExtraDepAllowedForHook = (hookName, node, scopes) => {
|
|
10593
|
-
if (!isExtraReactiveDepAllowed(node, scopes)) return false;
|
|
10594
|
-
if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) return true;
|
|
10595
|
-
if (hookName !== "useMemo") return false;
|
|
10596
|
-
const rootSymbol = getRootSymbol(node, scopes);
|
|
10597
|
-
return Boolean(rootSymbol && !symbolHasStableValue(rootSymbol, scopes) && !isUnstableInitializer(rootSymbol.initializer));
|
|
10598
|
-
};
|
|
10599
10126
|
const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
|
|
10600
10127
|
const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
|
|
10601
|
-
const isStableSetterLikeSymbol = (symbol
|
|
10602
|
-
if (!symbolHasStableHookOrigin(symbol
|
|
10128
|
+
const isStableSetterLikeSymbol = (symbol) => {
|
|
10129
|
+
if (!symbolHasStableHookOrigin(symbol)) return false;
|
|
10603
10130
|
return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
|
|
10604
10131
|
};
|
|
10605
10132
|
const findStableSetterReference = (node, scopes) => {
|
|
@@ -10609,7 +10136,7 @@ const findStableSetterReference = (node, scopes) => {
|
|
|
10609
10136
|
if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
|
|
10610
10137
|
if (isNodeOfType(current, "Identifier")) {
|
|
10611
10138
|
const symbol = scopes.referenceFor(current)?.resolvedSymbol;
|
|
10612
|
-
if (symbol && isStableSetterLikeSymbol(symbol
|
|
10139
|
+
if (symbol && isStableSetterLikeSymbol(symbol)) {
|
|
10613
10140
|
setterName = symbol.name;
|
|
10614
10141
|
return;
|
|
10615
10142
|
}
|
|
@@ -10725,11 +10252,11 @@ const hasRefCurrentAssignmentInComponent = (refSymbol) => {
|
|
|
10725
10252
|
}
|
|
10726
10253
|
return false;
|
|
10727
10254
|
};
|
|
10728
|
-
const isSeededDataRefSymbol = (refSymbol
|
|
10255
|
+
const isSeededDataRefSymbol = (refSymbol) => {
|
|
10729
10256
|
if (!refSymbol) return false;
|
|
10730
10257
|
const initializer = refSymbol.initializer ? unwrapExpression$3(refSymbol.initializer) : null;
|
|
10731
10258
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
10732
|
-
if (getHookName(initializer.callee
|
|
10259
|
+
if (getHookName(initializer.callee) !== "useRef") return false;
|
|
10733
10260
|
const firstArgument = initializer.arguments[0];
|
|
10734
10261
|
if (!firstArgument || !isAstNode(firstArgument)) return false;
|
|
10735
10262
|
const strippedArgument = unwrapExpression$3(firstArgument);
|
|
@@ -10769,9 +10296,7 @@ const isOuterFunctionScopeDep = (node, callback, scopes) => {
|
|
|
10769
10296
|
const componentOrHookScope = componentOrHookFunction ? scopes.ownScopeFor(componentOrHookFunction) : null;
|
|
10770
10297
|
return Boolean(componentOrHookScope && !isDescendantScope(symbol.scope, componentOrHookScope));
|
|
10771
10298
|
};
|
|
10772
|
-
const hasMemberCallForRoot = (node, rootName
|
|
10773
|
-
const rootSymbol = closureCaptures(node, scopes).find((reference) => reference.resolvedSymbol?.name === rootName)?.resolvedSymbol ?? null;
|
|
10774
|
-
if (!rootSymbol) return false;
|
|
10299
|
+
const hasMemberCallForRoot = (node, rootName) => {
|
|
10775
10300
|
let didFindMemberCall = false;
|
|
10776
10301
|
const visit = (current) => {
|
|
10777
10302
|
if (didFindMemberCall) return;
|
|
@@ -10787,7 +10312,7 @@ const hasMemberCallForRoot = (node, rootName, scopes) => {
|
|
|
10787
10312
|
}
|
|
10788
10313
|
chainObject = unwrapExpression$3(chainObject.object);
|
|
10789
10314
|
}
|
|
10790
|
-
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName
|
|
10315
|
+
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
|
|
10791
10316
|
didFindMemberCall = true;
|
|
10792
10317
|
return;
|
|
10793
10318
|
}
|
|
@@ -10805,11 +10330,9 @@ const hasMemberCallForRoot = (node, rootName, scopes) => {
|
|
|
10805
10330
|
visit(node);
|
|
10806
10331
|
return didFindMemberCall;
|
|
10807
10332
|
};
|
|
10808
|
-
const addAggregatePropsDependency = (captureKeys, declaredKeys, callback
|
|
10809
|
-
|
|
10810
|
-
if (
|
|
10811
|
-
if (propsCaptureKeys.every((captureKey) => [...declaredKeys].some((declaredKey) => isMatchingDepOrPrefix(declaredKey, captureKey)))) return;
|
|
10812
|
-
if (hasMemberCallForRoot(callback, "props", scopes)) captureKeys.add("props");
|
|
10333
|
+
const addAggregatePropsDependency = (captureKeys, declaredKeys, callback) => {
|
|
10334
|
+
if ([...captureKeys].filter((captureKey) => captureKey.startsWith("props.")).length < 2 || declaredKeys.has("props")) return;
|
|
10335
|
+
if (hasMemberCallForRoot(callback, "props")) captureKeys.add("props");
|
|
10813
10336
|
};
|
|
10814
10337
|
const exhaustiveDeps = defineRule({
|
|
10815
10338
|
id: "exhaustive-deps",
|
|
@@ -10864,7 +10387,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10864
10387
|
};
|
|
10865
10388
|
const isAutoDependenciesHook = (hookName) => settings.experimental_autoDependenciesHooks.includes(hookName);
|
|
10866
10389
|
return { CallExpression(node) {
|
|
10867
|
-
const hookName = getHookName(node.callee
|
|
10390
|
+
const hookName = getHookName(node.callee);
|
|
10868
10391
|
if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
|
|
10869
10392
|
const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
|
|
10870
10393
|
const depsArgumentIndex = getDepsArgumentIndex(hookName);
|
|
@@ -10921,7 +10444,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10921
10444
|
if (outerAssignments.length > 0) return;
|
|
10922
10445
|
const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
|
|
10923
10446
|
const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
|
|
10924
|
-
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol
|
|
10447
|
+
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol)) context.report({
|
|
10925
10448
|
node: callbackToAnalyze,
|
|
10926
10449
|
message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
|
|
10927
10450
|
});
|
|
@@ -11020,7 +10543,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
11020
10543
|
const fullChain = stringifyMemberChain(stripped);
|
|
11021
10544
|
if (fullChain && fullChain.endsWith(".current") && isNodeOfType(stripped, "MemberExpression") && isNodeOfType(stripped.object, "Identifier")) {
|
|
11022
10545
|
const refSymbol = context.scopes.symbolFor(stripped.object);
|
|
11023
|
-
if (refSymbol && symbolHasStableHookOrigin(refSymbol
|
|
10546
|
+
if (refSymbol && symbolHasStableHookOrigin(refSymbol)) {
|
|
11024
10547
|
if (!didReportRefCurrentDep) {
|
|
11025
10548
|
context.report({
|
|
11026
10549
|
node: elementNode,
|
|
@@ -11058,7 +10581,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
11058
10581
|
declaredKeys.add(key);
|
|
11059
10582
|
declaredKeyToReportNode.set(key, elementNode);
|
|
11060
10583
|
}
|
|
11061
|
-
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument
|
|
10584
|
+
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument);
|
|
11062
10585
|
const missingCaptureKeys = [];
|
|
11063
10586
|
for (const captureKey of captureKeys) {
|
|
11064
10587
|
let isCoveredByDeclared = false;
|
|
@@ -11144,7 +10667,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
11144
10667
|
if (stableCapturedNames.has(rootName) || stableCapturedNames.has(declaredKey)) continue;
|
|
11145
10668
|
if (outerFunctionCapturedNames.has(rootName)) continue;
|
|
11146
10669
|
const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
|
|
11147
|
-
if (
|
|
10670
|
+
if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) && isExtraEffectDepAllowed(reportNode, context.scopes)) continue;
|
|
11148
10671
|
if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
|
|
11149
10672
|
const rootSymbol = getRootSymbol(reportNode, context.scopes);
|
|
11150
10673
|
if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
|
|
@@ -12816,6 +12339,20 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
12816
12339
|
return names;
|
|
12817
12340
|
};
|
|
12818
12341
|
//#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
|
|
12819
12356
|
//#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
|
|
12820
12357
|
const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
|
|
12821
12358
|
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
@@ -13960,7 +13497,7 @@ const jsHoistIntl = defineRule({
|
|
|
13960
13497
|
});
|
|
13961
13498
|
//#endregion
|
|
13962
13499
|
//#region src/plugin/utils/create-loop-aware-visitors.ts
|
|
13963
|
-
const ITERATOR_CALLBACK_METHOD_NAMES
|
|
13500
|
+
const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
13964
13501
|
"map",
|
|
13965
13502
|
"flatMap",
|
|
13966
13503
|
"forEach",
|
|
@@ -13979,7 +13516,7 @@ const isIteratorCallback = (node) => {
|
|
|
13979
13516
|
const parent = node.parent;
|
|
13980
13517
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
13981
13518
|
if (!parent.arguments.includes(node)) return false;
|
|
13982
|
-
return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES
|
|
13519
|
+
return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(parent.callee.property.name);
|
|
13983
13520
|
};
|
|
13984
13521
|
const createLoopAwareVisitors = (innerVisitors, options = {}) => {
|
|
13985
13522
|
let loopDepth = 0;
|
|
@@ -14208,7 +13745,7 @@ const findIndexedArrayObject = (callbackBody, indexParameterName) => {
|
|
|
14208
13745
|
});
|
|
14209
13746
|
return indexedArrayObject;
|
|
14210
13747
|
};
|
|
14211
|
-
const unwrapChainExpression$
|
|
13748
|
+
const unwrapChainExpression$2 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
14212
13749
|
const LENGTH_EQUALITY_OPERATORS = new Set(["===", "=="]);
|
|
14213
13750
|
const LENGTH_MISMATCH_OPERATORS = new Set([
|
|
14214
13751
|
"!==",
|
|
@@ -14220,17 +13757,17 @@ const LENGTH_MISMATCH_OPERATORS = new Set([
|
|
|
14220
13757
|
]);
|
|
14221
13758
|
const LENGTH_ANY_COMPARISON_OPERATORS = new Set([...LENGTH_EQUALITY_OPERATORS, ...LENGTH_MISMATCH_OPERATORS]);
|
|
14222
13759
|
const isLengthComparison = (candidate, receiverArray, indexedArray, operators) => {
|
|
14223
|
-
const binaryGuard = unwrapChainExpression$
|
|
13760
|
+
const binaryGuard = unwrapChainExpression$2(candidate);
|
|
14224
13761
|
if (!isNodeOfType(binaryGuard, "BinaryExpression")) return false;
|
|
14225
13762
|
if (!operators.has(binaryGuard.operator)) return false;
|
|
14226
|
-
const leftSide = unwrapChainExpression$
|
|
14227
|
-
const rightSide = unwrapChainExpression$
|
|
13763
|
+
const leftSide = unwrapChainExpression$2(binaryGuard.left);
|
|
13764
|
+
const rightSide = unwrapChainExpression$2(binaryGuard.right);
|
|
14228
13765
|
if (!isMemberProperty(leftSide, "length")) return false;
|
|
14229
13766
|
if (!isMemberProperty(rightSide, "length")) return false;
|
|
14230
|
-
const leftLengthObject = unwrapChainExpression$
|
|
14231
|
-
const rightLengthObject = unwrapChainExpression$
|
|
14232
|
-
const normalizedReceiver = unwrapChainExpression$
|
|
14233
|
-
const normalizedIndexed = unwrapChainExpression$
|
|
13767
|
+
const leftLengthObject = unwrapChainExpression$2(leftSide.object);
|
|
13768
|
+
const rightLengthObject = unwrapChainExpression$2(rightSide.object);
|
|
13769
|
+
const normalizedReceiver = unwrapChainExpression$2(receiverArray);
|
|
13770
|
+
const normalizedIndexed = unwrapChainExpression$2(indexedArray);
|
|
14234
13771
|
const matchesReceiverThenIndexed = areExpressionsStructurallyEqual(leftLengthObject, normalizedReceiver) && areExpressionsStructurallyEqual(rightLengthObject, normalizedIndexed);
|
|
14235
13772
|
const matchesIndexedThenReceiver = areExpressionsStructurallyEqual(leftLengthObject, normalizedIndexed) && areExpressionsStructurallyEqual(rightLengthObject, normalizedReceiver);
|
|
14236
13773
|
return matchesReceiverThenIndexed || matchesIndexedThenReceiver;
|
|
@@ -14317,18 +13854,18 @@ const LENGTH_PRESERVING_METHOD_NAMES = new Set([
|
|
|
14317
13854
|
"toReversed"
|
|
14318
13855
|
]);
|
|
14319
13856
|
const peelLengthPreservingDerivation = (expression) => {
|
|
14320
|
-
let current = unwrapChainExpression$
|
|
13857
|
+
let current = unwrapChainExpression$2(expression);
|
|
14321
13858
|
for (;;) {
|
|
14322
13859
|
if (isNodeOfType(current, "ArrayExpression") && current.elements?.length === 1 && isNodeOfType(current.elements[0], "SpreadElement")) {
|
|
14323
|
-
current = unwrapChainExpression$
|
|
13860
|
+
current = unwrapChainExpression$2(current.elements[0].argument);
|
|
14324
13861
|
continue;
|
|
14325
13862
|
}
|
|
14326
13863
|
if (isNodeOfType(current, "CallExpression")) {
|
|
14327
|
-
const callee = unwrapChainExpression$
|
|
13864
|
+
const callee = unwrapChainExpression$2(current.callee);
|
|
14328
13865
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
14329
|
-
const calleeObject = unwrapChainExpression$
|
|
13866
|
+
const calleeObject = unwrapChainExpression$2(callee.object);
|
|
14330
13867
|
if (isNodeOfType(calleeObject, "Identifier") && calleeObject.name === "Array" && callee.property.name === "from" && current.arguments?.length === 1) {
|
|
14331
|
-
current = unwrapChainExpression$
|
|
13868
|
+
current = unwrapChainExpression$2(current.arguments[0]);
|
|
14332
13869
|
continue;
|
|
14333
13870
|
}
|
|
14334
13871
|
if (LENGTH_PRESERVING_METHOD_NAMES.has(callee.property.name)) {
|
|
@@ -14373,7 +13910,7 @@ const resolveComparedArraySource = (expression, scopeNode) => {
|
|
|
14373
13910
|
const initializer = findConstInitializer(current.name, scopeNode);
|
|
14374
13911
|
if (!initializer) return current;
|
|
14375
13912
|
const peeledInitializer = peelLengthPreservingDerivation(initializer);
|
|
14376
|
-
if (peeledInitializer === unwrapChainExpression$
|
|
13913
|
+
if (peeledInitializer === unwrapChainExpression$2(initializer)) return current;
|
|
14377
13914
|
current = peeledInitializer;
|
|
14378
13915
|
}
|
|
14379
13916
|
return current;
|
|
@@ -20103,6 +19640,54 @@ const nextjsNoAElement = defineRule({
|
|
|
20103
19640
|
} })
|
|
20104
19641
|
});
|
|
20105
19642
|
//#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
|
|
20106
19691
|
//#region src/plugin/utils/contains-fetch-call.ts
|
|
20107
19692
|
const isFetchCall$1 = (node) => {
|
|
20108
19693
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
@@ -21086,7 +20671,6 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
21086
20671
|
continue;
|
|
21087
20672
|
}
|
|
21088
20673
|
if (isNodeOfType(statement, "ExportNamedDeclaration")) {
|
|
21089
|
-
if (statement.exportKind === "type") continue;
|
|
21090
20674
|
const declaration = statement.declaration;
|
|
21091
20675
|
if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
|
|
21092
20676
|
recordVariableDeclaration(declaration);
|
|
@@ -21101,7 +20685,6 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
21101
20685
|
}
|
|
21102
20686
|
for (const specifier of statement.specifiers ?? []) {
|
|
21103
20687
|
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
21104
|
-
if (specifier.exportKind === "type") continue;
|
|
21105
20688
|
const local = specifier.local;
|
|
21106
20689
|
const exported = specifier.exported;
|
|
21107
20690
|
if (!isNodeOfType(local, "Identifier")) continue;
|
|
@@ -21150,37 +20733,25 @@ const resolveImportedExportName = (importSpecifier) => {
|
|
|
21150
20733
|
if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
|
|
21151
20734
|
return null;
|
|
21152
20735
|
};
|
|
21153
|
-
const
|
|
20736
|
+
const findReExportSourcesForName = (programRoot, exportedName) => {
|
|
21154
20737
|
if (!isNodeOfType(programRoot, "Program")) return [];
|
|
21155
|
-
const
|
|
20738
|
+
const exportAllSources = [];
|
|
21156
20739
|
for (const statement of programRoot.body ?? []) {
|
|
21157
20740
|
if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
|
|
21158
|
-
if (statement.exportKind === "type") continue;
|
|
21159
20741
|
const sourceValue = statement.source.value;
|
|
21160
20742
|
if (typeof sourceValue !== "string") continue;
|
|
21161
20743
|
for (const specifier of statement.specifiers ?? []) {
|
|
21162
20744
|
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
21163
|
-
if (specifier.exportKind === "type") continue;
|
|
21164
20745
|
const exported = specifier.exported;
|
|
21165
|
-
if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null)
|
|
21166
|
-
const local = specifier.local;
|
|
21167
|
-
const importedName = isNodeOfType(local, "Identifier") ? local.name : isNodeOfType(local, "Literal") && typeof local.value === "string" ? local.value : null;
|
|
21168
|
-
if (importedName) return [{
|
|
21169
|
-
importedName,
|
|
21170
|
-
source: sourceValue
|
|
21171
|
-
}];
|
|
20746
|
+
if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
|
|
21172
20747
|
}
|
|
21173
20748
|
}
|
|
21174
20749
|
if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
|
|
21175
|
-
if (statement.exportKind === "type" || statement.exported) continue;
|
|
21176
20750
|
const sourceValue = statement.source.value;
|
|
21177
|
-
if (typeof sourceValue === "string")
|
|
21178
|
-
importedName: exportedName,
|
|
21179
|
-
source: sourceValue
|
|
21180
|
-
});
|
|
20751
|
+
if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
|
|
21181
20752
|
}
|
|
21182
20753
|
}
|
|
21183
|
-
return
|
|
20754
|
+
return exportAllSources;
|
|
21184
20755
|
};
|
|
21185
20756
|
//#endregion
|
|
21186
20757
|
//#region src/plugin/utils/attach-parent-references.ts
|
|
@@ -21269,6 +20840,139 @@ const parseSourceFile = (absoluteFilePath) => {
|
|
|
21269
20840
|
return parsedProgram;
|
|
21270
20841
|
};
|
|
21271
20842
|
//#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
|
|
21272
20976
|
//#region src/plugin/utils/resolve-relative-import-path.ts
|
|
21273
20977
|
const MODULE_FILE_EXTENSIONS = [
|
|
21274
20978
|
".ts",
|
|
@@ -21385,6 +21089,35 @@ const resolveModuleFileFromAbsolutePath = (importPath) => {
|
|
|
21385
21089
|
};
|
|
21386
21090
|
const resolveRelativeImportPath = (filename, source) => resolveModuleFileFromAbsolutePath(path.resolve(path.dirname(filename), source));
|
|
21387
21091
|
//#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
|
|
21388
21121
|
//#region src/plugin/utils/resolve-tsconfig-alias.ts
|
|
21389
21122
|
const TSCONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
|
|
21390
21123
|
const isObjectRecord = (value) => typeof value === "object" && value !== null;
|
|
@@ -21563,29 +21296,25 @@ const resolveTsconfigAliasPath = (fromFilename, source) => {
|
|
|
21563
21296
|
};
|
|
21564
21297
|
//#endregion
|
|
21565
21298
|
//#region src/plugin/utils/resolve-module-path.ts
|
|
21566
|
-
const resolveModulePath = (fromFilename, source) =>
|
|
21567
|
-
if (path.isAbsolute(source)) return null;
|
|
21568
|
-
return source.startsWith(".") ? resolveRelativeImportPath(fromFilename, source) : resolveTsconfigAliasPath(fromFilename, source);
|
|
21569
|
-
};
|
|
21299
|
+
const resolveModulePath = (fromFilename, source) => resolveRelativeImportPath(fromFilename, source) ?? resolveTsconfigAliasPath(fromFilename, source);
|
|
21570
21300
|
//#endregion
|
|
21571
21301
|
//#region src/plugin/utils/resolve-cross-file-function-export.ts
|
|
21572
21302
|
const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
|
|
21573
21303
|
if (visitedFilePaths.size >= 4) return null;
|
|
21574
21304
|
if (visitedFilePaths.has(filePath)) return null;
|
|
21575
21305
|
visitedFilePaths.add(filePath);
|
|
21576
|
-
const
|
|
21306
|
+
const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
|
|
21307
|
+
const programRoot = parseSourceFile(actualFilePath);
|
|
21577
21308
|
if (!programRoot) return null;
|
|
21578
21309
|
const exported = findExportedFunctionBody(programRoot, exportedName);
|
|
21579
21310
|
if (exported) return exported;
|
|
21580
|
-
const
|
|
21581
|
-
|
|
21582
|
-
const nextFilePath = resolveModulePath(filePath, target.source);
|
|
21311
|
+
for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
|
|
21312
|
+
const nextFilePath = resolveModulePath(actualFilePath, reExportSource);
|
|
21583
21313
|
if (!nextFilePath) continue;
|
|
21584
|
-
const resolved = resolveFunctionExportInFile(nextFilePath,
|
|
21585
|
-
if (resolved)
|
|
21314
|
+
const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
|
|
21315
|
+
if (resolved) return resolved;
|
|
21586
21316
|
}
|
|
21587
|
-
|
|
21588
|
-
return resolvedCandidates.values().next().value ?? null;
|
|
21317
|
+
return null;
|
|
21589
21318
|
};
|
|
21590
21319
|
const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
|
|
21591
21320
|
const resolvedFilePath = resolveModulePath(fromFilename, source);
|
|
@@ -22370,26 +22099,6 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
22370
22099
|
const importDeclaration = declarationNode.parent;
|
|
22371
22100
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22372
22101
|
}));
|
|
22373
|
-
const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.some((def) => {
|
|
22374
|
-
if (def.type !== "ImportBinding") return false;
|
|
22375
|
-
const declarationNode = def.node;
|
|
22376
|
-
if (!isNodeOfType(declarationNode, "ImportNamespaceSpecifier") && !isNodeOfType(declarationNode, "ImportDefaultSpecifier")) return false;
|
|
22377
|
-
const importDeclaration = declarationNode.parent;
|
|
22378
|
-
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22379
|
-
}));
|
|
22380
|
-
const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
22381
|
-
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
|
|
22382
|
-
const callee = stripParenExpression(declarator.init.callee);
|
|
22383
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
22384
|
-
const reference = getRef(analysis, callee);
|
|
22385
|
-
if (!reference?.resolved) return callee.name === hookName;
|
|
22386
|
-
return isReactNamedImportReference(reference, hookName);
|
|
22387
|
-
}
|
|
22388
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
|
|
22389
|
-
const namespaceReference = getRef(analysis, callee.object);
|
|
22390
|
-
if (!namespaceReference?.resolved) return callee.object.name === "React";
|
|
22391
|
-
return isReactNamespaceImportReference(namespaceReference);
|
|
22392
|
-
};
|
|
22393
22102
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
22394
22103
|
if (!node) return false;
|
|
22395
22104
|
if (isNodeOfType(node, "Identifier")) {
|
|
@@ -22788,46 +22497,6 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
|
|
|
22788
22497
|
"parseInt"
|
|
22789
22498
|
]);
|
|
22790
22499
|
const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
|
|
22791
|
-
const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
|
|
22792
|
-
"isRawJSON",
|
|
22793
|
-
"parse",
|
|
22794
|
-
"rawJSON",
|
|
22795
|
-
"stringify"
|
|
22796
|
-
])], ["Math", new Set([
|
|
22797
|
-
"abs",
|
|
22798
|
-
"acos",
|
|
22799
|
-
"acosh",
|
|
22800
|
-
"asin",
|
|
22801
|
-
"asinh",
|
|
22802
|
-
"atan",
|
|
22803
|
-
"atan2",
|
|
22804
|
-
"atanh",
|
|
22805
|
-
"cbrt",
|
|
22806
|
-
"ceil",
|
|
22807
|
-
"clz32",
|
|
22808
|
-
"cos",
|
|
22809
|
-
"cosh",
|
|
22810
|
-
"exp",
|
|
22811
|
-
"floor",
|
|
22812
|
-
"fround",
|
|
22813
|
-
"hypot",
|
|
22814
|
-
"imul",
|
|
22815
|
-
"log",
|
|
22816
|
-
"log10",
|
|
22817
|
-
"log1p",
|
|
22818
|
-
"log2",
|
|
22819
|
-
"max",
|
|
22820
|
-
"min",
|
|
22821
|
-
"pow",
|
|
22822
|
-
"round",
|
|
22823
|
-
"sign",
|
|
22824
|
-
"sin",
|
|
22825
|
-
"sinh",
|
|
22826
|
-
"sqrt",
|
|
22827
|
-
"tan",
|
|
22828
|
-
"tanh",
|
|
22829
|
-
"trunc"
|
|
22830
|
-
])]]);
|
|
22831
22500
|
const PURE_MEMBER_TRANSFORM_NAMES = new Set([
|
|
22832
22501
|
"concat",
|
|
22833
22502
|
"filter",
|
|
@@ -22874,42 +22543,6 @@ const getParameterBindingIdentity = (analysis, functionNode, parameter) => {
|
|
|
22874
22543
|
}
|
|
22875
22544
|
return parameter;
|
|
22876
22545
|
};
|
|
22877
|
-
const isAsyncOrGeneratorFunction = (functionNode) => Boolean(functionNode.async === true || functionNode.generator === true);
|
|
22878
|
-
const isModuleFunction = (functionNode) => {
|
|
22879
|
-
let ancestor = functionNode.parent;
|
|
22880
|
-
while (ancestor) {
|
|
22881
|
-
if (isFunctionLike$1(ancestor)) return false;
|
|
22882
|
-
if (isNodeOfType(ancestor, "Program")) return true;
|
|
22883
|
-
ancestor = ancestor.parent;
|
|
22884
|
-
}
|
|
22885
|
-
return false;
|
|
22886
|
-
};
|
|
22887
|
-
const getFunctionBindingNames = (functionNode) => {
|
|
22888
|
-
const names = /* @__PURE__ */ new Set();
|
|
22889
|
-
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) names.add(functionNode.id.name);
|
|
22890
|
-
const parent = functionNode.parent;
|
|
22891
|
-
if (parent && isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) names.add(parent.id.name);
|
|
22892
|
-
return names;
|
|
22893
|
-
};
|
|
22894
|
-
const collectModuleBindingNames = (functionNode) => {
|
|
22895
|
-
let program = functionNode.parent;
|
|
22896
|
-
while (program && !isNodeOfType(program, "Program")) program = program.parent;
|
|
22897
|
-
const bindingNames = /* @__PURE__ */ new Set();
|
|
22898
|
-
if (!program || !isNodeOfType(program, "Program")) return bindingNames;
|
|
22899
|
-
for (const statement of program.body ?? []) {
|
|
22900
|
-
if (isNodeOfType(statement, "ImportDeclaration")) {
|
|
22901
|
-
for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier.local, "Identifier")) bindingNames.add(specifier.local.name);
|
|
22902
|
-
continue;
|
|
22903
|
-
}
|
|
22904
|
-
const declaration = isNodeOfType(statement, "ExportNamedDeclaration") || isNodeOfType(statement, "ExportDefaultDeclaration") ? statement.declaration : statement;
|
|
22905
|
-
if (isNodeOfType(declaration, "VariableDeclaration")) {
|
|
22906
|
-
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, bindingNames);
|
|
22907
|
-
continue;
|
|
22908
|
-
}
|
|
22909
|
-
if ((isNodeOfType(declaration, "FunctionDeclaration") || isNodeOfType(declaration, "ClassDeclaration")) && isNodeOfType(declaration.id, "Identifier")) bindingNames.add(declaration.id.name);
|
|
22910
|
-
}
|
|
22911
|
-
return bindingNames;
|
|
22912
|
-
};
|
|
22913
22546
|
const buildSubstitutions = (analysis, functionNode, argumentExpressions, parentFrame) => {
|
|
22914
22547
|
const substitutions = /* @__PURE__ */ new Map();
|
|
22915
22548
|
const parameters = getFunctionParameters(functionNode);
|
|
@@ -23040,7 +22673,7 @@ const collectIntroducedBindings = (analysis, functionNode) => {
|
|
|
23040
22673
|
for (const parameter of getFunctionParameters(functionNode)) if (isNodeOfType(parameter, "Identifier")) introducedBindings.add(getParameterBindingIdentity(analysis, functionNode, parameter));
|
|
23041
22674
|
return introducedBindings;
|
|
23042
22675
|
};
|
|
23043
|
-
const collectBoundedEffectExecutionFrames = (analysis, effectNode
|
|
22676
|
+
const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
|
|
23044
22677
|
const effectFunction = getEffectFn(analysis, effectNode);
|
|
23045
22678
|
if (!effectFunction || !isFunctionLike$1(effectFunction) || effectFunction.async === true) return [];
|
|
23046
22679
|
const invokedFunctionEvidence = collectEffectInvokedFunctions(effectFunction);
|
|
@@ -23049,8 +22682,7 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilena
|
|
|
23049
22682
|
invocation: null,
|
|
23050
22683
|
isDeferred: false,
|
|
23051
22684
|
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23052
|
-
substitutions: /* @__PURE__ */ new Map()
|
|
23053
|
-
currentFilename
|
|
22685
|
+
substitutions: /* @__PURE__ */ new Map()
|
|
23054
22686
|
};
|
|
23055
22687
|
const frames = [rootFrame];
|
|
23056
22688
|
walkAst(effectFunction, (child) => {
|
|
@@ -23071,8 +22703,7 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilena
|
|
|
23071
22703
|
invocation: child,
|
|
23072
22704
|
isDeferred,
|
|
23073
22705
|
introducedBindings,
|
|
23074
|
-
substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame)
|
|
23075
|
-
currentFilename
|
|
22706
|
+
substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame)
|
|
23076
22707
|
});
|
|
23077
22708
|
};
|
|
23078
22709
|
if (isFunctionLike$1(callee)) {
|
|
@@ -23125,174 +22756,6 @@ const isOpaqueHookCall = (callExpression) => {
|
|
|
23125
22756
|
const calleeName = getCallCalleeName$1(callExpression);
|
|
23126
22757
|
return Boolean(calleeName && /^use[A-Z0-9]/.test(calleeName) && calleeName !== "useMemo" && calleeName !== "useCallback" && calleeName !== "useEffectEvent");
|
|
23127
22758
|
};
|
|
23128
|
-
const analyzeHelperStatements = (statements, environment, usedParameterIndices, analyzeExpression) => {
|
|
23129
|
-
let canContinue = true;
|
|
23130
|
-
for (const statement of statements) {
|
|
23131
|
-
if (!canContinue) {
|
|
23132
|
-
if (!isNodeOfType(statement, "EmptyStatement")) return {
|
|
23133
|
-
canContinue: false,
|
|
23134
|
-
isValid: false
|
|
23135
|
-
};
|
|
23136
|
-
continue;
|
|
23137
|
-
}
|
|
23138
|
-
if (isNodeOfType(statement, "EmptyStatement")) continue;
|
|
23139
|
-
if (isNodeOfType(statement, "ReturnStatement")) {
|
|
23140
|
-
if (!statement.argument || !analyzeExpression(statement.argument, environment, usedParameterIndices)) return {
|
|
23141
|
-
canContinue: false,
|
|
23142
|
-
isValid: false
|
|
23143
|
-
};
|
|
23144
|
-
canContinue = false;
|
|
23145
|
-
continue;
|
|
23146
|
-
}
|
|
23147
|
-
if (isNodeOfType(statement, "BlockStatement")) {
|
|
23148
|
-
const blockSummary = analyzeHelperStatements(statement.body ?? [], environment, usedParameterIndices, analyzeExpression);
|
|
23149
|
-
if (!blockSummary.isValid) return blockSummary;
|
|
23150
|
-
canContinue = blockSummary.canContinue;
|
|
23151
|
-
continue;
|
|
23152
|
-
}
|
|
23153
|
-
if (isNodeOfType(statement, "IfStatement")) {
|
|
23154
|
-
if (!analyzeExpression(statement.test, environment, usedParameterIndices)) return {
|
|
23155
|
-
canContinue: false,
|
|
23156
|
-
isValid: false
|
|
23157
|
-
};
|
|
23158
|
-
const consequentSummary = analyzeHelperStatements([statement.consequent], environment, usedParameterIndices, analyzeExpression);
|
|
23159
|
-
if (!consequentSummary.isValid) return consequentSummary;
|
|
23160
|
-
const alternateSummary = statement.alternate ? analyzeHelperStatements([statement.alternate], environment, usedParameterIndices, analyzeExpression) : {
|
|
23161
|
-
canContinue: true,
|
|
23162
|
-
isValid: true
|
|
23163
|
-
};
|
|
23164
|
-
if (!alternateSummary.isValid) return alternateSummary;
|
|
23165
|
-
canContinue = consequentSummary.canContinue || alternateSummary.canContinue;
|
|
23166
|
-
continue;
|
|
23167
|
-
}
|
|
23168
|
-
return {
|
|
23169
|
-
canContinue: false,
|
|
23170
|
-
isValid: false
|
|
23171
|
-
};
|
|
23172
|
-
}
|
|
23173
|
-
return {
|
|
23174
|
-
canContinue,
|
|
23175
|
-
isValid: true
|
|
23176
|
-
};
|
|
23177
|
-
};
|
|
23178
|
-
const analyzeHelperExpression = (expression, environment, usedParameterIndices) => {
|
|
23179
|
-
const node = stripParenExpression(expression);
|
|
23180
|
-
if (isNodeOfType(node, "Literal") || isNodeOfType(node, "TemplateElement")) return true;
|
|
23181
|
-
if (isNodeOfType(node, "Identifier")) {
|
|
23182
|
-
const parameterIndex = environment.parameterIndices.get(node.name);
|
|
23183
|
-
if (parameterIndex !== void 0) {
|
|
23184
|
-
if (parameterIndex !== null) usedParameterIndices.add(parameterIndex);
|
|
23185
|
-
return true;
|
|
23186
|
-
}
|
|
23187
|
-
return node.name === "undefined" || node.name === "NaN" || node.name === "Infinity";
|
|
23188
|
-
}
|
|
23189
|
-
if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => !element || analyzeHelperExpression(element, environment, usedParameterIndices));
|
|
23190
|
-
if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).every((property) => {
|
|
23191
|
-
if (isNodeOfType(property, "SpreadElement")) return analyzeHelperExpression(property.argument, environment, usedParameterIndices);
|
|
23192
|
-
if (!isNodeOfType(property, "Property") || property.kind !== "init" || property.method === true) return false;
|
|
23193
|
-
if (property.computed && !analyzeHelperExpression(property.key, environment, usedParameterIndices)) return false;
|
|
23194
|
-
return analyzeHelperExpression(property.value, environment, usedParameterIndices);
|
|
23195
|
-
});
|
|
23196
|
-
if (isNodeOfType(node, "TemplateLiteral")) return (node.expressions ?? []).every((templateExpression) => analyzeHelperExpression(templateExpression, environment, usedParameterIndices));
|
|
23197
|
-
if (isNodeOfType(node, "UnaryExpression")) return node.operator !== "delete" && analyzeHelperExpression(node.argument, environment, usedParameterIndices);
|
|
23198
|
-
if (isNodeOfType(node, "BinaryExpression") || isNodeOfType(node, "LogicalExpression")) return analyzeHelperExpression(node.left, environment, usedParameterIndices) && analyzeHelperExpression(node.right, environment, usedParameterIndices);
|
|
23199
|
-
if (isNodeOfType(node, "ConditionalExpression")) return analyzeHelperExpression(node.test, environment, usedParameterIndices) && analyzeHelperExpression(node.consequent, environment, usedParameterIndices) && analyzeHelperExpression(node.alternate, environment, usedParameterIndices);
|
|
23200
|
-
if (isNodeOfType(node, "MemberExpression")) {
|
|
23201
|
-
if (!analyzeHelperExpression(node.object, environment, usedParameterIndices)) return false;
|
|
23202
|
-
return !node.computed || analyzeHelperExpression(node.property, environment, usedParameterIndices);
|
|
23203
|
-
}
|
|
23204
|
-
if (isFunctionLike$1(node)) {
|
|
23205
|
-
if (isAsyncOrGeneratorFunction(node)) return false;
|
|
23206
|
-
const callbackParameterIndices = new Map(environment.parameterIndices);
|
|
23207
|
-
for (const parameter of getFunctionParameters(node)) {
|
|
23208
|
-
if (!isNodeOfType(parameter, "Identifier")) return false;
|
|
23209
|
-
callbackParameterIndices.set(parameter.name, null);
|
|
23210
|
-
}
|
|
23211
|
-
const callbackEnvironment = {
|
|
23212
|
-
parameterIndices: callbackParameterIndices,
|
|
23213
|
-
recursiveNames: environment.recursiveNames,
|
|
23214
|
-
shadowedGlobalNames: environment.shadowedGlobalNames
|
|
23215
|
-
};
|
|
23216
|
-
if (!isNodeOfType(node.body, "BlockStatement")) return analyzeHelperExpression(node.body, callbackEnvironment, usedParameterIndices);
|
|
23217
|
-
const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
|
|
23218
|
-
return callbackSummary.isValid && !callbackSummary.canContinue;
|
|
23219
|
-
}
|
|
23220
|
-
if (isNodeOfType(node, "CallExpression")) {
|
|
23221
|
-
const callee = stripParenExpression(node.callee);
|
|
23222
|
-
const calleeRoot = getMemberRoot(callee);
|
|
23223
|
-
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && !environment.parameterIndices.has(callee.name) && !environment.recursiveNames.has(callee.name) && !environment.shadowedGlobalNames.has(callee.name);
|
|
23224
|
-
const namespaceName = isNodeOfType(calleeRoot, "Identifier") && !environment.parameterIndices.has(calleeRoot.name) && !environment.recursiveNames.has(calleeRoot.name) && !environment.shadowedGlobalNames.has(calleeRoot.name) ? calleeRoot.name : null;
|
|
23225
|
-
const namespaceMemberName = getStaticMemberName(callee);
|
|
23226
|
-
const isPureNamespaceCall = isNodeOfType(callee, "MemberExpression") && namespaceName !== null && namespaceMemberName !== null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(namespaceName)?.has(namespaceMemberName) === true;
|
|
23227
|
-
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
|
|
23228
|
-
if (!isPureGlobalCall && !isPureNamespaceCall && !isPureMemberTransform) return false;
|
|
23229
|
-
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression") && !analyzeHelperExpression(callee.object, environment, usedParameterIndices)) return false;
|
|
23230
|
-
return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
|
|
23231
|
-
}
|
|
23232
|
-
if (isNodeOfType(node, "SpreadElement")) return analyzeHelperExpression(node.argument, environment, usedParameterIndices);
|
|
23233
|
-
return false;
|
|
23234
|
-
};
|
|
23235
|
-
const helperSummaryCache = /* @__PURE__ */ new WeakMap();
|
|
23236
|
-
const summarizeHelperReturn = (functionNode) => {
|
|
23237
|
-
if (!isFunctionLike$1(functionNode) || !isModuleFunction(functionNode)) return null;
|
|
23238
|
-
if (helperSummaryCache.has(functionNode)) return helperSummaryCache.get(functionNode) ?? null;
|
|
23239
|
-
if (isAsyncOrGeneratorFunction(functionNode)) {
|
|
23240
|
-
helperSummaryCache.set(functionNode, null);
|
|
23241
|
-
return null;
|
|
23242
|
-
}
|
|
23243
|
-
const parameterIndices = /* @__PURE__ */ new Map();
|
|
23244
|
-
const parameters = getFunctionParameters(functionNode);
|
|
23245
|
-
for (let parameterIndex = 0; parameterIndex < parameters.length; parameterIndex += 1) {
|
|
23246
|
-
const parameter = parameters[parameterIndex];
|
|
23247
|
-
if (!parameter || !isNodeOfType(parameter, "Identifier") || parameterIndices.has(parameter.name)) {
|
|
23248
|
-
helperSummaryCache.set(functionNode, null);
|
|
23249
|
-
return null;
|
|
23250
|
-
}
|
|
23251
|
-
parameterIndices.set(parameter.name, parameterIndex);
|
|
23252
|
-
}
|
|
23253
|
-
const environment = {
|
|
23254
|
-
parameterIndices,
|
|
23255
|
-
recursiveNames: getFunctionBindingNames(functionNode),
|
|
23256
|
-
shadowedGlobalNames: collectModuleBindingNames(functionNode)
|
|
23257
|
-
};
|
|
23258
|
-
const usedParameterIndices = /* @__PURE__ */ new Set();
|
|
23259
|
-
if (!isNodeOfType(functionNode.body, "BlockStatement")) {
|
|
23260
|
-
if (!analyzeHelperExpression(functionNode.body, environment, usedParameterIndices)) {
|
|
23261
|
-
helperSummaryCache.set(functionNode, null);
|
|
23262
|
-
return null;
|
|
23263
|
-
}
|
|
23264
|
-
} else {
|
|
23265
|
-
const controlFlowSummary = analyzeHelperStatements(functionNode.body.body ?? [], environment, usedParameterIndices, analyzeHelperExpression);
|
|
23266
|
-
if (!controlFlowSummary.isValid || controlFlowSummary.canContinue) {
|
|
23267
|
-
helperSummaryCache.set(functionNode, null);
|
|
23268
|
-
return null;
|
|
23269
|
-
}
|
|
23270
|
-
}
|
|
23271
|
-
const summary = { usedParameterIndices };
|
|
23272
|
-
helperSummaryCache.set(functionNode, summary);
|
|
23273
|
-
return summary;
|
|
23274
|
-
};
|
|
23275
|
-
const resolveValueHelperFunction = (analysis, callee, currentFilename) => {
|
|
23276
|
-
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
23277
|
-
const reference = getRef(analysis, callee);
|
|
23278
|
-
if (!reference?.resolved) return null;
|
|
23279
|
-
const importDefinition = reference.resolved.defs.find((definition) => definition.type === "ImportBinding");
|
|
23280
|
-
if (importDefinition) {
|
|
23281
|
-
if (!currentFilename) return null;
|
|
23282
|
-
const specifier = importDefinition.node;
|
|
23283
|
-
if (!isNodeOfType(specifier, "ImportSpecifier") && !isNodeOfType(specifier, "ImportDefaultSpecifier")) return null;
|
|
23284
|
-
const importDeclaration = specifier.parent;
|
|
23285
|
-
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
|
|
23286
|
-
if (importDeclaration.importKind === "type" || isNodeOfType(specifier, "ImportSpecifier") && specifier.importKind === "type") return null;
|
|
23287
|
-
const source = importDeclaration.source?.value;
|
|
23288
|
-
if (typeof source !== "string") return null;
|
|
23289
|
-
const exportedName = resolveImportedExportName(specifier);
|
|
23290
|
-
if (!exportedName) return null;
|
|
23291
|
-
return resolveCrossFileFunctionExport(currentFilename, source, exportedName);
|
|
23292
|
-
}
|
|
23293
|
-
const callable = resolveWrappedCallable(analysis, callee);
|
|
23294
|
-
return callable && isModuleFunction(callable) ? callable : null;
|
|
23295
|
-
};
|
|
23296
22759
|
const isLocallyConstructedObjectMember = (reference, memberExpression) => isNodeOfType(memberExpression, "MemberExpression") && reference.resolved?.defs.some((definition) => {
|
|
23297
22760
|
const definitionNode = definition.node;
|
|
23298
22761
|
return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
|
|
@@ -23378,55 +22841,38 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23378
22841
|
const calleeRoot = getMemberRoot(callee);
|
|
23379
22842
|
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;
|
|
23380
22843
|
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
|
|
23381
|
-
if (
|
|
23382
|
-
|
|
23383
|
-
|
|
23384
|
-
|
|
23385
|
-
|
|
23386
|
-
|
|
22844
|
+
if (isPureMemberTransform) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
22845
|
+
for (const argument of node.arguments ?? []) {
|
|
22846
|
+
if (isFunctionLike$1(argument)) continue;
|
|
22847
|
+
mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
22848
|
+
}
|
|
22849
|
+
if (isPureGlobalCall || isPureMemberTransform) return evidence;
|
|
22850
|
+
if (isNodeOfType(callee, "MemberExpression")) {
|
|
22851
|
+
evidence.hasUnknownSource = true;
|
|
23387
22852
|
return evidence;
|
|
23388
22853
|
}
|
|
23389
22854
|
if (remainingCallFrames <= 0) {
|
|
23390
22855
|
evidence.hasUnknownSource = true;
|
|
23391
22856
|
return evidence;
|
|
23392
22857
|
}
|
|
23393
|
-
const
|
|
23394
|
-
if (
|
|
23395
|
-
|
|
23396
|
-
evidence.hasUnknownSource = true;
|
|
23397
|
-
return evidence;
|
|
23398
|
-
}
|
|
23399
|
-
const localHelperFrame = {
|
|
23400
|
-
functionNode: localHelperFunction,
|
|
23401
|
-
invocation: node,
|
|
23402
|
-
isDeferred: false,
|
|
23403
|
-
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23404
|
-
substitutions: buildSubstitutions(analysis, localHelperFunction, node.arguments ?? [], frame),
|
|
23405
|
-
currentFilename: frame.currentFilename
|
|
23406
|
-
};
|
|
23407
|
-
const returnedExpressions = getReturnedExpressions(localHelperFunction);
|
|
23408
|
-
if (returnedExpressions.length === 0) {
|
|
23409
|
-
evidence.hasUnknownSource = true;
|
|
23410
|
-
return evidence;
|
|
23411
|
-
}
|
|
23412
|
-
for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, localHelperFrame, remainingCallFrames - 1, new Set(visitedBindings)));
|
|
22858
|
+
const callable = resolveWrappedCallable(analysis, callee);
|
|
22859
|
+
if (!callable || callable.async === true || functionInvokesItself(analysis, callable)) {
|
|
22860
|
+
evidence.hasUnknownSource = true;
|
|
23413
22861
|
return evidence;
|
|
23414
22862
|
}
|
|
23415
|
-
const
|
|
23416
|
-
|
|
23417
|
-
|
|
22863
|
+
const valueFrame = {
|
|
22864
|
+
functionNode: callable,
|
|
22865
|
+
invocation: node,
|
|
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) {
|
|
23418
22872
|
evidence.hasUnknownSource = true;
|
|
23419
22873
|
return evidence;
|
|
23420
22874
|
}
|
|
23421
|
-
const
|
|
23422
|
-
for (const parameterIndex of helperSummary.usedParameterIndices) {
|
|
23423
|
-
const argument = argumentsForHelper[parameterIndex];
|
|
23424
|
-
if (!argument) {
|
|
23425
|
-
evidence.hasUnknownSource = true;
|
|
23426
|
-
return evidence;
|
|
23427
|
-
}
|
|
23428
|
-
mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames - 1, new Set(visitedBindings)));
|
|
23429
|
-
}
|
|
22875
|
+
for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, valueFrame, remainingCallFrames - 1, new Set(visitedBindings)));
|
|
23430
22876
|
return evidence;
|
|
23431
22877
|
}
|
|
23432
22878
|
if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
|
|
@@ -23444,20 +22890,6 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23444
22890
|
}
|
|
23445
22891
|
return evidence;
|
|
23446
22892
|
};
|
|
23447
|
-
const collectRenderValueEvidence = (analysis, expression, componentFunction, currentFilename) => {
|
|
23448
|
-
const evidence = collectValueEvidence(analysis, expression, {
|
|
23449
|
-
functionNode: componentFunction,
|
|
23450
|
-
invocation: null,
|
|
23451
|
-
isDeferred: false,
|
|
23452
|
-
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23453
|
-
substitutions: /* @__PURE__ */ new Map(),
|
|
23454
|
-
currentFilename
|
|
23455
|
-
}, 1);
|
|
23456
|
-
return {
|
|
23457
|
-
sourceReferences: evidence.sourceReferences,
|
|
23458
|
-
isExclusivelyRenderKnown: evidence.sourceReferences.size > 0 && !evidence.hasUnknownSource && !evidence.hasDeferredIntroducedValue && !evidence.readsExternalValue
|
|
23459
|
-
};
|
|
23460
|
-
};
|
|
23461
22893
|
const findStateSetterReference = (analysis, callExpression) => {
|
|
23462
22894
|
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
23463
22895
|
const callee = stripParenExpression(callExpression.callee);
|
|
@@ -23520,8 +22952,8 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
|
|
|
23520
22952
|
}
|
|
23521
22953
|
return false;
|
|
23522
22954
|
};
|
|
23523
|
-
const collectEffectStateWriteFacts = (analysis, effectNode
|
|
23524
|
-
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode
|
|
22955
|
+
const collectEffectStateWriteFacts = (analysis, effectNode) => {
|
|
22956
|
+
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode);
|
|
23525
22957
|
if (frames.length === 0) return [];
|
|
23526
22958
|
const effectHasCleanup = hasCleanup(analysis, effectNode);
|
|
23527
22959
|
const cleanupManagedStateDeclarators = /* @__PURE__ */ new Set();
|
|
@@ -23541,8 +22973,7 @@ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) =>
|
|
|
23541
22973
|
invocation: callExpression,
|
|
23542
22974
|
isDeferred: frame.isDeferred,
|
|
23543
22975
|
introducedBindings: collectIntroducedBindings(analysis, writtenValue),
|
|
23544
|
-
substitutions: /* @__PURE__ */ new Map()
|
|
23545
|
-
currentFilename
|
|
22976
|
+
substitutions: /* @__PURE__ */ new Map()
|
|
23546
22977
|
};
|
|
23547
22978
|
valueEvidence = emptyEvidence();
|
|
23548
22979
|
const returnedExpressions = getReturnedExpressions(writtenValue);
|
|
@@ -23591,7 +23022,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
23591
23022
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
23592
23023
|
if (!dependencyReferences) return;
|
|
23593
23024
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
23594
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node
|
|
23025
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
23595
23026
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
23596
23027
|
context.report({
|
|
23597
23028
|
node: fact.callExpression,
|
|
@@ -24852,139 +24283,6 @@ const createRelativeImportSource = (filename, targetFilePath) => {
|
|
|
24852
24283
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
24853
24284
|
};
|
|
24854
24285
|
//#endregion
|
|
24855
|
-
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
24856
|
-
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
24857
|
-
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24858
|
-
const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24859
|
-
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24860
|
-
const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
|
|
24861
|
-
const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
|
|
24862
|
-
const createNonBarrelInfo = () => ({
|
|
24863
|
-
isBarrel: false,
|
|
24864
|
-
exportsByName: /* @__PURE__ */ new Map(),
|
|
24865
|
-
starExportSources: []
|
|
24866
|
-
});
|
|
24867
|
-
const addImportedBinding = (importedBindings, binding) => {
|
|
24868
|
-
importedBindings.set(binding.localName, {
|
|
24869
|
-
...binding,
|
|
24870
|
-
didExport: false
|
|
24871
|
-
});
|
|
24872
|
-
};
|
|
24873
|
-
const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
|
|
24874
|
-
for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
|
|
24875
|
-
localName: specifier.exportedName,
|
|
24876
|
-
importedName: specifier.localName,
|
|
24877
|
-
source,
|
|
24878
|
-
isTypeOnly: specifier.isTypeOnly
|
|
24879
|
-
});
|
|
24880
|
-
};
|
|
24881
|
-
const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
|
|
24882
|
-
const trimmedImportClause = importClause.trim();
|
|
24883
|
-
const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
|
|
24884
|
-
if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
|
|
24885
|
-
localName: namespaceMatch[1],
|
|
24886
|
-
importedName: "*",
|
|
24887
|
-
source,
|
|
24888
|
-
isTypeOnly: declarationIsTypeOnly
|
|
24889
|
-
});
|
|
24890
|
-
const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
|
|
24891
|
-
if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
|
|
24892
|
-
const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
|
|
24893
|
-
if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
|
|
24894
|
-
localName: defaultImportName,
|
|
24895
|
-
importedName: "default",
|
|
24896
|
-
source,
|
|
24897
|
-
isTypeOnly: declarationIsTypeOnly
|
|
24898
|
-
});
|
|
24899
|
-
};
|
|
24900
|
-
const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
|
|
24901
|
-
let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
|
|
24902
|
-
collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
|
|
24903
|
-
return "";
|
|
24904
|
-
});
|
|
24905
|
-
withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
|
|
24906
|
-
const isTypeOnly = Boolean(typeKeyword);
|
|
24907
|
-
if (namespaceExportName) {
|
|
24908
|
-
exportsByName.set(namespaceExportName, {
|
|
24909
|
-
exportedName: namespaceExportName,
|
|
24910
|
-
importedName: "*",
|
|
24911
|
-
source,
|
|
24912
|
-
isTypeOnly
|
|
24913
|
-
});
|
|
24914
|
-
return "";
|
|
24915
|
-
}
|
|
24916
|
-
if (specifiersText) {
|
|
24917
|
-
for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
|
|
24918
|
-
exportedName: specifier.exportedName,
|
|
24919
|
-
importedName: specifier.localName,
|
|
24920
|
-
source,
|
|
24921
|
-
isTypeOnly: specifier.isTypeOnly
|
|
24922
|
-
});
|
|
24923
|
-
return "";
|
|
24924
|
-
}
|
|
24925
|
-
starExportSources.push(source);
|
|
24926
|
-
return "";
|
|
24927
|
-
});
|
|
24928
|
-
withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
|
|
24929
|
-
for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
|
|
24930
|
-
const importedBinding = importedBindings.get(specifier.localName);
|
|
24931
|
-
if (!importedBinding) return _match;
|
|
24932
|
-
importedBinding.didExport = true;
|
|
24933
|
-
exportsByName.set(specifier.exportedName, {
|
|
24934
|
-
exportedName: specifier.exportedName,
|
|
24935
|
-
importedName: importedBinding.importedName,
|
|
24936
|
-
source: importedBinding.source,
|
|
24937
|
-
isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
|
|
24938
|
-
});
|
|
24939
|
-
}
|
|
24940
|
-
return "";
|
|
24941
|
-
});
|
|
24942
|
-
return withoutKnownDeclarations;
|
|
24943
|
-
};
|
|
24944
|
-
const hasUnexportedRuntimeImport = (importedBindings) => {
|
|
24945
|
-
for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
|
|
24946
|
-
return false;
|
|
24947
|
-
};
|
|
24948
|
-
const classifyBarrelModule = (sourceText) => {
|
|
24949
|
-
const strippedSource = stripJsComments(sourceText).trim();
|
|
24950
|
-
if (!strippedSource) return createNonBarrelInfo();
|
|
24951
|
-
const importedBindings = /* @__PURE__ */ new Map();
|
|
24952
|
-
const exportsByName = /* @__PURE__ */ new Map();
|
|
24953
|
-
const starExportSources = [];
|
|
24954
|
-
if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
|
|
24955
|
-
return {
|
|
24956
|
-
isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
|
|
24957
|
-
exportsByName,
|
|
24958
|
-
starExportSources
|
|
24959
|
-
};
|
|
24960
|
-
};
|
|
24961
|
-
const getBarrelIndexModuleInfo = (filePath) => {
|
|
24962
|
-
if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
|
|
24963
|
-
recordContentProbe(filePath);
|
|
24964
|
-
let fileStat;
|
|
24965
|
-
try {
|
|
24966
|
-
fileStat = fs.statSync(filePath);
|
|
24967
|
-
} catch {
|
|
24968
|
-
fileStat = null;
|
|
24969
|
-
}
|
|
24970
|
-
const cachedResult = barrelIndexModuleInfoCache.get(filePath);
|
|
24971
|
-
if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
|
|
24972
|
-
if (fileStat === null) return createNonBarrelInfo();
|
|
24973
|
-
let moduleInfo = createNonBarrelInfo();
|
|
24974
|
-
try {
|
|
24975
|
-
moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
|
|
24976
|
-
} catch {
|
|
24977
|
-
moduleInfo = createNonBarrelInfo();
|
|
24978
|
-
}
|
|
24979
|
-
barrelIndexModuleInfoCache.set(filePath, {
|
|
24980
|
-
mtimeMs: fileStat.mtimeMs,
|
|
24981
|
-
size: fileStat.size,
|
|
24982
|
-
moduleInfo
|
|
24983
|
-
});
|
|
24984
|
-
return moduleInfo;
|
|
24985
|
-
};
|
|
24986
|
-
const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
|
|
24987
|
-
//#endregion
|
|
24988
24286
|
//#region src/react-native-dependency-names.ts
|
|
24989
24287
|
const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
|
|
24990
24288
|
"expo",
|
|
@@ -25187,35 +24485,6 @@ const classifyReactNativeFileTarget = (context) => {
|
|
|
25187
24485
|
};
|
|
25188
24486
|
const isReactNativeFileActive = (context) => classifyReactNativeFileTarget(context) !== "web";
|
|
25189
24487
|
//#endregion
|
|
25190
|
-
//#region src/plugin/utils/resolve-barrel-export-file-path.ts
|
|
25191
|
-
const getUniqueFilePath = (filePaths) => {
|
|
25192
|
-
const uniqueFilePaths = new Set(filePaths);
|
|
25193
|
-
if (uniqueFilePaths.size !== 1) return null;
|
|
25194
|
-
const [filePath] = uniqueFilePaths;
|
|
25195
|
-
return filePath ?? null;
|
|
25196
|
-
};
|
|
25197
|
-
const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
|
|
25198
|
-
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
|
|
25199
|
-
if (!resolvedTargetPath) return null;
|
|
25200
|
-
const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
|
|
25201
|
-
if (nestedTargetPath) return nestedTargetPath;
|
|
25202
|
-
return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
|
|
25203
|
-
};
|
|
25204
|
-
const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
|
|
25205
|
-
if (visitedFilePaths.has(barrelFilePath)) return null;
|
|
25206
|
-
visitedFilePaths.add(barrelFilePath);
|
|
25207
|
-
const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
|
|
25208
|
-
if (!moduleInfo.isBarrel) return null;
|
|
25209
|
-
const target = moduleInfo.exportsByName.get(exportedName);
|
|
25210
|
-
if (target) {
|
|
25211
|
-
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
|
|
25212
|
-
if (!resolvedTargetPath) return null;
|
|
25213
|
-
return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
|
|
25214
|
-
}
|
|
25215
|
-
if (exportedName === "default") return null;
|
|
25216
|
-
return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
|
|
25217
|
-
};
|
|
25218
|
-
//#endregion
|
|
25219
24488
|
//#region src/plugin/rules/bundle-size/no-barrel-import.ts
|
|
25220
24489
|
const TYPE_DECLARATION_FILE_PATTERN = /\.d\.[cm]?ts$/;
|
|
25221
24490
|
const SERVER_ONLY_FILE_PATTERN = /\.server\.[cm]?[jt]sx?$/;
|
|
@@ -25545,7 +24814,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
25545
24814
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
25546
24815
|
return false;
|
|
25547
24816
|
};
|
|
25548
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
24817
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
|
|
25549
24818
|
"forEach",
|
|
25550
24819
|
"map",
|
|
25551
24820
|
"filter",
|
|
@@ -25566,7 +24835,7 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
25566
24835
|
if (parent.callee === functionNode) return true;
|
|
25567
24836
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
25568
24837
|
const callee = parent.callee;
|
|
25569
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
24838
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
|
|
25570
24839
|
};
|
|
25571
24840
|
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
25572
24841
|
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
@@ -26510,6 +25779,57 @@ const noDefaultProps = defineRule({
|
|
|
26510
25779
|
} })
|
|
26511
25780
|
});
|
|
26512
25781
|
//#endregion
|
|
25782
|
+
//#region src/plugin/rules/state-and-effects/no-derived-state.ts
|
|
25783
|
+
const getStateName$1 = (stateDeclarator) => {
|
|
25784
|
+
if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
|
|
25785
|
+
if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
|
|
25786
|
+
const stateBinding = stateDeclarator.id.elements?.[0];
|
|
25787
|
+
if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
|
|
25788
|
+
const setterBinding = stateDeclarator.id.elements?.[1];
|
|
25789
|
+
if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
|
|
25790
|
+
if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
|
|
25791
|
+
return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
|
|
25792
|
+
};
|
|
25793
|
+
const noDerivedState = defineRule({
|
|
25794
|
+
id: "no-derived-state",
|
|
25795
|
+
title: "Derived value copied into state",
|
|
25796
|
+
severity: "warn",
|
|
25797
|
+
tags: ["test-noise"],
|
|
25798
|
+
recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into useState through a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
|
|
25799
|
+
create: (context) => ({ CallExpression(node) {
|
|
25800
|
+
if (!isUseEffect(node)) return;
|
|
25801
|
+
const analysis = getProgramAnalysis(node);
|
|
25802
|
+
if (!analysis) return;
|
|
25803
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
25804
|
+
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
25805
|
+
const stateName = getStateName$1(fact.stateDeclarator);
|
|
25806
|
+
context.report({
|
|
25807
|
+
node: fact.callExpression,
|
|
25808
|
+
message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
|
|
25809
|
+
});
|
|
25810
|
+
}
|
|
25811
|
+
} })
|
|
25812
|
+
});
|
|
25813
|
+
//#endregion
|
|
25814
|
+
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
25815
|
+
const noDerivedStateEffect = defineRule({
|
|
25816
|
+
id: "no-derived-state-effect",
|
|
25817
|
+
title: "Derived state stored in an effect",
|
|
25818
|
+
severity: "warn",
|
|
25819
|
+
tags: ["test-noise"],
|
|
25820
|
+
recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
25821
|
+
create: (context) => ({ CallExpression(node) {
|
|
25822
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
25823
|
+
const analysis = getProgramAnalysis(node);
|
|
25824
|
+
if (!analysis) return;
|
|
25825
|
+
if (!collectEffectStateWriteFacts(analysis, node).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
25826
|
+
context.report({
|
|
25827
|
+
node,
|
|
25828
|
+
message: "You pay an extra render for state you can derive from other values."
|
|
25829
|
+
});
|
|
25830
|
+
} })
|
|
25831
|
+
});
|
|
25832
|
+
//#endregion
|
|
26513
25833
|
//#region src/plugin/utils/is-component-function.ts
|
|
26514
25834
|
const isFunctionAssignedToComponent = (functionNode) => {
|
|
26515
25835
|
let cursor = functionNode.parent ?? null;
|
|
@@ -26643,236 +25963,6 @@ const createComponentPropStackTracker = (callbacks) => {
|
|
|
26643
25963
|
};
|
|
26644
25964
|
};
|
|
26645
25965
|
//#endregion
|
|
26646
|
-
//#region src/plugin/rules/state-and-effects/utils/collect-render-state-write-facts.ts
|
|
26647
|
-
const findReferenceDeclarator = (reference) => {
|
|
26648
|
-
for (const definition of reference.resolved?.defs ?? []) {
|
|
26649
|
-
const definitionNode = definition.node;
|
|
26650
|
-
if (isNodeOfType(definitionNode, "VariableDeclarator")) return definitionNode;
|
|
26651
|
-
}
|
|
26652
|
-
return null;
|
|
26653
|
-
};
|
|
26654
|
-
const resolveSimpleRenderSource = (analysis, expression, visitedBindings = /* @__PURE__ */ new Set()) => {
|
|
26655
|
-
const node = stripParenExpression(expression);
|
|
26656
|
-
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26657
|
-
const reference = getRef(analysis, node);
|
|
26658
|
-
if (!reference?.resolved || visitedBindings.has(reference.resolved)) return null;
|
|
26659
|
-
if (isProp(analysis, reference)) {
|
|
26660
|
-
if (isWholePropsObjectReference(analysis, reference)) return null;
|
|
26661
|
-
return { bindingIdentity: reference.resolved };
|
|
26662
|
-
}
|
|
26663
|
-
if (isState(analysis, reference)) {
|
|
26664
|
-
const stateDeclarator = getUseStateDecl(analysis, reference);
|
|
26665
|
-
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || isExternallyDrivenState(analysis, reference)) return null;
|
|
26666
|
-
return { bindingIdentity: reference.resolved };
|
|
26667
|
-
}
|
|
26668
|
-
if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
|
|
26669
|
-
const declarator = findReferenceDeclarator(reference);
|
|
26670
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
26671
|
-
const nextVisitedBindings = new Set(visitedBindings);
|
|
26672
|
-
nextVisitedBindings.add(reference.resolved);
|
|
26673
|
-
return resolveSimpleRenderSource(analysis, declarator.init, nextVisitedBindings);
|
|
26674
|
-
};
|
|
26675
|
-
const doesEvidenceMatchSource = (evidence, source) => evidence.isExclusivelyRenderKnown && evidence.sourceReferences.size > 0 && [...evidence.sourceReferences].every((sourceReference) => sourceReference.resolved === source.bindingIdentity);
|
|
26676
|
-
const getDirectBranchStatements = (branch) => {
|
|
26677
|
-
if (isNodeOfType(branch, "BlockStatement")) return branch.body ?? [];
|
|
26678
|
-
return [branch];
|
|
26679
|
-
};
|
|
26680
|
-
const getDirectCallExpression = (statement) => {
|
|
26681
|
-
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26682
|
-
const expression = stripParenExpression(statement.expression);
|
|
26683
|
-
return isNodeOfType(expression, "CallExpression") ? expression : null;
|
|
26684
|
-
};
|
|
26685
|
-
const getDirectAssignmentExpression = (statement) => {
|
|
26686
|
-
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26687
|
-
const expression = stripParenExpression(statement.expression);
|
|
26688
|
-
return isNodeOfType(expression, "AssignmentExpression") && expression.operator === "=" ? expression : null;
|
|
26689
|
-
};
|
|
26690
|
-
const findDirectStateSetterReference = (analysis, callExpression) => {
|
|
26691
|
-
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
26692
|
-
const callee = stripParenExpression(callExpression.callee);
|
|
26693
|
-
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
26694
|
-
const reference = getRef(analysis, callee);
|
|
26695
|
-
return reference && isStateSetter(analysis, reference) ? reference : null;
|
|
26696
|
-
};
|
|
26697
|
-
const getStaticRefCurrentReference = (analysis, expression) => {
|
|
26698
|
-
const node = stripParenExpression(expression);
|
|
26699
|
-
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.object, "Identifier") || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
|
|
26700
|
-
const reference = getRef(analysis, node.object);
|
|
26701
|
-
if (!reference) return null;
|
|
26702
|
-
const declarator = findReferenceDeclarator(reference);
|
|
26703
|
-
return declarator && isGenuineReactHookDeclarator(analysis, declarator, "useRef") ? reference : null;
|
|
26704
|
-
};
|
|
26705
|
-
const getStateTracker = (analysis, expression) => {
|
|
26706
|
-
const node = stripParenExpression(expression);
|
|
26707
|
-
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26708
|
-
const reference = getRef(analysis, node);
|
|
26709
|
-
if (!reference || !isState(analysis, reference)) return null;
|
|
26710
|
-
const declarator = getUseStateDecl(analysis, reference);
|
|
26711
|
-
if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useState")) return null;
|
|
26712
|
-
return {
|
|
26713
|
-
kind: "state",
|
|
26714
|
-
declarator
|
|
26715
|
-
};
|
|
26716
|
-
};
|
|
26717
|
-
const getRefTracker = (analysis, expression) => {
|
|
26718
|
-
const reference = getStaticRefCurrentReference(analysis, expression);
|
|
26719
|
-
return reference ? {
|
|
26720
|
-
kind: "ref",
|
|
26721
|
-
reference
|
|
26722
|
-
} : null;
|
|
26723
|
-
};
|
|
26724
|
-
const getTrackerInitializer = (tracker) => {
|
|
26725
|
-
const declarator = tracker.kind === "state" ? tracker.declarator : findReferenceDeclarator(tracker.reference);
|
|
26726
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return null;
|
|
26727
|
-
const initializer = declarator.init.arguments?.[0];
|
|
26728
|
-
return initializer ? initializer : null;
|
|
26729
|
-
};
|
|
26730
|
-
const isTrackerSynchronized = (analysis, guard) => {
|
|
26731
|
-
for (const statement of guard.statements) {
|
|
26732
|
-
if (guard.tracker.kind === "state") {
|
|
26733
|
-
const callExpression = getDirectCallExpression(statement);
|
|
26734
|
-
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26735
|
-
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26736
|
-
if (!setterReference || getUseStateDecl(analysis, setterReference) !== guard.tracker.declarator || (callExpression.arguments ?? []).length !== 1) continue;
|
|
26737
|
-
const writtenValue = callExpression.arguments?.[0];
|
|
26738
|
-
if (writtenValue && resolveSimpleRenderSource(analysis, writtenValue)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26739
|
-
continue;
|
|
26740
|
-
}
|
|
26741
|
-
const assignmentExpression = getDirectAssignmentExpression(statement);
|
|
26742
|
-
if (!assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression")) continue;
|
|
26743
|
-
if (getStaticRefCurrentReference(analysis, assignmentExpression.left)?.resolved !== guard.tracker.reference.resolved) continue;
|
|
26744
|
-
if (resolveSimpleRenderSource(analysis, assignmentExpression.right)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26745
|
-
}
|
|
26746
|
-
return false;
|
|
26747
|
-
};
|
|
26748
|
-
const parseRenderTrackerGuard = (analysis, statement) => {
|
|
26749
|
-
if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isNodeOfType(statement.test, "BinaryExpression") || statement.test.operator !== "!==" || !isNodeOfType(statement.consequent, "BlockStatement")) return null;
|
|
26750
|
-
const left = statement.test.left;
|
|
26751
|
-
const right = statement.test.right;
|
|
26752
|
-
const candidates = [{
|
|
26753
|
-
sourceExpression: left,
|
|
26754
|
-
trackerExpression: right
|
|
26755
|
-
}, {
|
|
26756
|
-
sourceExpression: right,
|
|
26757
|
-
trackerExpression: left
|
|
26758
|
-
}];
|
|
26759
|
-
for (const candidate of candidates) {
|
|
26760
|
-
const source = resolveSimpleRenderSource(analysis, candidate.sourceExpression);
|
|
26761
|
-
if (!source) continue;
|
|
26762
|
-
const tracker = getStateTracker(analysis, candidate.trackerExpression) ?? getRefTracker(analysis, candidate.trackerExpression);
|
|
26763
|
-
if (!tracker) continue;
|
|
26764
|
-
const trackerInitializer = getTrackerInitializer(tracker);
|
|
26765
|
-
if (!trackerInitializer || resolveSimpleRenderSource(analysis, trackerInitializer)?.bindingIdentity !== source.bindingIdentity) continue;
|
|
26766
|
-
const guard = {
|
|
26767
|
-
source,
|
|
26768
|
-
tracker,
|
|
26769
|
-
statements: getDirectBranchStatements(statement.consequent)
|
|
26770
|
-
};
|
|
26771
|
-
return isTrackerSynchronized(analysis, guard) ? guard : null;
|
|
26772
|
-
}
|
|
26773
|
-
return null;
|
|
26774
|
-
};
|
|
26775
|
-
const isExclusiveDestinationSetterReference = (setterReference, callExpression) => {
|
|
26776
|
-
if (!setterReference.resolved || !isNodeOfType(callExpression, "CallExpression")) return false;
|
|
26777
|
-
const references = setterReference.resolved.references.filter((reference) => !reference.init);
|
|
26778
|
-
if (references.length !== 1) return false;
|
|
26779
|
-
return references[0].identifier === callExpression.callee;
|
|
26780
|
-
};
|
|
26781
|
-
const getStateInitializer = (stateDeclarator) => {
|
|
26782
|
-
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression")) return null;
|
|
26783
|
-
const initializer = stateDeclarator.init.arguments?.[0];
|
|
26784
|
-
return initializer ? initializer : null;
|
|
26785
|
-
};
|
|
26786
|
-
const collectRenderStateWriteFacts = (analysis, componentBody, currentFilename) => {
|
|
26787
|
-
if (!isNodeOfType(componentBody, "BlockStatement") || !componentBody.parent || !isFunctionLike$1(componentBody.parent)) return [];
|
|
26788
|
-
const componentFunction = componentBody.parent;
|
|
26789
|
-
const facts = [];
|
|
26790
|
-
for (const statement of componentBody.body ?? []) {
|
|
26791
|
-
const guard = parseRenderTrackerGuard(analysis, statement);
|
|
26792
|
-
if (!guard) continue;
|
|
26793
|
-
for (const branchStatement of guard.statements) {
|
|
26794
|
-
const callExpression = getDirectCallExpression(branchStatement);
|
|
26795
|
-
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26796
|
-
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26797
|
-
if (!setterReference || (callExpression.arguments ?? []).length !== 1 || !isExclusiveDestinationSetterReference(setterReference, callExpression)) continue;
|
|
26798
|
-
const stateDeclarator = getUseStateDecl(analysis, setterReference);
|
|
26799
|
-
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || guard.tracker.kind === "state" && stateDeclarator === guard.tracker.declarator) continue;
|
|
26800
|
-
const writtenValue = callExpression.arguments?.[0];
|
|
26801
|
-
const initializer = getStateInitializer(stateDeclarator);
|
|
26802
|
-
if (!writtenValue || !initializer || isFunctionLike$1(writtenValue) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, writtenValue, componentFunction, currentFilename), guard.source) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, initializer, componentFunction, currentFilename), guard.source)) continue;
|
|
26803
|
-
facts.push({
|
|
26804
|
-
callExpression,
|
|
26805
|
-
stateDeclarator
|
|
26806
|
-
});
|
|
26807
|
-
}
|
|
26808
|
-
}
|
|
26809
|
-
return facts;
|
|
26810
|
-
};
|
|
26811
|
-
//#endregion
|
|
26812
|
-
//#region src/plugin/rules/state-and-effects/no-derived-state.ts
|
|
26813
|
-
const getStateName$1 = (stateDeclarator) => {
|
|
26814
|
-
if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
|
|
26815
|
-
if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
|
|
26816
|
-
const stateBinding = stateDeclarator.id.elements?.[0];
|
|
26817
|
-
if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
|
|
26818
|
-
const setterBinding = stateDeclarator.id.elements?.[1];
|
|
26819
|
-
if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
|
|
26820
|
-
if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
|
|
26821
|
-
return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
|
|
26822
|
-
};
|
|
26823
|
-
const noDerivedState = defineRule({
|
|
26824
|
-
id: "no-derived-state",
|
|
26825
|
-
title: "Derived value copied into state",
|
|
26826
|
-
severity: "warn",
|
|
26827
|
-
tags: ["test-noise"],
|
|
26828
|
-
recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into state and synchronizing it during render or through an effect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
|
|
26829
|
-
create: (context) => {
|
|
26830
|
-
const reportStateWrite = (callExpression, stateDeclarator) => {
|
|
26831
|
-
const stateName = getStateName$1(stateDeclarator);
|
|
26832
|
-
context.report({
|
|
26833
|
-
node: callExpression,
|
|
26834
|
-
message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
|
|
26835
|
-
});
|
|
26836
|
-
};
|
|
26837
|
-
return {
|
|
26838
|
-
...createComponentPropStackTracker({ onComponentEnter: (componentBody) => {
|
|
26839
|
-
if (!componentBody) return;
|
|
26840
|
-
const analysis = getProgramAnalysis(componentBody);
|
|
26841
|
-
if (!analysis) return;
|
|
26842
|
-
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26843
|
-
} }).visitors,
|
|
26844
|
-
CallExpression(node) {
|
|
26845
|
-
if (!isUseEffect(node)) return;
|
|
26846
|
-
const analysis = getProgramAnalysis(node);
|
|
26847
|
-
if (!analysis) return;
|
|
26848
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
26849
|
-
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
26850
|
-
reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26851
|
-
}
|
|
26852
|
-
}
|
|
26853
|
-
};
|
|
26854
|
-
}
|
|
26855
|
-
});
|
|
26856
|
-
//#endregion
|
|
26857
|
-
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
26858
|
-
const noDerivedStateEffect = defineRule({
|
|
26859
|
-
id: "no-derived-state-effect",
|
|
26860
|
-
title: "Derived state stored in an effect",
|
|
26861
|
-
severity: "warn",
|
|
26862
|
-
tags: ["test-noise"],
|
|
26863
|
-
recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
26864
|
-
create: (context) => ({ CallExpression(node) {
|
|
26865
|
-
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
26866
|
-
const analysis = getProgramAnalysis(node);
|
|
26867
|
-
if (!analysis) return;
|
|
26868
|
-
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
26869
|
-
context.report({
|
|
26870
|
-
node,
|
|
26871
|
-
message: "You pay an extra render for state you can derive from other values."
|
|
26872
|
-
});
|
|
26873
|
-
} })
|
|
26874
|
-
});
|
|
26875
|
-
//#endregion
|
|
26876
25966
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
26877
25967
|
const isInitialOnlyPropName = (propName) => {
|
|
26878
25968
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -28378,14 +27468,14 @@ const hasDocumentClassListMutation = (node) => {
|
|
|
28378
27468
|
//#endregion
|
|
28379
27469
|
//#region src/plugin/rules/state-and-effects/no-effect-event-handler.ts
|
|
28380
27470
|
const hasEventLikeNode = (node) => findTriggeredSideEffectCalleeName(node) !== null || hasDocumentClassListMutation(node);
|
|
28381
|
-
const unwrapChainExpression$
|
|
27471
|
+
const unwrapChainExpression$1 = (node) => {
|
|
28382
27472
|
if (!node) return null;
|
|
28383
27473
|
if (isNodeOfType(node, "ChainExpression")) return node.expression;
|
|
28384
27474
|
return node;
|
|
28385
27475
|
};
|
|
28386
27476
|
const collectGuardExpressions = (node, into) => {
|
|
28387
27477
|
if (!node) return;
|
|
28388
|
-
const unwrappedNode = unwrapChainExpression$
|
|
27478
|
+
const unwrappedNode = unwrapChainExpression$1(node);
|
|
28389
27479
|
if (!unwrappedNode) return;
|
|
28390
27480
|
const rootIdentifierName = getRootIdentifierName(unwrappedNode);
|
|
28391
27481
|
if (rootIdentifierName) {
|
|
@@ -28416,7 +27506,7 @@ const isReturnOnlyStatement = (node) => {
|
|
|
28416
27506
|
};
|
|
28417
27507
|
const hasEventLikeRemainingStatements = (statements) => statements.some((statement) => !isNodeOfType(statement, "ReturnStatement") && hasEventLikeNode(statement));
|
|
28418
27508
|
const doesGuardMatchDependency = (guardExpression, dependencyExpression) => {
|
|
28419
|
-
const unwrappedDependencyExpression = unwrapChainExpression$
|
|
27509
|
+
const unwrappedDependencyExpression = unwrapChainExpression$1(dependencyExpression);
|
|
28420
27510
|
if (!unwrappedDependencyExpression) return false;
|
|
28421
27511
|
if (areExpressionsStructurallyEqual(guardExpression.expression, unwrappedDependencyExpression)) return true;
|
|
28422
27512
|
return isNodeOfType(unwrappedDependencyExpression, "Identifier") && unwrappedDependencyExpression.name === guardExpression.rootIdentifierName;
|
|
@@ -30475,405 +29565,6 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
30475
29565
|
} })
|
|
30476
29566
|
});
|
|
30477
29567
|
//#endregion
|
|
30478
|
-
//#region src/plugin/utils/find-enclosing-jsx-opening-element.ts
|
|
30479
|
-
const findEnclosingJsxOpeningElement = (node) => {
|
|
30480
|
-
let cursor = node.parent;
|
|
30481
|
-
while (cursor) {
|
|
30482
|
-
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
30483
|
-
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
30484
|
-
cursor = cursor.parent ?? null;
|
|
30485
|
-
}
|
|
30486
|
-
return null;
|
|
30487
|
-
};
|
|
30488
|
-
//#endregion
|
|
30489
|
-
//#region src/plugin/utils/has-client-render-evidence.ts
|
|
30490
|
-
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
30491
|
-
if (fileHasUseClientDirective) return true;
|
|
30492
|
-
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
30493
|
-
if (displayName && isReactHookName(displayName)) return true;
|
|
30494
|
-
let callsHook = false;
|
|
30495
|
-
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
30496
|
-
if (callsHook) return false;
|
|
30497
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
30498
|
-
callsHook = true;
|
|
30499
|
-
return false;
|
|
30500
|
-
}
|
|
30501
|
-
});
|
|
30502
|
-
return callsHook;
|
|
30503
|
-
};
|
|
30504
|
-
//#endregion
|
|
30505
|
-
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
30506
|
-
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
30507
|
-
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
30508
|
-
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
30509
|
-
return false;
|
|
30510
|
-
};
|
|
30511
|
-
//#endregion
|
|
30512
|
-
//#region src/plugin/utils/read-initial-state-boolean.ts
|
|
30513
|
-
const readLogicalResult = (operator, leftResult, rightResult) => {
|
|
30514
|
-
if (operator === "&&") {
|
|
30515
|
-
if (leftResult === false || rightResult === false) return false;
|
|
30516
|
-
if (leftResult === true && rightResult === true) return true;
|
|
30517
|
-
return null;
|
|
30518
|
-
}
|
|
30519
|
-
if (leftResult === true || rightResult === true) return true;
|
|
30520
|
-
if (leftResult === false && rightResult === false) return false;
|
|
30521
|
-
return null;
|
|
30522
|
-
};
|
|
30523
|
-
const readIdentifierInitialStateBoolean = (identifier, scopes, visitedBindings, allowLazyInitializer) => {
|
|
30524
|
-
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
30525
|
-
if (identifier.name === "undefined" && scopes.isGlobalReference(identifier)) return false;
|
|
30526
|
-
const binding = findVariableInitializer(identifier, identifier.name);
|
|
30527
|
-
if (!binding || visitedBindings.has(binding.bindingIdentifier)) return null;
|
|
30528
|
-
visitedBindings.add(binding.bindingIdentifier);
|
|
30529
|
-
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
30530
|
-
if (!declarator?.init || scopes.symbolFor(identifier)?.declarationNode !== declarator) return null;
|
|
30531
|
-
const initializer = stripParenExpression(declarator.init);
|
|
30532
|
-
if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements?.[0] === binding.bindingIdentifier && isReactApiCall(initializer, "useState", scopes, { allowGlobalReactNamespace: true }) && isNodeOfType(initializer, "CallExpression")) {
|
|
30533
|
-
const initialState = initializer.arguments?.[0];
|
|
30534
|
-
if (!initialState) return false;
|
|
30535
|
-
if (initialState.type === "SpreadElement") return null;
|
|
30536
|
-
return readInitialStateBooleanInternal(initialState, scopes, visitedBindings, true);
|
|
30537
|
-
}
|
|
30538
|
-
const declaration = declarator.parent;
|
|
30539
|
-
if (!isNodeOfType(declarator.id, "Identifier") || declarator.id !== binding.bindingIdentifier || !isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const") return null;
|
|
30540
|
-
return readInitialStateBooleanInternal(initializer, scopes, visitedBindings, allowLazyInitializer);
|
|
30541
|
-
};
|
|
30542
|
-
const readInitialStateBooleanInternal = (expression, scopes, visitedBindings, allowLazyInitializer) => {
|
|
30543
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
30544
|
-
if (isNodeOfType(unwrappedExpression, "Literal")) return Boolean(unwrappedExpression.value);
|
|
30545
|
-
if (isNodeOfType(unwrappedExpression, "Identifier")) return readIdentifierInitialStateBoolean(unwrappedExpression, scopes, visitedBindings, allowLazyInitializer);
|
|
30546
|
-
if (allowLazyInitializer && (isNodeOfType(unwrappedExpression, "ArrowFunctionExpression") || isNodeOfType(unwrappedExpression, "FunctionExpression"))) {
|
|
30547
|
-
if (unwrappedExpression.async || isNodeOfType(unwrappedExpression, "FunctionExpression") && unwrappedExpression.generator) return null;
|
|
30548
|
-
if (!isNodeOfType(unwrappedExpression.body, "BlockStatement")) return readInitialStateBooleanInternal(unwrappedExpression.body, scopes, visitedBindings, false);
|
|
30549
|
-
if (unwrappedExpression.body.body.length !== 1) return null;
|
|
30550
|
-
const returnStatement = unwrappedExpression.body.body[0];
|
|
30551
|
-
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return null;
|
|
30552
|
-
return readInitialStateBooleanInternal(returnStatement.argument, scopes, visitedBindings, false);
|
|
30553
|
-
}
|
|
30554
|
-
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
30555
|
-
const argumentResult = readInitialStateBooleanInternal(unwrappedExpression.argument, scopes, visitedBindings, false);
|
|
30556
|
-
return argumentResult === null ? null : !argumentResult;
|
|
30557
|
-
}
|
|
30558
|
-
if (isNodeOfType(unwrappedExpression, "LogicalExpression") && (unwrappedExpression.operator === "&&" || unwrappedExpression.operator === "||")) return readLogicalResult(unwrappedExpression.operator, readInitialStateBooleanInternal(unwrappedExpression.left, scopes, new Set(visitedBindings), false), readInitialStateBooleanInternal(unwrappedExpression.right, scopes, new Set(visitedBindings), false));
|
|
30559
|
-
return null;
|
|
30560
|
-
};
|
|
30561
|
-
const readInitialStateBoolean = (expression, scopes) => readInitialStateBooleanInternal(expression, scopes, /* @__PURE__ */ new Set(), false);
|
|
30562
|
-
//#endregion
|
|
30563
|
-
//#region src/plugin/utils/is-after-client-only-early-return.ts
|
|
30564
|
-
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode, scopes) => {
|
|
30565
|
-
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
30566
|
-
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
30567
|
-
const ancestors = /* @__PURE__ */ new Set();
|
|
30568
|
-
let currentNode = node;
|
|
30569
|
-
while (currentNode) {
|
|
30570
|
-
ancestors.add(currentNode);
|
|
30571
|
-
currentNode = currentNode.parent ?? null;
|
|
30572
|
-
}
|
|
30573
|
-
for (const statement of body.body ?? []) {
|
|
30574
|
-
if (ancestors.has(statement)) return false;
|
|
30575
|
-
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
30576
|
-
const initialConditionResult = readInitialStateBoolean(statement.test, scopes);
|
|
30577
|
-
if (initialConditionResult === true && statementAlwaysExits(statement.consequent)) return true;
|
|
30578
|
-
if (initialConditionResult === false && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
|
|
30579
|
-
}
|
|
30580
|
-
return false;
|
|
30581
|
-
};
|
|
30582
|
-
//#endregion
|
|
30583
|
-
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
30584
|
-
const isGatedByFalsyInitialState = (node, scopes) => {
|
|
30585
|
-
let cursor = node;
|
|
30586
|
-
let parent = node.parent;
|
|
30587
|
-
while (parent) {
|
|
30588
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === cursor && (parent.operator === "&&" && readInitialStateBoolean(parent.left, scopes) === false || parent.operator === "||" && readInitialStateBoolean(parent.left, scopes) === true)) return true;
|
|
30589
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor && readInitialStateBoolean(parent.test, scopes) === false || parent.alternate === cursor && readInitialStateBoolean(parent.test, scopes) === true)) return true;
|
|
30590
|
-
if (isNodeOfType(parent, "IfStatement") && (parent.consequent === cursor && readInitialStateBoolean(parent.test, scopes) === false || parent.alternate === cursor && readInitialStateBoolean(parent.test, scopes) === true)) return true;
|
|
30591
|
-
cursor = parent;
|
|
30592
|
-
parent = parent.parent ?? null;
|
|
30593
|
-
}
|
|
30594
|
-
return false;
|
|
30595
|
-
};
|
|
30596
|
-
//#endregion
|
|
30597
|
-
//#region src/plugin/rules/performance/no-hydration-branch-on-browser-global.ts
|
|
30598
|
-
const evaluateEquality = (operator, left, right) => {
|
|
30599
|
-
if (operator === "===" || operator === "==") return left === right;
|
|
30600
|
-
if (operator === "!==" || operator === "!=") return left !== right;
|
|
30601
|
-
return null;
|
|
30602
|
-
};
|
|
30603
|
-
const readTypeofBrowserGlobal = (expression, context) => {
|
|
30604
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
30605
|
-
if (!isNodeOfType(unwrappedExpression, "UnaryExpression") || unwrappedExpression.operator !== "typeof") return null;
|
|
30606
|
-
const argument = stripParenExpression(unwrappedExpression.argument);
|
|
30607
|
-
if (isNodeOfType(argument, "Identifier")) return (argument.name === "window" || argument.name === "document") && context.scopes.isGlobalReference(argument) ? argument.name : null;
|
|
30608
|
-
if (!isNodeOfType(argument, "MemberExpression") || argument.computed || !isNodeOfType(argument.object, "Identifier") || argument.object.name !== "globalThis" || !context.scopes.isGlobalReference(argument.object) || !isNodeOfType(argument.property, "Identifier") || argument.property.name !== "window" && argument.property.name !== "document") return null;
|
|
30609
|
-
return argument.property.name;
|
|
30610
|
-
};
|
|
30611
|
-
const matchBrowserPredicate = (expression, context) => {
|
|
30612
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
30613
|
-
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
30614
|
-
const innerMatch = matchBrowserPredicate(unwrappedExpression.argument, context);
|
|
30615
|
-
return innerMatch ? {
|
|
30616
|
-
browserGlobalName: innerMatch.browserGlobalName,
|
|
30617
|
-
clientResult: !innerMatch.clientResult,
|
|
30618
|
-
serverResult: !innerMatch.serverResult
|
|
30619
|
-
} : null;
|
|
30620
|
-
}
|
|
30621
|
-
if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
|
|
30622
|
-
const leftGlobalName = readTypeofBrowserGlobal(unwrappedExpression.left, context);
|
|
30623
|
-
const rightGlobalName = readTypeofBrowserGlobal(unwrappedExpression.right, context);
|
|
30624
|
-
const leftString = isNodeOfType(unwrappedExpression.left, "Literal") ? unwrappedExpression.left.value : null;
|
|
30625
|
-
const rightString = isNodeOfType(unwrappedExpression.right, "Literal") ? unwrappedExpression.right.value : null;
|
|
30626
|
-
const browserGlobalName = leftGlobalName && typeof rightString === "string" ? leftGlobalName : rightGlobalName && typeof leftString === "string" ? rightGlobalName : null;
|
|
30627
|
-
const comparedType = leftGlobalName && typeof rightString === "string" ? rightString : rightGlobalName && typeof leftString === "string" ? leftString : null;
|
|
30628
|
-
if (!browserGlobalName || !comparedType) return null;
|
|
30629
|
-
const clientResult = evaluateEquality(unwrappedExpression.operator, "object", comparedType);
|
|
30630
|
-
const serverResult = evaluateEquality(unwrappedExpression.operator, "undefined", comparedType);
|
|
30631
|
-
if (clientResult === null || serverResult === null || clientResult === serverResult) return null;
|
|
30632
|
-
return {
|
|
30633
|
-
browserGlobalName,
|
|
30634
|
-
clientResult,
|
|
30635
|
-
serverResult
|
|
30636
|
-
};
|
|
30637
|
-
};
|
|
30638
|
-
const readLogicalConditionResult = (operator, leftResult, rightResult) => {
|
|
30639
|
-
if (operator === "&&") {
|
|
30640
|
-
if (leftResult === false || rightResult === false) return false;
|
|
30641
|
-
if (leftResult === true && rightResult === true) return true;
|
|
30642
|
-
return null;
|
|
30643
|
-
}
|
|
30644
|
-
if (leftResult === true || rightResult === true) return true;
|
|
30645
|
-
if (leftResult === false && rightResult === false) return false;
|
|
30646
|
-
return null;
|
|
30647
|
-
};
|
|
30648
|
-
const readHydrationConditionResult = (expression, context, runtime) => {
|
|
30649
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
30650
|
-
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
30651
|
-
if (predicateMatch) return predicateMatch[`${runtime}Result`];
|
|
30652
|
-
const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
|
|
30653
|
-
if (staticResult !== null) return staticResult;
|
|
30654
|
-
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
30655
|
-
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
|
|
30656
|
-
return argumentResult === null ? null : !argumentResult;
|
|
30657
|
-
}
|
|
30658
|
-
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
30659
|
-
return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime), readHydrationConditionResult(unwrappedExpression.right, context, runtime));
|
|
30660
|
-
};
|
|
30661
|
-
const matchHydrationCondition = (expression, context) => {
|
|
30662
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
30663
|
-
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
30664
|
-
if (predicateMatch) return {
|
|
30665
|
-
predicateMatch,
|
|
30666
|
-
predicateNode: unwrappedExpression
|
|
30667
|
-
};
|
|
30668
|
-
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationCondition(unwrappedExpression.argument, context);
|
|
30669
|
-
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
30670
|
-
const leftMatch = matchHydrationCondition(unwrappedExpression.left, context);
|
|
30671
|
-
const rightMatch = matchHydrationCondition(unwrappedExpression.right, context);
|
|
30672
|
-
if (leftMatch && rightMatch) {
|
|
30673
|
-
const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client");
|
|
30674
|
-
const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server");
|
|
30675
|
-
return clientResult !== null && serverResult !== null && clientResult !== serverResult ? leftMatch : null;
|
|
30676
|
-
}
|
|
30677
|
-
const nestedMatch = leftMatch ?? rightMatch;
|
|
30678
|
-
if (!nestedMatch) return null;
|
|
30679
|
-
const otherResult = readInitialStateBoolean(leftMatch ? unwrappedExpression.right : unwrappedExpression.left, context.scopes);
|
|
30680
|
-
if (unwrappedExpression.operator === "&&" && otherResult === false || unwrappedExpression.operator === "||" && otherResult === true) return null;
|
|
30681
|
-
return nestedMatch;
|
|
30682
|
-
};
|
|
30683
|
-
const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
|
|
30684
|
-
const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
|
|
30685
|
-
if (!leftNode || !rightNode) return leftNode === rightNode;
|
|
30686
|
-
const left = stripParenExpression(leftNode);
|
|
30687
|
-
const right = stripParenExpression(rightNode);
|
|
30688
|
-
if (areExpressionsStructurallyEqual(left, right)) return true;
|
|
30689
|
-
if (left.type !== right.type) return false;
|
|
30690
|
-
if (isNodeOfType(left, "JSXText") && isNodeOfType(right, "JSXText")) return left.value === right.value;
|
|
30691
|
-
if (isNodeOfType(left, "JSXExpressionContainer") && isNodeOfType(right, "JSXExpressionContainer")) {
|
|
30692
|
-
if (!isAstNode(left.expression) || !isAstNode(right.expression)) return left.expression.type === right.expression.type;
|
|
30693
|
-
return areRenderedBranchesEquivalent(left.expression, right.expression);
|
|
30694
|
-
}
|
|
30695
|
-
if (isNodeOfType(left, "JSXElement") && isNodeOfType(right, "JSXElement")) {
|
|
30696
|
-
if (flattenJsxName$1(left.openingElement.name) !== flattenJsxName$1(right.openingElement.name)) return false;
|
|
30697
|
-
if (!areNodeArraysEquivalent(left.openingElement.attributes, right.openingElement.attributes)) return false;
|
|
30698
|
-
return areNodeArraysEquivalent(left.children, right.children);
|
|
30699
|
-
}
|
|
30700
|
-
if (isNodeOfType(left, "JSXFragment") && isNodeOfType(right, "JSXFragment")) return areNodeArraysEquivalent(left.children, right.children);
|
|
30701
|
-
if (isNodeOfType(left, "JSXAttribute") && isNodeOfType(right, "JSXAttribute")) {
|
|
30702
|
-
if (flattenJsxName$1(left.name) !== flattenJsxName$1(right.name)) return false;
|
|
30703
|
-
return areRenderedBranchesEquivalent(left.value, right.value);
|
|
30704
|
-
}
|
|
30705
|
-
if (isNodeOfType(left, "JSXSpreadAttribute") && isNodeOfType(right, "JSXSpreadAttribute")) return areRenderedBranchesEquivalent(left.argument, right.argument);
|
|
30706
|
-
if (isNodeOfType(left, "TemplateLiteral") && isNodeOfType(right, "TemplateLiteral")) {
|
|
30707
|
-
if (left.quasis.length !== right.quasis.length) return false;
|
|
30708
|
-
if (!left.quasis.every((quasi, index) => quasi.value.cooked === right.quasis[index]?.value.cooked && quasi.value.raw === right.quasis[index]?.value.raw)) return false;
|
|
30709
|
-
return areNodeArraysEquivalent(left.expressions, right.expressions);
|
|
30710
|
-
}
|
|
30711
|
-
return false;
|
|
30712
|
-
};
|
|
30713
|
-
const isRenderedValue = (node) => {
|
|
30714
|
-
const unwrappedNode = stripParenExpression(node);
|
|
30715
|
-
if (isNodeOfType(unwrappedNode, "Literal")) return unwrappedNode.value !== null && unwrappedNode.value !== true && unwrappedNode.value !== false && unwrappedNode.value !== "";
|
|
30716
|
-
if (isNodeOfType(unwrappedNode, "TemplateLiteral")) return unwrappedNode.expressions.length > 0 || unwrappedNode.quasis[0]?.value.cooked !== "";
|
|
30717
|
-
return isNodeOfType(unwrappedNode, "JSXElement") || isNodeOfType(unwrappedNode, "JSXFragment");
|
|
30718
|
-
};
|
|
30719
|
-
const findRenderedValueInAndBranch = (node) => {
|
|
30720
|
-
const unwrappedNode = stripParenExpression(node);
|
|
30721
|
-
if (isRenderedValue(unwrappedNode)) return unwrappedNode;
|
|
30722
|
-
if (!isNodeOfType(unwrappedNode, "LogicalExpression") || unwrappedNode.operator !== "&&") return null;
|
|
30723
|
-
return findRenderedValueInAndBranch(unwrappedNode.right);
|
|
30724
|
-
};
|
|
30725
|
-
const findEnclosingJsxAttribute = (node) => {
|
|
30726
|
-
let currentNode = node.parent;
|
|
30727
|
-
while (currentNode) {
|
|
30728
|
-
if (isNodeOfType(currentNode, "JSXAttribute")) return currentNode;
|
|
30729
|
-
if (isNodeOfType(currentNode, "JSXElement") || isNodeOfType(currentNode, "JSXFragment") || isFunctionLike$1(currentNode)) return null;
|
|
30730
|
-
currentNode = currentNode.parent;
|
|
30731
|
-
}
|
|
30732
|
-
return null;
|
|
30733
|
-
};
|
|
30734
|
-
const isInRenderedOutput = (node, componentOrHookNode, scopes) => {
|
|
30735
|
-
let currentNode = node;
|
|
30736
|
-
let parentNode = currentNode.parent;
|
|
30737
|
-
while (parentNode) {
|
|
30738
|
-
if (isNodeOfType(parentNode, "JSXExpressionContainer")) {
|
|
30739
|
-
const attribute = findEnclosingJsxAttribute(parentNode);
|
|
30740
|
-
return attribute ? !isEventHandlerAttribute(attribute) : true;
|
|
30741
|
-
}
|
|
30742
|
-
if (isNodeOfType(parentNode, "ReturnStatement")) {
|
|
30743
|
-
if (findEnclosingFunction(parentNode) === componentOrHookNode) return true;
|
|
30744
|
-
}
|
|
30745
|
-
if (parentNode === componentOrHookNode) return isFunctionLike$1(componentOrHookNode) && !isNodeOfType(componentOrHookNode.body, "BlockStatement") && componentOrHookNode.body === currentNode;
|
|
30746
|
-
if (isFunctionLike$1(parentNode) && !executesDuringRender(parentNode, scopes)) return false;
|
|
30747
|
-
currentNode = parentNode;
|
|
30748
|
-
parentNode = currentNode.parent;
|
|
30749
|
-
}
|
|
30750
|
-
return false;
|
|
30751
|
-
};
|
|
30752
|
-
const getReturnedValues = (statement) => {
|
|
30753
|
-
if (!statement) return [];
|
|
30754
|
-
if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? [statement.argument] : [];
|
|
30755
|
-
if (isNodeOfType(statement, "IfStatement")) return [...getReturnedValues(statement.consequent), ...getReturnedValues(statement.alternate)];
|
|
30756
|
-
if (!isNodeOfType(statement, "BlockStatement")) return [];
|
|
30757
|
-
const returnedValues = [];
|
|
30758
|
-
for (const childStatement of statement.body) {
|
|
30759
|
-
returnedValues.push(...getReturnedValues(childStatement));
|
|
30760
|
-
if (statementAlwaysExits(childStatement)) break;
|
|
30761
|
-
}
|
|
30762
|
-
return returnedValues;
|
|
30763
|
-
};
|
|
30764
|
-
const findFollowingReturnedValues = (ifStatement) => {
|
|
30765
|
-
const parentNode = ifStatement.parent;
|
|
30766
|
-
if (!isNodeOfType(parentNode, "BlockStatement")) return [];
|
|
30767
|
-
const statementIndex = parentNode.body.findIndex((statement) => statement === ifStatement);
|
|
30768
|
-
if (statementIndex < 0) return [];
|
|
30769
|
-
const returnedValues = [];
|
|
30770
|
-
for (const statement of parentNode.body.slice(statementIndex + 1)) {
|
|
30771
|
-
returnedValues.push(...getReturnedValues(statement));
|
|
30772
|
-
if (statementAlwaysExits(statement)) break;
|
|
30773
|
-
}
|
|
30774
|
-
return returnedValues;
|
|
30775
|
-
};
|
|
30776
|
-
const areConditionExpressionsEquivalent = (leftExpression, rightExpression) => {
|
|
30777
|
-
const left = stripParenExpression(leftExpression);
|
|
30778
|
-
const right = stripParenExpression(rightExpression);
|
|
30779
|
-
if (areExpressionsStructurallyEqual(left, right)) return true;
|
|
30780
|
-
if (left.type !== right.type) return false;
|
|
30781
|
-
if (isNodeOfType(left, "UnaryExpression") && isNodeOfType(right, "UnaryExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.argument, right.argument);
|
|
30782
|
-
if (isNodeOfType(left, "LogicalExpression") && isNodeOfType(right, "LogicalExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.left, right.left) && areConditionExpressionsEquivalent(left.right, right.right);
|
|
30783
|
-
if (isNodeOfType(left, "BinaryExpression") && isNodeOfType(right, "BinaryExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.left, right.left) && areConditionExpressionsEquivalent(left.right, right.right);
|
|
30784
|
-
return false;
|
|
30785
|
-
};
|
|
30786
|
-
const areReturnTreesEquivalent = (leftStatement, rightStatement) => {
|
|
30787
|
-
if (!leftStatement || !rightStatement) return leftStatement === rightStatement;
|
|
30788
|
-
if (isNodeOfType(leftStatement, "ReturnStatement") && isNodeOfType(rightStatement, "ReturnStatement")) return areRenderedBranchesEquivalent(leftStatement.argument, rightStatement.argument);
|
|
30789
|
-
if (isNodeOfType(leftStatement, "IfStatement") && isNodeOfType(rightStatement, "IfStatement")) return areConditionExpressionsEquivalent(leftStatement.test, rightStatement.test) && areReturnTreesEquivalent(leftStatement.consequent, rightStatement.consequent) && areReturnTreesEquivalent(leftStatement.alternate, rightStatement.alternate);
|
|
30790
|
-
if (!isNodeOfType(leftStatement, "BlockStatement") || !isNodeOfType(rightStatement, "BlockStatement")) return false;
|
|
30791
|
-
const leftReturningStatements = leftStatement.body.filter((statement) => getReturnedValues(statement).length > 0);
|
|
30792
|
-
const rightReturningStatements = rightStatement.body.filter((statement) => getReturnedValues(statement).length > 0);
|
|
30793
|
-
return leftReturningStatements.length === rightReturningStatements.length && leftReturningStatements.every((statement, index) => areReturnTreesEquivalent(statement, rightReturningStatements[index]));
|
|
30794
|
-
};
|
|
30795
|
-
const isStructuralRenderedValue = (node) => {
|
|
30796
|
-
if (!node) return false;
|
|
30797
|
-
const unwrappedNode = stripParenExpression(node);
|
|
30798
|
-
return isNodeOfType(unwrappedNode, "JSXElement") || isNodeOfType(unwrappedNode, "JSXFragment");
|
|
30799
|
-
};
|
|
30800
|
-
const branchRootsSuppressSameElement = (leftBranch, rightBranch) => {
|
|
30801
|
-
if (!rightBranch) return false;
|
|
30802
|
-
const left = stripParenExpression(leftBranch);
|
|
30803
|
-
const right = stripParenExpression(rightBranch);
|
|
30804
|
-
return isNodeOfType(left, "JSXElement") && isNodeOfType(right, "JSXElement") && flattenJsxName$1(left.openingElement.name) === flattenJsxName$1(right.openingElement.name) && hasSuppressHydrationWarningAttribute(left.openingElement) && hasSuppressHydrationWarningAttribute(right.openingElement);
|
|
30805
|
-
};
|
|
30806
|
-
const noHydrationBranchOnBrowserGlobal = defineRule({
|
|
30807
|
-
id: "no-hydration-branch-on-browser-global",
|
|
30808
|
-
title: "Server and client render different branches",
|
|
30809
|
-
severity: "error",
|
|
30810
|
-
category: "Correctness",
|
|
30811
|
-
requires: ["ssr"],
|
|
30812
|
-
recommendation: "Render the same initial output on the server and client, then switch after mount or use useSyncExternalStore with a stable server snapshot.",
|
|
30813
|
-
create: (context) => {
|
|
30814
|
-
if (isTestlikeFilename(context.filename)) return {};
|
|
30815
|
-
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
30816
|
-
let fileHasUseClientDirective = false;
|
|
30817
|
-
let fileIsEmailTemplate = false;
|
|
30818
|
-
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30819
|
-
const reportHydrationBranch = (conditionNode, leftBranch, rightBranch, requiresRenderedContext) => {
|
|
30820
|
-
const conditionMatch = matchHydrationCondition(conditionNode, context);
|
|
30821
|
-
if (!conditionMatch) return;
|
|
30822
|
-
const { predicateMatch, predicateNode } = conditionMatch;
|
|
30823
|
-
if (reportedNodes.has(predicateNode)) return;
|
|
30824
|
-
if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
|
|
30825
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(predicateNode, context.scopes);
|
|
30826
|
-
if (!componentOrHookNode) return;
|
|
30827
|
-
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30828
|
-
if (requiresRenderedContext && !isInRenderedOutput(predicateNode, componentOrHookNode, context.scopes)) return;
|
|
30829
|
-
if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
|
|
30830
|
-
const attribute = findEnclosingJsxAttribute(predicateNode);
|
|
30831
|
-
if (!attribute || isEventHandlerAttribute(attribute)) return;
|
|
30832
|
-
}
|
|
30833
|
-
if (fileIsEmailTemplate || isGatedByFalsyInitialState(predicateNode, context.scopes)) return;
|
|
30834
|
-
if (isAfterClientOnlyEarlyReturn(predicateNode, componentOrHookNode, context.scopes)) return;
|
|
30835
|
-
const openingElement = findEnclosingJsxOpeningElement(predicateNode);
|
|
30836
|
-
if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
|
|
30837
|
-
if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
|
|
30838
|
-
if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
|
|
30839
|
-
reportedNodes.add(predicateNode);
|
|
30840
|
-
context.report({
|
|
30841
|
-
node: predicateNode,
|
|
30842
|
-
message: `\`typeof ${predicateMatch.browserGlobalName}\` selects different rendered output on the server and during hydration. Render the same initial output, then switch after mount.`
|
|
30843
|
-
});
|
|
30844
|
-
};
|
|
30845
|
-
return {
|
|
30846
|
-
Program(node) {
|
|
30847
|
-
fileHasUseClientDirective = hasDirective(node, "use client");
|
|
30848
|
-
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
30849
|
-
},
|
|
30850
|
-
ConditionalExpression(node) {
|
|
30851
|
-
reportHydrationBranch(node.test, node.consequent, node.alternate, true);
|
|
30852
|
-
},
|
|
30853
|
-
LogicalExpression(node) {
|
|
30854
|
-
if (node.operator !== "&&" && node.operator !== "||") return;
|
|
30855
|
-
const renderedValue = node.operator === "&&" ? findRenderedValueInAndBranch(node.right) : isRenderedValue(node.right) ? node.right : null;
|
|
30856
|
-
if (!renderedValue) return;
|
|
30857
|
-
reportHydrationBranch(node, renderedValue, null, true);
|
|
30858
|
-
},
|
|
30859
|
-
IfStatement(node) {
|
|
30860
|
-
if (node.alternate && areReturnTreesEquivalent(node.consequent, node.alternate)) return;
|
|
30861
|
-
const consequentValues = getReturnedValues(node.consequent);
|
|
30862
|
-
const alternateValues = node.alternate ? getReturnedValues(node.alternate) : findFollowingReturnedValues(node);
|
|
30863
|
-
if (consequentValues.length === 0 || alternateValues.length === 0) return;
|
|
30864
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(node.test, context.scopes);
|
|
30865
|
-
if (!componentOrHookNode) return;
|
|
30866
|
-
const enclosingFunction = findEnclosingFunction(node);
|
|
30867
|
-
if (enclosingFunction !== componentOrHookNode && (!enclosingFunction || !isInRenderedOutput(enclosingFunction, componentOrHookNode, context.scopes))) return;
|
|
30868
|
-
for (const consequentValue of consequentValues) for (const alternateValue of alternateValues) {
|
|
30869
|
-
if (!isRenderedValue(consequentValue) && !isRenderedValue(alternateValue)) continue;
|
|
30870
|
-
reportHydrationBranch(node.test, consequentValue, alternateValue, false);
|
|
30871
|
-
}
|
|
30872
|
-
}
|
|
30873
|
-
};
|
|
30874
|
-
}
|
|
30875
|
-
});
|
|
30876
|
-
//#endregion
|
|
30877
29568
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
30878
29569
|
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.";
|
|
30879
29570
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
@@ -30894,131 +29585,6 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
30894
29585
|
} })
|
|
30895
29586
|
});
|
|
30896
29587
|
//#endregion
|
|
30897
|
-
//#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
|
|
30898
|
-
const TIMER_FUNCTION_NAMES = new Set([
|
|
30899
|
-
"cancelAnimationFrame",
|
|
30900
|
-
"clearInterval",
|
|
30901
|
-
"clearTimeout",
|
|
30902
|
-
"queueMicrotask",
|
|
30903
|
-
"requestAnimationFrame",
|
|
30904
|
-
"setInterval",
|
|
30905
|
-
"setTimeout"
|
|
30906
|
-
]);
|
|
30907
|
-
const STORAGE_MUTATION_METHOD_NAMES = new Set([
|
|
30908
|
-
"clear",
|
|
30909
|
-
"removeItem",
|
|
30910
|
-
"setItem"
|
|
30911
|
-
]);
|
|
30912
|
-
const STORAGE_RECEIVER_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
30913
|
-
const EXTERNAL_READ_METHOD_NAMES = new Set(["getBoundingClientRect", "getClientRects"]);
|
|
30914
|
-
const NOTIFICATION_RECEIVER_NAMES = new Set([
|
|
30915
|
-
"message",
|
|
30916
|
-
"notification",
|
|
30917
|
-
"toast"
|
|
30918
|
-
]);
|
|
30919
|
-
const NOTIFICATION_METHOD_NAMES = new Set([
|
|
30920
|
-
"error",
|
|
30921
|
-
"info",
|
|
30922
|
-
"loading",
|
|
30923
|
-
"open",
|
|
30924
|
-
"show",
|
|
30925
|
-
"success",
|
|
30926
|
-
"warning"
|
|
30927
|
-
]);
|
|
30928
|
-
const getMemberCall = (node) => {
|
|
30929
|
-
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
30930
|
-
if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
|
|
30931
|
-
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
30932
|
-
return {
|
|
30933
|
-
methodName: node.callee.property.name,
|
|
30934
|
-
receiver: stripParenExpression(node.callee.object)
|
|
30935
|
-
};
|
|
30936
|
-
};
|
|
30937
|
-
const isNotificationReceiver = (receiver, scopes) => {
|
|
30938
|
-
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
30939
|
-
if (!NOTIFICATION_RECEIVER_NAMES.has(receiver.name)) return false;
|
|
30940
|
-
const symbol = scopes.symbolFor(receiver);
|
|
30941
|
-
if (symbol?.kind === "import") return true;
|
|
30942
|
-
if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
|
|
30943
|
-
const callee = symbol.initializer.callee;
|
|
30944
|
-
return isNodeOfType(callee, "Identifier") && /^use(?:Message|Notification|Toast)$/.test(callee.name);
|
|
30945
|
-
};
|
|
30946
|
-
const getKnownImpureCall = (callExpression, scopes) => {
|
|
30947
|
-
if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
|
|
30948
|
-
const memberCall = getMemberCall(callExpression);
|
|
30949
|
-
if (!memberCall) return null;
|
|
30950
|
-
const { methodName, receiver } = memberCall;
|
|
30951
|
-
if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && STORAGE_RECEIVER_NAMES.has(receiver.name) && scopes.isGlobalReference(receiver)) return `${receiver.name}.${methodName}()`;
|
|
30952
|
-
if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
|
|
30953
|
-
if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && isNotificationReceiver(receiver, scopes)) return `${receiver.name}.${methodName}()`;
|
|
30954
|
-
return null;
|
|
30955
|
-
};
|
|
30956
|
-
const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
|
|
30957
|
-
let rootIdentifier = null;
|
|
30958
|
-
if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
|
|
30959
|
-
else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
|
|
30960
|
-
if (!rootIdentifier) return null;
|
|
30961
|
-
const updaterScope = scopes.ownScopeFor(updater);
|
|
30962
|
-
if (!updaterScope) return null;
|
|
30963
|
-
const symbol = scopes.symbolFor(rootIdentifier);
|
|
30964
|
-
if (!symbol) return `the external value "${rootIdentifier.name}"`;
|
|
30965
|
-
if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
|
|
30966
|
-
return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
|
|
30967
|
-
};
|
|
30968
|
-
const findImpureUpdaterOperation = (updater, scopes) => {
|
|
30969
|
-
const analysis = getProgramAnalysis(updater);
|
|
30970
|
-
let operation = null;
|
|
30971
|
-
walkAst(updater, (child) => {
|
|
30972
|
-
if (operation) return false;
|
|
30973
|
-
if (child !== updater && isFunctionLike$1(child) && !executesDuringRender(child, scopes)) return false;
|
|
30974
|
-
if (isNodeOfType(child, "CallExpression")) {
|
|
30975
|
-
if (isNodeOfType(child.callee, "Identifier") && analysis) {
|
|
30976
|
-
const calleeReference = getRef(analysis, child.callee);
|
|
30977
|
-
if (calleeReference && isStateSetterCall(analysis, calleeReference)) {
|
|
30978
|
-
operation = `the nested state update "${child.callee.name}()"`;
|
|
30979
|
-
return false;
|
|
30980
|
-
}
|
|
30981
|
-
}
|
|
30982
|
-
const impureCall = getKnownImpureCall(child, scopes);
|
|
30983
|
-
if (impureCall) {
|
|
30984
|
-
operation = impureCall;
|
|
30985
|
-
return false;
|
|
30986
|
-
}
|
|
30987
|
-
}
|
|
30988
|
-
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
30989
|
-
operation = getExternalAssignmentDescription(child.left, updater, scopes);
|
|
30990
|
-
if (operation) return false;
|
|
30991
|
-
}
|
|
30992
|
-
if (isNodeOfType(child, "UpdateExpression")) {
|
|
30993
|
-
operation = getExternalAssignmentDescription(child.argument, updater, scopes);
|
|
30994
|
-
if (operation) return false;
|
|
30995
|
-
}
|
|
30996
|
-
});
|
|
30997
|
-
return operation;
|
|
30998
|
-
};
|
|
30999
|
-
const noImpureStateUpdater = defineRule({
|
|
31000
|
-
id: "no-impure-state-updater",
|
|
31001
|
-
title: "State updater has side effects",
|
|
31002
|
-
severity: "error",
|
|
31003
|
-
recommendation: "Keep state updater callbacks pure and return only the next state. Move notifications, storage, timers, ref writes, and other external work into the event or effect that queues the update.",
|
|
31004
|
-
create: (context) => ({ CallExpression(node) {
|
|
31005
|
-
const updater = node.arguments?.[0];
|
|
31006
|
-
if (!updater || !isFunctionLike$1(updater)) return;
|
|
31007
|
-
const analysis = getProgramAnalysis(node);
|
|
31008
|
-
if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
|
|
31009
|
-
const calleeReference = getRef(analysis, node.callee);
|
|
31010
|
-
if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
|
|
31011
|
-
const stateDeclarator = getUseStateDecl(analysis, calleeReference);
|
|
31012
|
-
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
|
|
31013
|
-
const operation = findImpureUpdaterOperation(updater, context.scopes);
|
|
31014
|
-
if (!operation) return;
|
|
31015
|
-
context.report({
|
|
31016
|
-
node: updater,
|
|
31017
|
-
message: `This state updater performs ${operation}. React may run updater functions more than once, so side effects here can repeat or observe inconsistent external state.`
|
|
31018
|
-
});
|
|
31019
|
-
} })
|
|
31020
|
-
});
|
|
31021
|
-
//#endregion
|
|
31022
29588
|
//#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
|
|
31023
29589
|
const MESSAGE$21 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
|
|
31024
29590
|
const REACT_USE_REF_OPTIONS = {
|
|
@@ -31185,7 +29751,7 @@ const noInitializeState = defineRule({
|
|
|
31185
29751
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
31186
29752
|
const analysis = getProgramAnalysis(node);
|
|
31187
29753
|
if (!analysis) return;
|
|
31188
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node
|
|
29754
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
31189
29755
|
if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
|
|
31190
29756
|
const stateName = getStateName(fact.stateDeclarator);
|
|
31191
29757
|
context.report({
|
|
@@ -31844,6 +30410,111 @@ const noLegacyContextApi = defineRule({
|
|
|
31844
30410
|
}
|
|
31845
30411
|
});
|
|
31846
30412
|
//#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
|
|
31847
30518
|
//#region src/plugin/rules/performance/no-locale-format-in-render.ts
|
|
31848
30519
|
const LOCALE_FORMAT_METHOD_NAMES = new Set([
|
|
31849
30520
|
"toLocaleString",
|
|
@@ -31973,12 +30644,75 @@ const matchDateDefaultStringification = (node) => {
|
|
|
31973
30644
|
}
|
|
31974
30645
|
return null;
|
|
31975
30646
|
};
|
|
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
|
+
};
|
|
31976
30704
|
const noLocaleFormatInRender = defineRule({
|
|
31977
30705
|
id: "no-locale-format-in-render",
|
|
31978
30706
|
title: "Locale/timezone formatting during render",
|
|
31979
30707
|
severity: "warn",
|
|
31980
30708
|
category: "Correctness",
|
|
31981
|
-
|
|
30709
|
+
disabledWhen: [
|
|
30710
|
+
"vite",
|
|
30711
|
+
"cra",
|
|
30712
|
+
"expo",
|
|
30713
|
+
"react-native",
|
|
30714
|
+
"unknown"
|
|
30715
|
+
],
|
|
31982
30716
|
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.",
|
|
31983
30717
|
create: (context) => {
|
|
31984
30718
|
if (isTestlikeFilename(context.filename)) return {};
|
|
@@ -31988,12 +30722,13 @@ const noLocaleFormatInRender = defineRule({
|
|
|
31988
30722
|
const reportedNodes = /* @__PURE__ */ new Set();
|
|
31989
30723
|
const reportIfRenderPhase = (match) => {
|
|
31990
30724
|
if (reportedNodes.has(match.node)) return;
|
|
31991
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node
|
|
30725
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
|
|
31992
30726
|
if (!componentOrHookNode) return;
|
|
31993
30727
|
if (fileIsEmailTemplate) return;
|
|
31994
30728
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
31995
|
-
if (
|
|
31996
|
-
if (
|
|
30729
|
+
if (isInsideClientOnlyGuard(match.node)) return;
|
|
30730
|
+
if (isGatedByFalsyInitialState(match.node)) return;
|
|
30731
|
+
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
|
|
31997
30732
|
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
|
|
31998
30733
|
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
|
|
31999
30734
|
reportedNodes.add(match.node);
|
|
@@ -32020,7 +30755,7 @@ const noLocaleFormatInRender = defineRule({
|
|
|
32020
30755
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
32021
30756
|
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
32022
30757
|
const helperName = expression.callee.name;
|
|
32023
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(node
|
|
30758
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node);
|
|
32024
30759
|
if (!componentOrHookNode) return;
|
|
32025
30760
|
const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
|
|
32026
30761
|
if (!helperNode || !isFunctionLike$1(helperNode)) return;
|
|
@@ -32032,8 +30767,9 @@ const noLocaleFormatInRender = defineRule({
|
|
|
32032
30767
|
if (!match || reportedNodes.has(match.node)) return;
|
|
32033
30768
|
if (fileIsEmailTemplate) return;
|
|
32034
30769
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
32035
|
-
if (
|
|
32036
|
-
if (
|
|
30770
|
+
if (isInsideClientOnlyGuard(node)) return;
|
|
30771
|
+
if (isGatedByFalsyInitialState(node)) return;
|
|
30772
|
+
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
|
|
32037
30773
|
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
|
|
32038
30774
|
reportedNodes.add(match.node);
|
|
32039
30775
|
context.report({
|
|
@@ -32135,6 +30871,9 @@ const noLongTransitionDuration = defineRule({
|
|
|
32135
30871
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
32136
30872
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
32137
30873
|
//#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
|
|
32138
30877
|
//#region src/plugin/rules/architecture/no-many-boolean-props.ts
|
|
32139
30878
|
const IMPERATIVE_CALLBACK_PREFIX_PATTERN = /^(?:show|hide|enable|disable)[A-Z]/;
|
|
32140
30879
|
const collectCallbackUsedNames = (componentBody, propsParam, scopes) => {
|
|
@@ -32266,7 +31005,7 @@ const noMatchMediaInStateInitializer = defineRule({
|
|
|
32266
31005
|
title: "matchMedia in state initializer",
|
|
32267
31006
|
severity: "warn",
|
|
32268
31007
|
category: "Correctness",
|
|
32269
|
-
|
|
31008
|
+
disabledWhen: ["vite", "cra"],
|
|
32270
31009
|
recommendation: "Prefer CSS media queries for layout, or subscribe with `useSyncExternalStore` and provide a stable server snapshot.",
|
|
32271
31010
|
create: (context) => {
|
|
32272
31011
|
if (isTestlikeFilename(context.filename)) return {};
|
|
@@ -32817,41 +31556,6 @@ const noMutableInDeps = defineRule({
|
|
|
32817
31556
|
}
|
|
32818
31557
|
});
|
|
32819
31558
|
//#endregion
|
|
32820
|
-
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
32821
|
-
const isResultDiscardedCall = (callExpression) => {
|
|
32822
|
-
let node = callExpression;
|
|
32823
|
-
let parent = node.parent;
|
|
32824
|
-
while (parent) {
|
|
32825
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
32826
|
-
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
32827
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
32828
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
32829
|
-
node = parent;
|
|
32830
|
-
parent = node.parent;
|
|
32831
|
-
continue;
|
|
32832
|
-
}
|
|
32833
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
32834
|
-
node = parent;
|
|
32835
|
-
parent = node.parent;
|
|
32836
|
-
continue;
|
|
32837
|
-
}
|
|
32838
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
32839
|
-
node = parent;
|
|
32840
|
-
parent = node.parent;
|
|
32841
|
-
continue;
|
|
32842
|
-
}
|
|
32843
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
32844
|
-
const expressions = parent.expressions ?? [];
|
|
32845
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
32846
|
-
node = parent;
|
|
32847
|
-
parent = node.parent;
|
|
32848
|
-
continue;
|
|
32849
|
-
}
|
|
32850
|
-
return false;
|
|
32851
|
-
}
|
|
32852
|
-
return false;
|
|
32853
|
-
};
|
|
32854
|
-
//#endregion
|
|
32855
31559
|
//#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
|
|
32856
31560
|
const LODASH_MUTATOR_NAMES = new Set([
|
|
32857
31561
|
"set",
|
|
@@ -34142,7 +32846,7 @@ const isUseRefIdentifier = (identifier) => {
|
|
|
34142
32846
|
};
|
|
34143
32847
|
const COMMAND_PROP_NAME_PATTERN = /^(fetch|load|refetch|dispatch|register|render)([A-Z_]|$)/;
|
|
34144
32848
|
const SETTER_NAMED_PROP_PATTERN = /^set[A-Z]/;
|
|
34145
|
-
const unwrapChainExpression
|
|
32849
|
+
const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
34146
32850
|
const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
|
|
34147
32851
|
"useCallback",
|
|
34148
32852
|
"useMemo",
|
|
@@ -34177,7 +32881,7 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
34177
32881
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
34178
32882
|
const node = def.node;
|
|
34179
32883
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
34180
|
-
const initializer = unwrapChainExpression
|
|
32884
|
+
const initializer = unwrapChainExpression(node.init);
|
|
34181
32885
|
const wrappedFunction = getWrapperHookWrappedFunction(initializer);
|
|
34182
32886
|
if (wrappedFunction) {
|
|
34183
32887
|
if (wrappedFunction.async) return false;
|
|
@@ -34187,170 +32891,10 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
34187
32891
|
return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
|
|
34188
32892
|
}));
|
|
34189
32893
|
};
|
|
34190
|
-
const getDeclarationKind = (declarator) => {
|
|
34191
|
-
const declaration = declarator.parent;
|
|
34192
|
-
return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
|
|
34193
|
-
};
|
|
34194
|
-
const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
34195
|
-
const getDestructuredPropertyName = (bindingIdentifier) => {
|
|
34196
|
-
const property = bindingIdentifier.parent;
|
|
34197
|
-
if (!property || !isNodeOfType(property, "Property")) return null;
|
|
34198
|
-
if (property.computed) return isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" ? String(property.key.value) : null;
|
|
34199
|
-
return isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
34200
|
-
};
|
|
34201
|
-
const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
|
|
34202
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
34203
|
-
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
34204
|
-
const callbackReference = getRef(analysis, unwrappedExpression);
|
|
34205
|
-
const callbackVariable = callbackReference?.resolved;
|
|
34206
|
-
if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable)) return null;
|
|
34207
|
-
if (isProp(analysis, callbackReference)) {
|
|
34208
|
-
if (isWholePropsObjectReference(analysis, callbackReference)) return null;
|
|
34209
|
-
const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
|
|
34210
|
-
return (bindingIdentifier && getDestructuredPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
|
|
34211
|
-
}
|
|
34212
|
-
if (hasMutableBindingWrite(callbackReference)) return null;
|
|
34213
|
-
const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
34214
|
-
if (definitions.length !== 1) return null;
|
|
34215
|
-
const declarator = definitions[0];
|
|
34216
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || getDeclarationKind(declarator) !== "const" || !declarator.init) return null;
|
|
34217
|
-
visitedVariables.add(callbackVariable);
|
|
34218
|
-
if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
34219
|
-
const bindingIdentifier = callbackVariable.defs[0]?.name;
|
|
34220
|
-
const propertyName = bindingIdentifier ? getDestructuredPropertyName(bindingIdentifier) : null;
|
|
34221
|
-
const propsReference = getDownstreamRefs(analysis, declarator.init).find((candidateReference) => isWholePropsObjectReference(analysis, candidateReference));
|
|
34222
|
-
return propertyName && propsReference ? propertyName : null;
|
|
34223
|
-
}
|
|
34224
|
-
if (!isNodeOfType(declarator.id, "Identifier")) return null;
|
|
34225
|
-
return getParentCallbackPropName(analysis, declarator.init, visitedVariables);
|
|
34226
|
-
}
|
|
34227
|
-
if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
|
|
34228
|
-
const callbackName = getStaticMemberPropertyName(unwrappedExpression);
|
|
34229
|
-
if (!callbackName) return null;
|
|
34230
|
-
const receiver = stripParenExpression(unwrappedExpression.object);
|
|
34231
|
-
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
34232
|
-
const receiverReference = getRef(analysis, receiver);
|
|
34233
|
-
if (!receiverReference || !isWholePropsObjectReference(analysis, receiverReference)) return null;
|
|
34234
|
-
return callbackName;
|
|
34235
|
-
};
|
|
34236
|
-
const isNullishRefInitializer = (initializer) => {
|
|
34237
|
-
if (!initializer) return true;
|
|
34238
|
-
const unwrappedInitializer = stripParenExpression(initializer);
|
|
34239
|
-
if (isNodeOfType(unwrappedInitializer, "Literal")) return unwrappedInitializer.value === null;
|
|
34240
|
-
if (!isNodeOfType(unwrappedInitializer, "Identifier")) return false;
|
|
34241
|
-
return unwrappedInitializer.name === "undefined";
|
|
34242
|
-
};
|
|
34243
|
-
const getDirectComponentBodyStatement = (node, componentBody) => {
|
|
34244
|
-
let current = node;
|
|
34245
|
-
while (current?.parent && current.parent !== componentBody) current = current.parent;
|
|
34246
|
-
return current?.parent === componentBody ? current : null;
|
|
34247
|
-
};
|
|
34248
|
-
const getVariableForDeclarator = (analysis, declarator) => {
|
|
34249
|
-
for (const scope of analysis.scopeManager.scopes) {
|
|
34250
|
-
const variable = scope.variables.find((candidateVariable) => candidateVariable.defs.some((definition) => definition.node === declarator));
|
|
34251
|
-
if (variable) return variable;
|
|
34252
|
-
}
|
|
34253
|
-
return null;
|
|
34254
|
-
};
|
|
34255
|
-
const getRefMember = (identifier) => {
|
|
34256
|
-
const receiver = findTransparentExpressionRoot(identifier);
|
|
34257
|
-
const member = receiver.parent;
|
|
34258
|
-
if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== receiver) return null;
|
|
34259
|
-
return member;
|
|
34260
|
-
};
|
|
34261
|
-
const getRefMemberAssignment = (identifier) => {
|
|
34262
|
-
const member = getRefMember(identifier);
|
|
34263
|
-
if (!member) return null;
|
|
34264
|
-
const memberRoot = findTransparentExpressionRoot(member);
|
|
34265
|
-
const assignment = memberRoot.parent;
|
|
34266
|
-
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== memberRoot) return null;
|
|
34267
|
-
return assignment;
|
|
34268
|
-
};
|
|
34269
|
-
const getRefAliasDeclarator = (identifier) => {
|
|
34270
|
-
const initializer = findTransparentExpressionRoot(identifier);
|
|
34271
|
-
const declarator = initializer.parent;
|
|
34272
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== initializer || !isNodeOfType(declarator.id, "Identifier") || getDeclarationKind(declarator) !== "const") return null;
|
|
34273
|
-
return declarator;
|
|
34274
|
-
};
|
|
34275
|
-
const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
|
|
34276
|
-
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
34277
|
-
const receiverReference = getRef(analysis, receiver);
|
|
34278
|
-
if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
|
|
34279
|
-
const variables = /* @__PURE__ */ new Set();
|
|
34280
|
-
let currentVariable = receiverReference.resolved;
|
|
34281
|
-
let refCall = null;
|
|
34282
|
-
while (currentVariable && !variables.has(currentVariable)) {
|
|
34283
|
-
variables.add(currentVariable);
|
|
34284
|
-
const definitions = currentVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
34285
|
-
if (definitions.length !== 1) return null;
|
|
34286
|
-
const declarator = definitions[0];
|
|
34287
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) return null;
|
|
34288
|
-
if (isNodeOfType(declarator.init, "CallExpression") && isReactUseRefCall(declarator.init)) {
|
|
34289
|
-
refCall = declarator.init;
|
|
34290
|
-
break;
|
|
34291
|
-
}
|
|
34292
|
-
if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
|
|
34293
|
-
const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
|
|
34294
|
-
if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
|
|
34295
|
-
currentVariable = upstreamReference.resolved;
|
|
34296
|
-
}
|
|
34297
|
-
if (!refCall) return null;
|
|
34298
|
-
const pendingVariables = [...variables];
|
|
34299
|
-
while (pendingVariables.length > 0) {
|
|
34300
|
-
const variable = pendingVariables.pop();
|
|
34301
|
-
if (!variable) continue;
|
|
34302
|
-
for (const candidateReference of variable.references) {
|
|
34303
|
-
const aliasDeclarator = getRefAliasDeclarator(candidateReference.identifier);
|
|
34304
|
-
if (!aliasDeclarator) continue;
|
|
34305
|
-
const aliasVariable = getVariableForDeclarator(analysis, aliasDeclarator);
|
|
34306
|
-
if (!aliasVariable || variables.has(aliasVariable)) continue;
|
|
34307
|
-
if (aliasVariable.references.some((aliasReference) => aliasReference.isWrite() && !aliasReference.init)) return null;
|
|
34308
|
-
variables.add(aliasVariable);
|
|
34309
|
-
pendingVariables.push(aliasVariable);
|
|
34310
|
-
}
|
|
34311
|
-
}
|
|
34312
|
-
return {
|
|
34313
|
-
refCall,
|
|
34314
|
-
variables
|
|
34315
|
-
};
|
|
34316
|
-
};
|
|
34317
|
-
const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactUseRefCall) => {
|
|
34318
|
-
const callee = stripParenExpression(callExpression.callee);
|
|
34319
|
-
if (!isNodeOfType(callee, "MemberExpression") || getStaticMemberPropertyName(callee) !== "current") return null;
|
|
34320
|
-
const bindingProvenance = getRefBindingProvenance(analysis, stripParenExpression(callee.object), isReactUseRefCall);
|
|
34321
|
-
if (!bindingProvenance) return null;
|
|
34322
|
-
const { refCall, variables } = bindingProvenance;
|
|
34323
|
-
const componentFunction = findEnclosingFunction(effectCall);
|
|
34324
|
-
if (!componentFunction || !isFunctionLike$1(componentFunction) || !isComponentFunction$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
|
|
34325
|
-
if (!getDirectComponentBodyStatement(effectCall, componentFunction.body)) return null;
|
|
34326
|
-
const callbackPropNames = /* @__PURE__ */ new Set();
|
|
34327
|
-
const initializer = refCall.arguments?.[0];
|
|
34328
|
-
const initializerCallbackName = initializer ? getParentCallbackPropName(analysis, initializer) : null;
|
|
34329
|
-
if (initializerCallbackName) callbackPropNames.add(initializerCallbackName);
|
|
34330
|
-
else if (!isNullishRefInitializer(initializer)) return null;
|
|
34331
|
-
for (const variable of variables) for (const candidateReference of variable.references) {
|
|
34332
|
-
if (candidateReference.init) continue;
|
|
34333
|
-
if (candidateReference.isWrite()) return null;
|
|
34334
|
-
const identifier = candidateReference.identifier;
|
|
34335
|
-
if (getRefAliasDeclarator(identifier)) continue;
|
|
34336
|
-
const member = getRefMember(identifier);
|
|
34337
|
-
if (!member || getStaticMemberPropertyName(member) !== "current") return null;
|
|
34338
|
-
const memberParent = findTransparentExpressionRoot(member).parent;
|
|
34339
|
-
if (memberParent && (isNodeOfType(memberParent, "UpdateExpression") || isNodeOfType(memberParent, "UnaryExpression") && memberParent.operator === "delete")) return null;
|
|
34340
|
-
const assignment = getRefMemberAssignment(identifier);
|
|
34341
|
-
if (!assignment) continue;
|
|
34342
|
-
const assignmentStatement = findTransparentExpressionRoot(assignment).parent;
|
|
34343
|
-
if (assignment.operator !== "=" || !assignmentStatement || !isNodeOfType(assignmentStatement, "ExpressionStatement") || assignmentStatement.parent !== componentFunction.body) return null;
|
|
34344
|
-
const assignedCallbackName = getParentCallbackPropName(analysis, assignment.right);
|
|
34345
|
-
if (!assignedCallbackName) return null;
|
|
34346
|
-
callbackPropNames.add(assignedCallbackName);
|
|
34347
|
-
}
|
|
34348
|
-
return callbackPropNames.size > 0 ? { callbackPropNames } : null;
|
|
34349
|
-
};
|
|
34350
32894
|
const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
34351
32895
|
const node = def.node;
|
|
34352
32896
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
34353
|
-
return getWrapperHookWrappedFunction(unwrapChainExpression
|
|
32897
|
+
return getWrapperHookWrappedFunction(unwrapChainExpression(node.init)) !== null;
|
|
34354
32898
|
}));
|
|
34355
32899
|
const isHandlerBagArgument = (analysis, argument) => {
|
|
34356
32900
|
if (!isNodeOfType(argument, "ObjectExpression")) return false;
|
|
@@ -34372,7 +32916,7 @@ const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
|
|
|
34372
32916
|
const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
34373
32917
|
const node = def.node;
|
|
34374
32918
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
34375
|
-
const init = unwrapChainExpression
|
|
32919
|
+
const init = unwrapChainExpression(node.init);
|
|
34376
32920
|
if (!isNodeOfType(init, "CallExpression")) return false;
|
|
34377
32921
|
const callee = init.callee;
|
|
34378
32922
|
if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
|
|
@@ -34402,79 +32946,71 @@ const noPassDataToParent = defineRule({
|
|
|
34402
32946
|
severity: "warn",
|
|
34403
32947
|
tags: ["test-noise"],
|
|
34404
32948
|
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",
|
|
34405
|
-
create: (context) => {
|
|
34406
|
-
|
|
34407
|
-
|
|
34408
|
-
|
|
34409
|
-
|
|
34410
|
-
|
|
34411
|
-
|
|
34412
|
-
|
|
34413
|
-
|
|
34414
|
-
|
|
34415
|
-
const
|
|
34416
|
-
if (!
|
|
34417
|
-
|
|
34418
|
-
if (!effectFn)
|
|
34419
|
-
|
|
34420
|
-
|
|
34421
|
-
|
|
34422
|
-
|
|
34423
|
-
if (
|
|
34424
|
-
|
|
34425
|
-
|
|
34426
|
-
|
|
34427
|
-
|
|
34428
|
-
|
|
34429
|
-
|
|
34430
|
-
|
|
34431
|
-
|
|
34432
|
-
|
|
34433
|
-
|
|
34434
|
-
|
|
34435
|
-
|
|
34436
|
-
|
|
34437
|
-
|
|
34438
|
-
|
|
34439
|
-
|
|
34440
|
-
|
|
34441
|
-
|
|
34442
|
-
|
|
34443
|
-
|
|
34444
|
-
|
|
34445
|
-
|
|
34446
|
-
|
|
34447
|
-
|
|
34448
|
-
|
|
34449
|
-
|
|
34450
|
-
|
|
34451
|
-
|
|
34452
|
-
|
|
34453
|
-
|
|
34454
|
-
|
|
34455
|
-
|
|
34456
|
-
if (
|
|
34457
|
-
if (
|
|
34458
|
-
|
|
34459
|
-
|
|
34460
|
-
|
|
34461
|
-
|
|
34462
|
-
|
|
34463
|
-
|
|
34464
|
-
|
|
34465
|
-
|
|
34466
|
-
|
|
34467
|
-
|
|
34468
|
-
|
|
34469
|
-
|
|
34470
|
-
})) continue;
|
|
34471
|
-
context.report({
|
|
34472
|
-
node: callExpr,
|
|
34473
|
-
message: "Handing data back to a parent from a useEffect costs your users an extra render."
|
|
34474
|
-
});
|
|
34475
|
-
}
|
|
34476
|
-
} };
|
|
34477
|
-
}
|
|
32949
|
+
create: (context) => ({ CallExpression(node) {
|
|
32950
|
+
if (!isUseEffect(node)) return;
|
|
32951
|
+
const analysis = getProgramAnalysis(node);
|
|
32952
|
+
if (!analysis) return;
|
|
32953
|
+
if (hasCleanup(analysis, node)) return;
|
|
32954
|
+
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
32955
|
+
if (!effectFnRefs) return;
|
|
32956
|
+
const effectFn = getEffectFn(analysis, node);
|
|
32957
|
+
if (!effectFn) return;
|
|
32958
|
+
for (const ref of effectFnRefs) {
|
|
32959
|
+
const callExpr = getCallExpr(ref);
|
|
32960
|
+
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
32961
|
+
if (isRefCall(analysis, ref)) continue;
|
|
32962
|
+
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
32963
|
+
const calleeNode = unwrapChainExpression(callExpr.callee);
|
|
32964
|
+
const identifier = ref.identifier;
|
|
32965
|
+
if (calleeNode === identifier) {
|
|
32966
|
+
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
32967
|
+
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
32968
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
32969
|
+
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
32970
|
+
if (isCustomHookParameter(ref)) continue;
|
|
32971
|
+
} else continue;
|
|
32972
|
+
const methodName = getCallMethodName(calleeNode);
|
|
32973
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32974
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
32975
|
+
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
32976
|
+
if (isNamespacedApiCallee(calleeNode)) continue;
|
|
32977
|
+
const calleeName = isNodeOfType(identifier, "Identifier") ? identifier.name : methodName;
|
|
32978
|
+
const isSetterNamedCallee = Boolean(calleeName && SETTER_NAMED_PROP_PATTERN.test(calleeName));
|
|
32979
|
+
const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
|
|
32980
|
+
const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
|
|
32981
|
+
if (isFunctionLike$1(argument)) {
|
|
32982
|
+
if (!isSetterNamedCallee) return [];
|
|
32983
|
+
return getFunctionalUpdaterDataRefs(analysis, argument);
|
|
32984
|
+
}
|
|
32985
|
+
if (isHandlerBagArgument(analysis, argument)) return [];
|
|
32986
|
+
if (isParentWiredHookResultArgument(analysis, argument)) return [];
|
|
32987
|
+
if (isNodeOfType(argument, "Identifier")) {
|
|
32988
|
+
const argumentRef = getRef(analysis, argument);
|
|
32989
|
+
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
32990
|
+
}
|
|
32991
|
+
return getDownstreamRefs(analysis, argument);
|
|
32992
|
+
}).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
32993
|
+
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
32994
|
+
if (!argsUpstreamRefs.some((argRef) => {
|
|
32995
|
+
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
32996
|
+
if (isProp(analysis, argRef)) return false;
|
|
32997
|
+
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
32998
|
+
if (isRefCurrent(argRef)) return false;
|
|
32999
|
+
if (isConstant(argRef)) return false;
|
|
33000
|
+
if (isParentWiredHookResultRef(analysis, argRef)) return false;
|
|
33001
|
+
if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
|
|
33002
|
+
if (resolvesToFunctionBinding(argRef)) return false;
|
|
33003
|
+
const argIdentifier = argRef.identifier;
|
|
33004
|
+
if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
|
|
33005
|
+
if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
|
|
33006
|
+
return true;
|
|
33007
|
+
})) continue;
|
|
33008
|
+
context.report({
|
|
33009
|
+
node: callExpr,
|
|
33010
|
+
message: "Handing data back to a parent from a useEffect costs your users an extra render."
|
|
33011
|
+
});
|
|
33012
|
+
}
|
|
33013
|
+
} })
|
|
34478
33014
|
});
|
|
34479
33015
|
//#endregion
|
|
34480
33016
|
//#region src/plugin/utils/is-call-result-consumed-as-argument.ts
|
|
@@ -35028,66 +33564,6 @@ const noPropCallbackInEffect = defineRule({
|
|
|
35028
33564
|
}
|
|
35029
33565
|
});
|
|
35030
33566
|
//#endregion
|
|
35031
|
-
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-render.ts
|
|
35032
|
-
const isPreservedThroughConciseArrow = (callExpression, scopes) => {
|
|
35033
|
-
let node = callExpression;
|
|
35034
|
-
let parent = node.parent;
|
|
35035
|
-
while (parent) {
|
|
35036
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
35037
|
-
node = parent;
|
|
35038
|
-
parent = node.parent;
|
|
35039
|
-
continue;
|
|
35040
|
-
}
|
|
35041
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
35042
|
-
node = parent;
|
|
35043
|
-
parent = node.parent;
|
|
35044
|
-
continue;
|
|
35045
|
-
}
|
|
35046
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
35047
|
-
node = parent;
|
|
35048
|
-
parent = node.parent;
|
|
35049
|
-
continue;
|
|
35050
|
-
}
|
|
35051
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
35052
|
-
const expressions = parent.expressions ?? [];
|
|
35053
|
-
if (expressions[expressions.length - 1] !== node) return false;
|
|
35054
|
-
node = parent;
|
|
35055
|
-
parent = node.parent;
|
|
35056
|
-
continue;
|
|
35057
|
-
}
|
|
35058
|
-
if (!isNodeOfType(parent, "ArrowFunctionExpression") || parent.body !== node) return !isResultDiscardedCall(node);
|
|
35059
|
-
const invocation = parent.parent;
|
|
35060
|
-
if (!isNodeOfType(invocation, "CallExpression") || !executesDuringRender(parent, scopes)) return true;
|
|
35061
|
-
if (invocation.arguments?.[0] === parent || invocation.arguments?.[1] === parent) {
|
|
35062
|
-
const callee = stripParenExpression(invocation.callee);
|
|
35063
|
-
return !(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "forEach" && invocation.arguments[0] === parent);
|
|
35064
|
-
}
|
|
35065
|
-
node = invocation;
|
|
35066
|
-
parent = node.parent;
|
|
35067
|
-
}
|
|
35068
|
-
return false;
|
|
35069
|
-
};
|
|
35070
|
-
const noPropCallbackInRender = defineRule({
|
|
35071
|
-
id: "no-prop-callback-in-render",
|
|
35072
|
-
title: "Prop callback invoked during render",
|
|
35073
|
-
severity: "error",
|
|
35074
|
-
recommendation: "Invoke the callback from the event or asynchronous operation that produced the value, or from an effect when synchronizing with an external system. Render must stay pure because React can replay or discard it.",
|
|
35075
|
-
create: (context) => ({ CallExpression(node) {
|
|
35076
|
-
if (!isResultDiscardedCall(node)) return;
|
|
35077
|
-
if (isPreservedThroughConciseArrow(node, context.scopes)) return;
|
|
35078
|
-
if (!findRenderPhaseComponentOrHook(node, context.scopes)) return;
|
|
35079
|
-
const analysis = getProgramAnalysis(node);
|
|
35080
|
-
if (!analysis) return;
|
|
35081
|
-
const callee = stripParenExpression(node.callee);
|
|
35082
|
-
if (isFunctionLike$1(callee)) return;
|
|
35083
|
-
if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return;
|
|
35084
|
-
context.report({
|
|
35085
|
-
node,
|
|
35086
|
-
message: "This prop callback runs during render. React can replay or discard render work, so the callback can fire more than once or for UI that never commits."
|
|
35087
|
-
});
|
|
35088
|
-
} })
|
|
35089
|
-
});
|
|
35090
|
-
//#endregion
|
|
35091
33567
|
//#region src/plugin/rules/architecture/no-prop-types.ts
|
|
35092
33568
|
const PROP_TYPES_PROPERTY = "propTypes";
|
|
35093
33569
|
const isPropTypesKey = (key, computed) => {
|
|
@@ -35782,63 +34258,6 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
35782
34258
|
}
|
|
35783
34259
|
});
|
|
35784
34260
|
//#endregion
|
|
35785
|
-
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
35786
|
-
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
35787
|
-
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
35788
|
-
if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
35789
|
-
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
35790
|
-
if (!symbol?.initializer) return null;
|
|
35791
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
35792
|
-
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
35793
|
-
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
35794
|
-
};
|
|
35795
|
-
const isSameRefCurrentMember = (node, refSymbol, scopes) => {
|
|
35796
|
-
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
|
|
35797
|
-
const receiver = stripParenExpression(node.object);
|
|
35798
|
-
return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
|
|
35799
|
-
};
|
|
35800
|
-
const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
|
|
35801
|
-
if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
|
|
35802
|
-
let descendant = assignmentExpression;
|
|
35803
|
-
let ancestor = descendant.parent;
|
|
35804
|
-
while (ancestor) {
|
|
35805
|
-
if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
|
|
35806
|
-
const { left, right } = ancestor.test;
|
|
35807
|
-
if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
|
|
35808
|
-
}
|
|
35809
|
-
descendant = ancestor;
|
|
35810
|
-
ancestor = descendant.parent;
|
|
35811
|
-
}
|
|
35812
|
-
return false;
|
|
35813
|
-
};
|
|
35814
|
-
const noRefCurrentInRender = defineRule({
|
|
35815
|
-
id: "no-ref-current-in-render",
|
|
35816
|
-
title: "Ref mutated during render",
|
|
35817
|
-
severity: "error",
|
|
35818
|
-
recommendation: "Move ref writes into an event handler or effect. Render must stay pure because React can replay or discard it. The predictable null-guarded lazy initialization pattern remains supported.",
|
|
35819
|
-
create: (context) => {
|
|
35820
|
-
const report = (memberExpression) => {
|
|
35821
|
-
if (!resolveReactRefSymbol(memberExpression, context.scopes)) return;
|
|
35822
|
-
if (!findRenderPhaseComponentOrHook(memberExpression, context.scopes)) return;
|
|
35823
|
-
context.report({
|
|
35824
|
-
node: memberExpression,
|
|
35825
|
-
message: "This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits."
|
|
35826
|
-
});
|
|
35827
|
-
};
|
|
35828
|
-
return {
|
|
35829
|
-
AssignmentExpression(node) {
|
|
35830
|
-
const refSymbol = resolveReactRefSymbol(node.left, context.scopes);
|
|
35831
|
-
if (!refSymbol) return;
|
|
35832
|
-
if (isDocumentedLazyInitialization(node, refSymbol, context.scopes)) return;
|
|
35833
|
-
report(node.left);
|
|
35834
|
-
},
|
|
35835
|
-
UpdateExpression(node) {
|
|
35836
|
-
report(node.argument);
|
|
35837
|
-
}
|
|
35838
|
-
};
|
|
35839
|
-
}
|
|
35840
|
-
});
|
|
35841
|
-
//#endregion
|
|
35842
34261
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
35843
34262
|
const isInsideComponentContext = (node) => {
|
|
35844
34263
|
let cursor = node.parent;
|
|
@@ -37853,140 +36272,6 @@ const noUnescapedEntities = defineRule({
|
|
|
37853
36272
|
} })
|
|
37854
36273
|
});
|
|
37855
36274
|
//#endregion
|
|
37856
|
-
//#region src/plugin/rules/performance/no-unguarded-browser-global-in-render-or-hook-init.ts
|
|
37857
|
-
const BROWSER_GLOBAL_NAMES = new Set([
|
|
37858
|
-
"window",
|
|
37859
|
-
"document",
|
|
37860
|
-
"localStorage",
|
|
37861
|
-
"sessionStorage",
|
|
37862
|
-
"navigator",
|
|
37863
|
-
"matchMedia"
|
|
37864
|
-
]);
|
|
37865
|
-
const getTypeofBrowserGlobalName = (expression, context) => {
|
|
37866
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
37867
|
-
if (!isNodeOfType(unwrappedExpression, "UnaryExpression") || unwrappedExpression.operator !== "typeof") return null;
|
|
37868
|
-
const argument = stripParenExpression(unwrappedExpression.argument);
|
|
37869
|
-
if (isNodeOfType(argument, "Identifier")) return BROWSER_GLOBAL_NAMES.has(argument.name) && context.scopes.isGlobalReference(argument) ? argument.name : null;
|
|
37870
|
-
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;
|
|
37871
|
-
return argument.property.name;
|
|
37872
|
-
};
|
|
37873
|
-
const browserGuardCoversGlobal = (guardName, browserGlobalName) => guardName === browserGlobalName || guardName === "window" || guardName === "document";
|
|
37874
|
-
const mergeAvailability = (leftAvailability, rightAvailability) => {
|
|
37875
|
-
if (leftAvailability === null) return rightAvailability;
|
|
37876
|
-
if (rightAvailability === null) return leftAvailability;
|
|
37877
|
-
return leftAvailability === rightAvailability ? leftAvailability : null;
|
|
37878
|
-
};
|
|
37879
|
-
const readAvailabilityWhenPredicate = (expression, browserGlobalName, context, predicateResult) => {
|
|
37880
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
37881
|
-
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return readAvailabilityWhenPredicate(unwrappedExpression.argument, browserGlobalName, context, !predicateResult);
|
|
37882
|
-
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
|
|
37883
|
-
if (unwrappedExpression.operator === "&&" && predicateResult) return mergeAvailability(readAvailabilityWhenPredicate(unwrappedExpression.left, browserGlobalName, context, true), readAvailabilityWhenPredicate(unwrappedExpression.right, browserGlobalName, context, true));
|
|
37884
|
-
if (unwrappedExpression.operator === "||" && !predicateResult) return mergeAvailability(readAvailabilityWhenPredicate(unwrappedExpression.left, browserGlobalName, context, false), readAvailabilityWhenPredicate(unwrappedExpression.right, browserGlobalName, context, false));
|
|
37885
|
-
return null;
|
|
37886
|
-
}
|
|
37887
|
-
if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
|
|
37888
|
-
const leftTypeofName = getTypeofBrowserGlobalName(unwrappedExpression.left, context);
|
|
37889
|
-
const rightTypeofName = getTypeofBrowserGlobalName(unwrappedExpression.right, context);
|
|
37890
|
-
const leftComparedType = isNodeOfType(unwrappedExpression.left, "Literal") && typeof unwrappedExpression.left.value === "string" ? unwrappedExpression.left.value : null;
|
|
37891
|
-
const rightComparedType = isNodeOfType(unwrappedExpression.right, "Literal") && typeof unwrappedExpression.right.value === "string" ? unwrappedExpression.right.value : null;
|
|
37892
|
-
const guardName = leftTypeofName && rightComparedType ? leftTypeofName : rightTypeofName && leftComparedType ? rightTypeofName : null;
|
|
37893
|
-
const comparedType = leftTypeofName && rightComparedType ? rightComparedType : rightTypeofName && leftComparedType ? leftComparedType : null;
|
|
37894
|
-
if (!guardName || !browserGuardCoversGlobal(guardName, browserGlobalName)) return null;
|
|
37895
|
-
if (!comparedType) return null;
|
|
37896
|
-
const isEquality = unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==";
|
|
37897
|
-
const isInequality = unwrappedExpression.operator === "!==" || unwrappedExpression.operator === "!=";
|
|
37898
|
-
if (!isEquality && !isInequality) return null;
|
|
37899
|
-
const browserType = guardName === "matchMedia" ? "function" : "object";
|
|
37900
|
-
const browserResult = isEquality ? browserType === comparedType : browserType !== comparedType;
|
|
37901
|
-
if (browserResult === (isEquality ? comparedType === "undefined" : comparedType !== "undefined")) return null;
|
|
37902
|
-
return predicateResult === browserResult;
|
|
37903
|
-
};
|
|
37904
|
-
const isInsideAvailabilityGuard = (node, browserGlobalName, context) => {
|
|
37905
|
-
let currentNode = node;
|
|
37906
|
-
let parentNode = currentNode.parent;
|
|
37907
|
-
while (parentNode) {
|
|
37908
|
-
if (isFunctionLike$1(parentNode) && !executesDuringRender(parentNode, context.scopes)) break;
|
|
37909
|
-
if (isNodeOfType(parentNode, "LogicalExpression") && (parentNode.operator === "&&" || parentNode.operator === "||") && parentNode.right === currentNode && readAvailabilityWhenPredicate(parentNode.left, browserGlobalName, context, parentNode.operator === "&&") === true) return true;
|
|
37910
|
-
if (isNodeOfType(parentNode, "ConditionalExpression")) {
|
|
37911
|
-
if (parentNode.consequent === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, true) === true || parentNode.alternate === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, false) === true) return true;
|
|
37912
|
-
}
|
|
37913
|
-
if (isNodeOfType(parentNode, "IfStatement")) {
|
|
37914
|
-
if (parentNode.consequent === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, true) === true || parentNode.alternate === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, false) === true) return true;
|
|
37915
|
-
}
|
|
37916
|
-
currentNode = parentNode;
|
|
37917
|
-
parentNode = currentNode.parent;
|
|
37918
|
-
}
|
|
37919
|
-
return false;
|
|
37920
|
-
};
|
|
37921
|
-
const isAfterAvailabilityEarlyExit = (node, componentOrHookNode, browserGlobalName, context) => {
|
|
37922
|
-
const enclosingFunction = findEnclosingFunction(node);
|
|
37923
|
-
if (!enclosingFunction || enclosingFunction !== componentOrHookNode && !executesDuringRender(enclosingFunction, context.scopes) || !isFunctionLike$1(enclosingFunction) || !isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
|
|
37924
|
-
let currentNode = node;
|
|
37925
|
-
while (currentNode !== enclosingFunction) {
|
|
37926
|
-
const parentNode = currentNode.parent;
|
|
37927
|
-
if (!parentNode) return false;
|
|
37928
|
-
if (isNodeOfType(parentNode, "BlockStatement")) for (const statement of parentNode.body) {
|
|
37929
|
-
if (statement === currentNode) break;
|
|
37930
|
-
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
37931
|
-
if (readAvailabilityWhenPredicate(statement.test, browserGlobalName, context, false) === true && statementAlwaysExits(statement.consequent)) return true;
|
|
37932
|
-
if (readAvailabilityWhenPredicate(statement.test, browserGlobalName, context, true) === true && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
|
|
37933
|
-
}
|
|
37934
|
-
currentNode = parentNode;
|
|
37935
|
-
}
|
|
37936
|
-
return false;
|
|
37937
|
-
};
|
|
37938
|
-
const isTypeofProbe = (node) => {
|
|
37939
|
-
const expressionRoot = findTransparentExpressionRoot(node);
|
|
37940
|
-
const parentNode = expressionRoot.parent;
|
|
37941
|
-
return isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "typeof" && parentNode.argument === expressionRoot;
|
|
37942
|
-
};
|
|
37943
|
-
const noUnguardedBrowserGlobalInRenderOrHookInit = defineRule({
|
|
37944
|
-
id: "no-unguarded-browser-global-in-render-or-hook-init",
|
|
37945
|
-
title: "Browser global read during server render",
|
|
37946
|
-
severity: "error",
|
|
37947
|
-
category: "Correctness",
|
|
37948
|
-
requires: ["ssr"],
|
|
37949
|
-
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.",
|
|
37950
|
-
create: (context) => {
|
|
37951
|
-
if (isTestlikeFilename(context.filename)) return {};
|
|
37952
|
-
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
37953
|
-
let fileIsEmailTemplate = false;
|
|
37954
|
-
const reportedNodes = /* @__PURE__ */ new Set();
|
|
37955
|
-
const reportBrowserRead = (node, browserGlobalName) => {
|
|
37956
|
-
if (reportedNodes.has(node) || isTypeofProbe(node)) return;
|
|
37957
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(node, context.scopes);
|
|
37958
|
-
if (!componentOrHookNode) return;
|
|
37959
|
-
if (fileIsEmailTemplate) return;
|
|
37960
|
-
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(node) ?? node)) return;
|
|
37961
|
-
if (isGatedByFalsyInitialState(node, context.scopes)) return;
|
|
37962
|
-
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode, context.scopes)) return;
|
|
37963
|
-
if (isInsideAvailabilityGuard(node, browserGlobalName, context)) return;
|
|
37964
|
-
if (isAfterAvailabilityEarlyExit(node, componentOrHookNode, browserGlobalName, context)) return;
|
|
37965
|
-
reportedNodes.add(node);
|
|
37966
|
-
context.report({
|
|
37967
|
-
node,
|
|
37968
|
-
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.`
|
|
37969
|
-
});
|
|
37970
|
-
};
|
|
37971
|
-
return {
|
|
37972
|
-
Program(node) {
|
|
37973
|
-
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
37974
|
-
},
|
|
37975
|
-
Identifier(node) {
|
|
37976
|
-
if (!BROWSER_GLOBAL_NAMES.has(node.name)) return;
|
|
37977
|
-
if (!context.scopes.isGlobalReference(node)) return;
|
|
37978
|
-
reportBrowserRead(node, node.name);
|
|
37979
|
-
},
|
|
37980
|
-
MemberExpression(node) {
|
|
37981
|
-
if (node.computed) return;
|
|
37982
|
-
const objectNode = stripParenExpression(node.object);
|
|
37983
|
-
if (!isNodeOfType(objectNode, "Identifier") || objectNode.name !== "globalThis" || !context.scopes.isGlobalReference(objectNode) || !isNodeOfType(node.property, "Identifier") || !BROWSER_GLOBAL_NAMES.has(node.property.name)) return;
|
|
37984
|
-
reportBrowserRead(node, node.property.name);
|
|
37985
|
-
}
|
|
37986
|
-
};
|
|
37987
|
-
}
|
|
37988
|
-
});
|
|
37989
|
-
//#endregion
|
|
37990
36275
|
//#region src/plugin/constants/dom-aria-properties.ts
|
|
37991
36276
|
const ARIA_PROPERTY_NAMES = new Set([
|
|
37992
36277
|
"activedescendant",
|
|
@@ -41829,123 +40114,6 @@ const preferUseEffectEvent = defineRule({
|
|
|
41829
40114
|
}
|
|
41830
40115
|
});
|
|
41831
40116
|
//#endregion
|
|
41832
|
-
//#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
|
|
41833
|
-
const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
41834
|
-
"each",
|
|
41835
|
-
"every",
|
|
41836
|
-
"filter",
|
|
41837
|
-
"find",
|
|
41838
|
-
"findIndex",
|
|
41839
|
-
"findLast",
|
|
41840
|
-
"findLastIndex",
|
|
41841
|
-
"flatMap",
|
|
41842
|
-
"forEach",
|
|
41843
|
-
"map",
|
|
41844
|
-
"reduce",
|
|
41845
|
-
"reduceRight",
|
|
41846
|
-
"some",
|
|
41847
|
-
"sort",
|
|
41848
|
-
"toSorted"
|
|
41849
|
-
]);
|
|
41850
|
-
const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
41851
|
-
"from",
|
|
41852
|
-
"fromAsync",
|
|
41853
|
-
"groupBy"
|
|
41854
|
-
]);
|
|
41855
|
-
const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
41856
|
-
const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
|
|
41857
|
-
const isListenerRemovalViaNullHandler = (callNode) => {
|
|
41858
|
-
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
41859
|
-
const callee = unwrapChainExpression(callNode.callee);
|
|
41860
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
|
|
41861
|
-
};
|
|
41862
|
-
const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
|
|
41863
|
-
"off",
|
|
41864
|
-
"removeEventListener",
|
|
41865
|
-
"removeListener"
|
|
41866
|
-
]);
|
|
41867
|
-
const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
|
|
41868
|
-
if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
|
|
41869
|
-
const handlerArgument = callNode.arguments?.[1];
|
|
41870
|
-
return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
|
|
41871
|
-
};
|
|
41872
|
-
const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
41873
|
-
const callNode = unwrapChainExpression(node);
|
|
41874
|
-
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
41875
|
-
if (isListenerRemovalViaNullHandler(callNode)) return true;
|
|
41876
|
-
const callee = unwrapChainExpression(callNode.callee);
|
|
41877
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
41878
|
-
if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
|
|
41879
|
-
if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
|
|
41880
|
-
if (knownCleanupFunctionNames.has(callee.name)) return true;
|
|
41881
|
-
return false;
|
|
41882
|
-
}
|
|
41883
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
41884
|
-
if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
|
|
41885
|
-
if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
|
|
41886
|
-
return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
|
|
41887
|
-
}
|
|
41888
|
-
return false;
|
|
41889
|
-
};
|
|
41890
|
-
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);
|
|
41891
|
-
const isIteratorCallbackArgument = (node) => {
|
|
41892
|
-
const parentNode = node.parent;
|
|
41893
|
-
if (!isNodeOfType(parentNode, "CallExpression")) return false;
|
|
41894
|
-
if (!parentNode.arguments?.some((argument) => argument === node)) return false;
|
|
41895
|
-
const callee = unwrapChainExpression(parentNode.callee);
|
|
41896
|
-
if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
|
|
41897
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
|
|
41898
|
-
};
|
|
41899
|
-
const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
41900
|
-
let didFindRelease = false;
|
|
41901
|
-
walkAst(node, (child) => {
|
|
41902
|
-
if (didFindRelease) return false;
|
|
41903
|
-
if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
|
|
41904
|
-
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
41905
|
-
didFindRelease = true;
|
|
41906
|
-
return false;
|
|
41907
|
-
}
|
|
41908
|
-
});
|
|
41909
|
-
return didFindRelease;
|
|
41910
|
-
};
|
|
41911
|
-
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
41912
|
-
if (!isFunctionLike$1(node)) return false;
|
|
41913
|
-
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
41914
|
-
};
|
|
41915
|
-
const isProvablyNoOpCleanupFunction = (node) => {
|
|
41916
|
-
if (!isFunctionLike$1(node)) return false;
|
|
41917
|
-
let sawNoOpRemoval = false;
|
|
41918
|
-
let sawOtherCall = false;
|
|
41919
|
-
walkAst(node.body, (child) => {
|
|
41920
|
-
if (sawOtherCall) return false;
|
|
41921
|
-
if (isFunctionLike$1(child)) return false;
|
|
41922
|
-
const callNode = unwrapChainExpression(child);
|
|
41923
|
-
if (!isNodeOfType(callNode, "CallExpression")) return;
|
|
41924
|
-
const callee = unwrapChainExpression(callNode.callee);
|
|
41925
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
|
|
41926
|
-
sawNoOpRemoval = true;
|
|
41927
|
-
return;
|
|
41928
|
-
}
|
|
41929
|
-
sawOtherCall = true;
|
|
41930
|
-
});
|
|
41931
|
-
return sawNoOpRemoval && !sawOtherCall;
|
|
41932
|
-
};
|
|
41933
|
-
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
|
|
41934
|
-
if (!returnedValue) return false;
|
|
41935
|
-
const unwrappedValue = unwrapChainExpression(returnedValue);
|
|
41936
|
-
if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
|
|
41937
|
-
if (isNodeOfType(unwrappedValue, "Identifier")) {
|
|
41938
|
-
if (unwrappedValue.name === "undefined") return false;
|
|
41939
|
-
if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
|
|
41940
|
-
return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
|
|
41941
|
-
}
|
|
41942
|
-
if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
41943
|
-
if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
|
|
41944
|
-
if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
41945
|
-
if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
|
|
41946
|
-
return false;
|
|
41947
|
-
};
|
|
41948
|
-
//#endregion
|
|
41949
40117
|
//#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
|
|
41950
40118
|
const findUseEffectsInComponent = (componentBody) => {
|
|
41951
40119
|
const effectCalls = [];
|
|
@@ -43414,7 +41582,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
43414
41582
|
title: "Time or random value in JSX",
|
|
43415
41583
|
severity: "warn",
|
|
43416
41584
|
category: "Correctness",
|
|
43417
|
-
|
|
41585
|
+
disabledWhen: ["vite", "cra"],
|
|
43418
41586
|
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",
|
|
43419
41587
|
create: (context) => {
|
|
43420
41588
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
@@ -43428,7 +41596,8 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
43428
41596
|
const matched = NONDETERMINISTIC_RENDER_PATTERNS.find((pattern) => pattern.matches(node.expression));
|
|
43429
41597
|
if (matched) {
|
|
43430
41598
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
43431
|
-
if (
|
|
41599
|
+
if (isInsideClientOnlyGuard(node)) return;
|
|
41600
|
+
if (isGatedByFalsyInitialState(node)) return;
|
|
43432
41601
|
if (isInsideMotionTransitionAttribute(node)) return;
|
|
43433
41602
|
context.report({
|
|
43434
41603
|
node,
|
|
@@ -43437,10 +41606,11 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
43437
41606
|
return;
|
|
43438
41607
|
}
|
|
43439
41608
|
walkAst(node.expression, (child) => {
|
|
43440
|
-
if (isFunctionLike$1(child) && !executesDuringRender(child
|
|
41609
|
+
if (isFunctionLike$1(child) && !executesDuringRender(child)) return false;
|
|
43441
41610
|
for (const pattern of NONDETERMINISTIC_RENDER_PATTERNS) if (pattern.matches(child)) {
|
|
43442
41611
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
43443
|
-
if (
|
|
41612
|
+
if (isInsideClientOnlyGuard(child)) return;
|
|
41613
|
+
if (isGatedByFalsyInitialState(child)) return;
|
|
43444
41614
|
if (isInsideMotionTransitionAttribute(child)) return;
|
|
43445
41615
|
if (pattern.display === "new Date()" && isYearOnlyDateRead(child)) return;
|
|
43446
41616
|
context.report({
|
|
@@ -56777,18 +54947,6 @@ const reactDoctorRules = [
|
|
|
56777
54947
|
category: "Accessibility"
|
|
56778
54948
|
}
|
|
56779
54949
|
},
|
|
56780
|
-
{
|
|
56781
|
-
key: "react-doctor/no-hydration-branch-on-browser-global",
|
|
56782
|
-
id: "no-hydration-branch-on-browser-global",
|
|
56783
|
-
source: "react-doctor",
|
|
56784
|
-
originallyExternal: false,
|
|
56785
|
-
rule: {
|
|
56786
|
-
...noHydrationBranchOnBrowserGlobal,
|
|
56787
|
-
framework: "global",
|
|
56788
|
-
category: "Bugs",
|
|
56789
|
-
requires: [...new Set(["react", ...noHydrationBranchOnBrowserGlobal.requires ?? []])]
|
|
56790
|
-
}
|
|
56791
|
-
},
|
|
56792
54950
|
{
|
|
56793
54951
|
key: "react-doctor/no-img-lazy-with-high-fetchpriority",
|
|
56794
54952
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
@@ -56801,18 +54959,6 @@ const reactDoctorRules = [
|
|
|
56801
54959
|
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
56802
54960
|
}
|
|
56803
54961
|
},
|
|
56804
|
-
{
|
|
56805
|
-
key: "react-doctor/no-impure-state-updater",
|
|
56806
|
-
id: "no-impure-state-updater",
|
|
56807
|
-
source: "react-doctor",
|
|
56808
|
-
originallyExternal: false,
|
|
56809
|
-
rule: {
|
|
56810
|
-
...noImpureStateUpdater,
|
|
56811
|
-
framework: "global",
|
|
56812
|
-
category: "Bugs",
|
|
56813
|
-
requires: [...new Set(["react", ...noImpureStateUpdater.requires ?? []])]
|
|
56814
|
-
}
|
|
56815
|
-
},
|
|
56816
54962
|
{
|
|
56817
54963
|
key: "react-doctor/no-indeterminate-attribute",
|
|
56818
54964
|
id: "no-indeterminate-attribute",
|
|
@@ -57229,18 +55375,6 @@ const reactDoctorRules = [
|
|
|
57229
55375
|
requires: [...new Set(["react", ...noPropCallbackInEffect.requires ?? []])]
|
|
57230
55376
|
}
|
|
57231
55377
|
},
|
|
57232
|
-
{
|
|
57233
|
-
key: "react-doctor/no-prop-callback-in-render",
|
|
57234
|
-
id: "no-prop-callback-in-render",
|
|
57235
|
-
source: "react-doctor",
|
|
57236
|
-
originallyExternal: false,
|
|
57237
|
-
rule: {
|
|
57238
|
-
...noPropCallbackInRender,
|
|
57239
|
-
framework: "global",
|
|
57240
|
-
category: "Bugs",
|
|
57241
|
-
requires: [...new Set(["react", ...noPropCallbackInRender.requires ?? []])]
|
|
57242
|
-
}
|
|
57243
|
-
},
|
|
57244
55378
|
{
|
|
57245
55379
|
key: "react-doctor/no-prop-types",
|
|
57246
55380
|
id: "no-prop-types",
|
|
@@ -57332,18 +55466,6 @@ const reactDoctorRules = [
|
|
|
57332
55466
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
57333
55467
|
}
|
|
57334
55468
|
},
|
|
57335
|
-
{
|
|
57336
|
-
key: "react-doctor/no-ref-current-in-render",
|
|
57337
|
-
id: "no-ref-current-in-render",
|
|
57338
|
-
source: "react-doctor",
|
|
57339
|
-
originallyExternal: false,
|
|
57340
|
-
rule: {
|
|
57341
|
-
...noRefCurrentInRender,
|
|
57342
|
-
framework: "global",
|
|
57343
|
-
category: "Bugs",
|
|
57344
|
-
requires: [...new Set(["react", ...noRefCurrentInRender.requires ?? []])]
|
|
57345
|
-
}
|
|
57346
|
-
},
|
|
57347
55469
|
{
|
|
57348
55470
|
key: "react-doctor/no-render-in-render",
|
|
57349
55471
|
id: "no-render-in-render",
|
|
@@ -57588,18 +55710,6 @@ const reactDoctorRules = [
|
|
|
57588
55710
|
requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
|
|
57589
55711
|
}
|
|
57590
55712
|
},
|
|
57591
|
-
{
|
|
57592
|
-
key: "react-doctor/no-unguarded-browser-global-in-render-or-hook-init",
|
|
57593
|
-
id: "no-unguarded-browser-global-in-render-or-hook-init",
|
|
57594
|
-
source: "react-doctor",
|
|
57595
|
-
originallyExternal: false,
|
|
57596
|
-
rule: {
|
|
57597
|
-
...noUnguardedBrowserGlobalInRenderOrHookInit,
|
|
57598
|
-
framework: "global",
|
|
57599
|
-
category: "Bugs",
|
|
57600
|
-
requires: [...new Set(["react", ...noUnguardedBrowserGlobalInRenderOrHookInit.requires ?? []])]
|
|
57601
|
-
}
|
|
57602
|
-
},
|
|
57603
55713
|
{
|
|
57604
55714
|
key: "react-doctor/no-unknown-property",
|
|
57605
55715
|
id: "no-unknown-property",
|
|
@@ -59932,17 +58042,10 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
59932
58042
|
"nextjs-no-use-search-params-without-suspense",
|
|
59933
58043
|
"no-dynamic-import-path",
|
|
59934
58044
|
"no-full-lodash-import",
|
|
59935
|
-
"no-hydration-branch-on-browser-global",
|
|
59936
58045
|
"no-indeterminate-attribute",
|
|
59937
58046
|
"no-locale-format-in-render",
|
|
59938
58047
|
"no-match-media-in-state-initializer",
|
|
59939
|
-
"no-adjust-state-on-prop-change",
|
|
59940
|
-
"no-derived-state",
|
|
59941
|
-
"no-derived-state-effect",
|
|
59942
|
-
"no-event-handler",
|
|
59943
|
-
"no-initialize-state",
|
|
59944
58048
|
"no-mutating-reducer-state",
|
|
59945
|
-
"no-unguarded-browser-global-in-render-or-hook-init",
|
|
59946
58049
|
"prefer-dynamic-import",
|
|
59947
58050
|
"rendering-hydration-mismatch-time",
|
|
59948
58051
|
"rn-no-legacy-shadow-styles",
|
|
@@ -59994,9 +58097,6 @@ const collectNextjsSearchParamsDependencies = ({ absoluteFilePath, staticImports
|
|
|
59994
58097
|
if (hasAncestorSuspenseLayout(absoluteFilePath)) return;
|
|
59995
58098
|
for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
|
|
59996
58099
|
};
|
|
59997
|
-
const collectEffectValueHelperDependencies = ({ absoluteFilePath, staticImports }) => {
|
|
59998
|
-
for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
|
|
59999
|
-
};
|
|
60000
58100
|
const collectMutatingReducerDependencies = ({ absoluteFilePath, sourceText, staticImports, program }) => {
|
|
60001
58101
|
const namedUseReducerLocals = /* @__PURE__ */ new Set();
|
|
60002
58102
|
const reactObjectLocals = /* @__PURE__ */ new Set();
|
|
@@ -60079,17 +58179,10 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
60079
58179
|
["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
|
|
60080
58180
|
["no-dynamic-import-path", collectNearestManifestDependencies],
|
|
60081
58181
|
["no-full-lodash-import", collectNearestManifestDependencies],
|
|
60082
|
-
["no-hydration-branch-on-browser-global", collectNearestManifestDependencies],
|
|
60083
58182
|
["no-indeterminate-attribute", collectNearestManifestDependencies],
|
|
60084
58183
|
["no-locale-format-in-render", collectNearestManifestDependencies],
|
|
60085
58184
|
["no-match-media-in-state-initializer", collectNearestManifestDependencies],
|
|
60086
|
-
["no-adjust-state-on-prop-change", collectEffectValueHelperDependencies],
|
|
60087
|
-
["no-derived-state", collectEffectValueHelperDependencies],
|
|
60088
|
-
["no-derived-state-effect", collectEffectValueHelperDependencies],
|
|
60089
|
-
["no-event-handler", collectEffectValueHelperDependencies],
|
|
60090
|
-
["no-initialize-state", collectEffectValueHelperDependencies],
|
|
60091
58185
|
["no-mutating-reducer-state", collectMutatingReducerDependencies],
|
|
60092
|
-
["no-unguarded-browser-global-in-render-or-hook-init", collectNearestManifestDependencies],
|
|
60093
58186
|
["prefer-dynamic-import", collectNearestManifestDependencies],
|
|
60094
58187
|
["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
|
|
60095
58188
|
["rn-no-legacy-shadow-styles", collectLegacyArchDependencies],
|