oxlint-plugin-react-doctor 0.7.4-dev.a31f5e8 → 0.7.4-dev.b686594
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 -93
- package/dist/index.js +706 -1944
- 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
|
|
@@ -7675,21 +7672,6 @@ const readStaticBoolean = (node) => {
|
|
|
7675
7672
|
return unwrappedNode.value;
|
|
7676
7673
|
};
|
|
7677
7674
|
//#endregion
|
|
7678
|
-
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
7679
|
-
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
7680
|
-
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
7681
|
-
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
7682
|
-
let symbol = scopes.symbolFor(identifier);
|
|
7683
|
-
while (symbol?.kind === "const") {
|
|
7684
|
-
if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
7685
|
-
visitedSymbolIds.add(symbol.id);
|
|
7686
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
7687
|
-
if (!isNodeOfType(initializer, "Identifier")) return symbol;
|
|
7688
|
-
symbol = scopes.symbolFor(initializer);
|
|
7689
|
-
}
|
|
7690
|
-
return symbol;
|
|
7691
|
-
};
|
|
7692
|
-
//#endregion
|
|
7693
7675
|
//#region src/plugin/utils/is-react-api-call.ts
|
|
7694
7676
|
const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
|
|
7695
7677
|
const isImportedFromReact = (symbol) => {
|
|
@@ -7705,7 +7687,8 @@ const isNamedReactApiImport = (identifier, apiNames, scopes) => {
|
|
|
7705
7687
|
return Boolean(importedName && includesApiName(apiNames, importedName));
|
|
7706
7688
|
};
|
|
7707
7689
|
const isReactNamespaceImport = (identifier, scopes) => {
|
|
7708
|
-
|
|
7690
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
7691
|
+
const symbol = scopes.symbolFor(identifier);
|
|
7709
7692
|
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
7710
7693
|
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
7711
7694
|
};
|
|
@@ -7716,11 +7699,9 @@ const isReactApiCall = (node, apiNames, scopes, options = {}) => {
|
|
|
7716
7699
|
if (isNamedReactApiImport(callee, apiNames, scopes)) return true;
|
|
7717
7700
|
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
7718
7701
|
}
|
|
7719
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
if (isReactNamespaceImport(receiver, scopes)) return true;
|
|
7723
|
-
return Boolean(options.allowGlobalReactNamespace && receiver.name === "React" && scopes.isGlobalReference(receiver));
|
|
7702
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
|
|
7703
|
+
if (isReactNamespaceImport(callee.object, scopes)) return true;
|
|
7704
|
+
return Boolean(options.allowGlobalReactNamespace && callee.object.name === "React" && scopes.isGlobalReference(callee.object));
|
|
7724
7705
|
};
|
|
7725
7706
|
//#endregion
|
|
7726
7707
|
//#region src/plugin/rules/state-and-effects/utils/build-listener-cleanup-mismatch-message.ts
|
|
@@ -8406,54 +8387,6 @@ const effectListenerCleanupMismatch = defineRule({
|
|
|
8406
8387
|
} })
|
|
8407
8388
|
});
|
|
8408
8389
|
//#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
8390
|
//#region src/plugin/utils/is-react-hook-name.ts
|
|
8458
8391
|
const isReactHookName = (name) => {
|
|
8459
8392
|
if (!name.startsWith("use")) return false;
|
|
@@ -8509,90 +8442,46 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
8509
8442
|
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
8510
8443
|
};
|
|
8511
8444
|
//#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
8445
|
//#region src/plugin/utils/get-range-start.ts
|
|
8589
8446
|
const getRangeStart = (node) => {
|
|
8590
8447
|
const rangeStart = node.range?.[0];
|
|
8591
8448
|
return typeof rangeStart === "number" ? rangeStart : null;
|
|
8592
8449
|
};
|
|
8593
8450
|
//#endregion
|
|
8594
|
-
//#region src/plugin/utils/is-
|
|
8595
|
-
const
|
|
8451
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
8452
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
8453
|
+
let node = callExpression;
|
|
8454
|
+
let parent = node.parent;
|
|
8455
|
+
while (parent) {
|
|
8456
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
8457
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
8458
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
8459
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
8460
|
+
node = parent;
|
|
8461
|
+
parent = node.parent;
|
|
8462
|
+
continue;
|
|
8463
|
+
}
|
|
8464
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
8465
|
+
node = parent;
|
|
8466
|
+
parent = node.parent;
|
|
8467
|
+
continue;
|
|
8468
|
+
}
|
|
8469
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
8470
|
+
node = parent;
|
|
8471
|
+
parent = node.parent;
|
|
8472
|
+
continue;
|
|
8473
|
+
}
|
|
8474
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
8475
|
+
const expressions = parent.expressions ?? [];
|
|
8476
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
8477
|
+
node = parent;
|
|
8478
|
+
parent = node.parent;
|
|
8479
|
+
continue;
|
|
8480
|
+
}
|
|
8481
|
+
return false;
|
|
8482
|
+
}
|
|
8483
|
+
return false;
|
|
8484
|
+
};
|
|
8596
8485
|
//#endregion
|
|
8597
8486
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
8598
8487
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
@@ -8627,9 +8516,125 @@ const isCleanupReturningSubscribeLikeCallExpression = (node) => {
|
|
|
8627
8516
|
return true;
|
|
8628
8517
|
};
|
|
8629
8518
|
//#endregion
|
|
8519
|
+
//#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
|
|
8520
|
+
const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
|
|
8521
|
+
"each",
|
|
8522
|
+
"every",
|
|
8523
|
+
"filter",
|
|
8524
|
+
"find",
|
|
8525
|
+
"findIndex",
|
|
8526
|
+
"findLast",
|
|
8527
|
+
"findLastIndex",
|
|
8528
|
+
"flatMap",
|
|
8529
|
+
"forEach",
|
|
8530
|
+
"map",
|
|
8531
|
+
"reduce",
|
|
8532
|
+
"reduceRight",
|
|
8533
|
+
"some",
|
|
8534
|
+
"sort",
|
|
8535
|
+
"toSorted"
|
|
8536
|
+
]);
|
|
8537
|
+
const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
8538
|
+
"from",
|
|
8539
|
+
"fromAsync",
|
|
8540
|
+
"groupBy"
|
|
8541
|
+
]);
|
|
8542
|
+
const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8543
|
+
const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
|
|
8544
|
+
const isListenerRemovalViaNullHandler = (callNode) => {
|
|
8545
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
8546
|
+
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8547
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
|
|
8548
|
+
};
|
|
8549
|
+
const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
|
|
8550
|
+
"off",
|
|
8551
|
+
"removeEventListener",
|
|
8552
|
+
"removeListener"
|
|
8553
|
+
]);
|
|
8554
|
+
const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
|
|
8555
|
+
if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
|
|
8556
|
+
const handlerArgument = callNode.arguments?.[1];
|
|
8557
|
+
return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
|
|
8558
|
+
};
|
|
8559
|
+
const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8560
|
+
const callNode = unwrapChainExpression$3(node);
|
|
8561
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
8562
|
+
if (isListenerRemovalViaNullHandler(callNode)) return true;
|
|
8563
|
+
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8564
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
8565
|
+
if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
|
|
8566
|
+
if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
|
|
8567
|
+
if (knownCleanupFunctionNames.has(callee.name)) return true;
|
|
8568
|
+
return false;
|
|
8569
|
+
}
|
|
8570
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
8571
|
+
if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
|
|
8572
|
+
if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
|
|
8573
|
+
return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
|
|
8574
|
+
}
|
|
8575
|
+
return false;
|
|
8576
|
+
};
|
|
8577
|
+
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);
|
|
8578
|
+
const isIteratorCallbackArgument = (node) => {
|
|
8579
|
+
const parentNode = node.parent;
|
|
8580
|
+
if (!isNodeOfType(parentNode, "CallExpression")) return false;
|
|
8581
|
+
if (!parentNode.arguments?.some((argument) => argument === node)) return false;
|
|
8582
|
+
const callee = unwrapChainExpression$3(parentNode.callee);
|
|
8583
|
+
if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
|
|
8584
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(callee.property.name);
|
|
8585
|
+
};
|
|
8586
|
+
const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8587
|
+
let didFindRelease = false;
|
|
8588
|
+
walkAst(node, (child) => {
|
|
8589
|
+
if (didFindRelease) return false;
|
|
8590
|
+
if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
|
|
8591
|
+
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
8592
|
+
didFindRelease = true;
|
|
8593
|
+
return false;
|
|
8594
|
+
}
|
|
8595
|
+
});
|
|
8596
|
+
return didFindRelease;
|
|
8597
|
+
};
|
|
8598
|
+
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8599
|
+
if (!isFunctionLike$1(node)) return false;
|
|
8600
|
+
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
8601
|
+
};
|
|
8602
|
+
const isProvablyNoOpCleanupFunction = (node) => {
|
|
8603
|
+
if (!isFunctionLike$1(node)) return false;
|
|
8604
|
+
let sawNoOpRemoval = false;
|
|
8605
|
+
let sawOtherCall = false;
|
|
8606
|
+
walkAst(node.body, (child) => {
|
|
8607
|
+
if (sawOtherCall) return false;
|
|
8608
|
+
if (isFunctionLike$1(child)) return false;
|
|
8609
|
+
const callNode = unwrapChainExpression$3(child);
|
|
8610
|
+
if (!isNodeOfType(callNode, "CallExpression")) return;
|
|
8611
|
+
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8612
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
|
|
8613
|
+
sawNoOpRemoval = true;
|
|
8614
|
+
return;
|
|
8615
|
+
}
|
|
8616
|
+
sawOtherCall = true;
|
|
8617
|
+
});
|
|
8618
|
+
return sawNoOpRemoval && !sawOtherCall;
|
|
8619
|
+
};
|
|
8620
|
+
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
|
|
8621
|
+
if (!returnedValue) return false;
|
|
8622
|
+
const unwrappedValue = unwrapChainExpression$3(returnedValue);
|
|
8623
|
+
if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
|
|
8624
|
+
if (isNodeOfType(unwrappedValue, "Identifier")) {
|
|
8625
|
+
if (unwrappedValue.name === "undefined") return false;
|
|
8626
|
+
if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
|
|
8627
|
+
return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
|
|
8628
|
+
}
|
|
8629
|
+
if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
8630
|
+
if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
|
|
8631
|
+
if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
8632
|
+
if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
|
|
8633
|
+
return false;
|
|
8634
|
+
};
|
|
8635
|
+
//#endregion
|
|
8630
8636
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
8631
8637
|
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
8632
|
-
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
8633
8638
|
const RESOURCE_NOUN_BY_KIND = {
|
|
8634
8639
|
subscribe: "subscription",
|
|
8635
8640
|
timer: "timer",
|
|
@@ -8640,66 +8645,7 @@ const isSubscribeOrObserveCall = (node) => {
|
|
|
8640
8645
|
if (isSubscribeLikeCallExpression(node)) return true;
|
|
8641
8646
|
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
8642
8647
|
};
|
|
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) => {
|
|
8648
|
+
const findSubscribeLikeUsages = (callback) => {
|
|
8703
8649
|
const usages = [];
|
|
8704
8650
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8705
8651
|
let cleanupArgument = null;
|
|
@@ -8708,22 +8654,13 @@ const findSubscribeLikeUsages = (callback, context) => {
|
|
|
8708
8654
|
const lastCallbackStatement = callbackStatements[callbackStatements.length - 1];
|
|
8709
8655
|
if (isNodeOfType(lastCallbackStatement, "ReturnStatement") && lastCallbackStatement.argument) cleanupArgument = lastCallbackStatement.argument;
|
|
8710
8656
|
}
|
|
8711
|
-
const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
|
|
8712
8657
|
walkAst(callback, (child) => {
|
|
8713
|
-
if (child
|
|
8714
|
-
if (child === cleanupArgument) return false;
|
|
8715
|
-
if (!effectInvokedFunctions.has(child) && !isSynchronousIteratorCallback(child)) return false;
|
|
8716
|
-
}
|
|
8658
|
+
if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
|
|
8717
8659
|
if (isSocketConstruction(child)) {
|
|
8718
8660
|
usages.push({
|
|
8719
8661
|
kind: "socket",
|
|
8720
8662
|
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
|
|
8663
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8727
8664
|
});
|
|
8728
8665
|
return;
|
|
8729
8666
|
}
|
|
@@ -8732,348 +8669,118 @@ const findSubscribeLikeUsages = (callback, context) => {
|
|
|
8732
8669
|
usages.push({
|
|
8733
8670
|
kind: "timer",
|
|
8734
8671
|
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
|
|
8672
|
+
resourceName: child.callee.name
|
|
8741
8673
|
});
|
|
8742
8674
|
return;
|
|
8743
8675
|
}
|
|
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
|
-
|
|
8676
|
+
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({
|
|
8677
|
+
kind: "subscribe",
|
|
8678
|
+
node: child,
|
|
8679
|
+
resourceName: child.callee.property.name
|
|
8680
|
+
});
|
|
8681
|
+
});
|
|
8682
|
+
return usages;
|
|
8683
|
+
};
|
|
8684
|
+
const collectCleanupBindings = (effectCallback) => {
|
|
8685
|
+
const bindings = {
|
|
8686
|
+
cleanupFunctionNames: /* @__PURE__ */ new Set(),
|
|
8687
|
+
subscriptionNames: /* @__PURE__ */ new Set(),
|
|
8688
|
+
effectScopeVariableNames: /* @__PURE__ */ new Set()
|
|
8689
|
+
};
|
|
8690
|
+
if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
|
|
8691
|
+
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
|
|
8692
|
+
walkAst(effectCallback.body, (child) => {
|
|
8693
|
+
if (!isSubscribeOrObserveCall(child)) return;
|
|
8694
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8695
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
8696
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
8697
|
+
bindings.subscriptionNames.add(child.callee.object.name);
|
|
8698
|
+
});
|
|
8699
|
+
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
8700
|
+
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
8701
|
+
for (const declarator of child.declarations ?? []) {
|
|
8702
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
8703
|
+
const bindingName = declarator.id.name;
|
|
8704
|
+
bindings.effectScopeVariableNames.add(bindingName);
|
|
8705
|
+
const init = declarator.init;
|
|
8706
|
+
if (!init) continue;
|
|
8707
|
+
if (isSocketConstruction(init)) {
|
|
8708
|
+
bindings.subscriptionNames.add(bindingName);
|
|
8709
|
+
continue;
|
|
8710
|
+
}
|
|
8711
|
+
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
8712
|
+
if (isSubscribeLikeCallExpression(init)) {
|
|
8713
|
+
bindings.subscriptionNames.add(bindingName);
|
|
8714
|
+
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
8715
|
+
}
|
|
8753
8716
|
}
|
|
8754
8717
|
});
|
|
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) => {
|
|
8718
|
+
walkAst(effectCallback.body, (child) => {
|
|
8719
|
+
if (child !== effectCallback.body && (isNodeOfType(child, "ArrowFunctionExpression") || isNodeOfType(child, "FunctionExpression"))) return false;
|
|
8720
|
+
if (isNodeOfType(child, "FunctionDeclaration") && child.id && isCleanupFunctionLike(child, bindings.cleanupFunctionNames, bindings.subscriptionNames)) {
|
|
8721
|
+
bindings.cleanupFunctionNames.add(child.id.name);
|
|
8722
|
+
return false;
|
|
8723
|
+
}
|
|
8724
|
+
});
|
|
8725
|
+
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
8726
|
+
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
8727
|
+
for (const declarator of child.declarations ?? []) {
|
|
8728
|
+
if (!isNodeOfType(declarator.id, "Identifier") || !declarator.init) continue;
|
|
8729
|
+
if (isCleanupFunctionLike(declarator.init, bindings.cleanupFunctionNames, bindings.subscriptionNames)) bindings.cleanupFunctionNames.add(declarator.id.name);
|
|
8730
|
+
}
|
|
8731
|
+
});
|
|
8732
|
+
walkAst(effectCallback.body, (child) => {
|
|
8733
|
+
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);
|
|
8734
|
+
});
|
|
8735
|
+
return bindings;
|
|
8736
|
+
};
|
|
8737
|
+
const removeSynchronouslyReleasedUsages = (callback, usages) => {
|
|
8820
8738
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8821
8739
|
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
8822
|
-
const
|
|
8740
|
+
const releaseStarts = [];
|
|
8823
8741
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8824
|
-
if (!
|
|
8825
|
-
|
|
8742
|
+
if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
|
|
8743
|
+
const releaseStart = getRangeStart(child);
|
|
8744
|
+
if (releaseStart !== null) releaseStarts.push(releaseStart);
|
|
8826
8745
|
});
|
|
8827
|
-
if (
|
|
8746
|
+
if (releaseStarts.length === 0) return usages;
|
|
8828
8747
|
return usages.filter((usage) => {
|
|
8829
8748
|
const usageStart = getRangeStart(usage.node);
|
|
8830
8749
|
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);
|
|
8750
|
+
return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
|
|
8836
8751
|
});
|
|
8837
8752
|
};
|
|
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
|
-
}
|
|
8753
|
+
const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
|
|
8754
|
+
if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
|
|
8755
|
+
const returnStart = getRangeStart(returnStatement);
|
|
8756
|
+
if (returnStart === null) return true;
|
|
8757
|
+
return usages.some((usage) => {
|
|
8758
|
+
const usageStart = getRangeStart(usage.node);
|
|
8759
|
+
return usageStart === null || usageStart < returnStart;
|
|
9016
8760
|
});
|
|
9017
|
-
return didCleanupFunctionMatch;
|
|
9018
8761
|
};
|
|
9019
|
-
const
|
|
8762
|
+
const effectHasCleanupReturn = (callback, usages) => {
|
|
9020
8763
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
9021
|
-
if (
|
|
9022
|
-
|
|
9023
|
-
|
|
8764
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return isCleanupReturningSubscribeLikeCallExpression(callback.body);
|
|
8765
|
+
const cleanupBindings = collectCleanupBindings(callback);
|
|
8766
|
+
let didFindCleanupReturn = false;
|
|
9024
8767
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8768
|
+
if (didFindCleanupReturn) return;
|
|
9025
8769
|
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);
|
|
8770
|
+
if (!cleanupReturnRunsAfterUsage(child, usages)) return;
|
|
8771
|
+
if (isCleanupReturn(child.argument, cleanupBindings.cleanupFunctionNames, cleanupBindings.subscriptionNames, { allowOpaqueReturn: true })) didFindCleanupReturn = true;
|
|
9048
8772
|
});
|
|
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));
|
|
8773
|
+
return didFindCleanupReturn;
|
|
9068
8774
|
};
|
|
9069
|
-
const
|
|
8775
|
+
const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
|
|
8776
|
+
const isSelfReleasingListenerOptionProperty = (property) => {
|
|
9070
8777
|
if (!isNodeOfType(property, "Property")) return false;
|
|
9071
8778
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
|
|
9072
|
-
if (keyName === "signal") return
|
|
8779
|
+
if (keyName === "signal") return true;
|
|
9073
8780
|
if (keyName !== "once") return false;
|
|
9074
8781
|
return isNodeOfType(property.value, "Literal") && property.value.value === true;
|
|
9075
8782
|
};
|
|
9076
|
-
const hasSelfReleasingListenerOptions = (node
|
|
8783
|
+
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
|
|
9077
8784
|
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
9078
8785
|
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
9079
8786
|
["addListener", new Set([
|
|
@@ -9099,185 +8806,85 @@ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
|
9099
8806
|
"teardown"
|
|
9100
8807
|
]);
|
|
9101
8808
|
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8809
|
+
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
9102
8810
|
const getReleaseVerbName = (node) => {
|
|
8811
|
+
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
9103
8812
|
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
9104
8813
|
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
9105
8814
|
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
|
-
}
|
|
8815
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8816
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
9111
8817
|
return null;
|
|
9112
8818
|
};
|
|
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
8819
|
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;
|
|
8820
|
+
const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
|
|
8821
|
+
let didFindPairedRelease = false;
|
|
8822
|
+
walkAst(body, (child) => {
|
|
8823
|
+
if (didFindPairedRelease) return false;
|
|
8824
|
+
if (child !== body && isFunctionLike$1(child)) return false;
|
|
8825
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8826
|
+
if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
|
|
8827
|
+
didFindPairedRelease = true;
|
|
9189
8828
|
return false;
|
|
9190
8829
|
}
|
|
9191
8830
|
});
|
|
9192
|
-
return
|
|
8831
|
+
return didFindPairedRelease;
|
|
9193
8832
|
};
|
|
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);
|
|
8833
|
+
const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
|
|
8834
|
+
const collectFileReleaseVerbNames = (anyNode) => {
|
|
8835
|
+
let programNode = anyNode;
|
|
8836
|
+
while (programNode.parent) programNode = programNode.parent;
|
|
8837
|
+
const cached = fileReleaseVerbNamesCache.get(programNode);
|
|
8838
|
+
if (cached) return cached;
|
|
8839
|
+
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
8840
|
+
walkAst(programNode, (child) => {
|
|
8841
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8842
|
+
if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
|
|
8843
|
+
});
|
|
8844
|
+
fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
|
|
8845
|
+
return releaseVerbNames;
|
|
9211
8846
|
};
|
|
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
|
-
}
|
|
8847
|
+
const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
|
|
8848
|
+
const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
|
|
8849
|
+
const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
|
|
8850
|
+
if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
|
|
8851
|
+
for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
|
|
9225
8852
|
return false;
|
|
9226
8853
|
};
|
|
9227
|
-
const findRetainedFunctionLeak = (retainedFunction
|
|
8854
|
+
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
9228
8855
|
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
9229
8856
|
const body = retainedFunction.body;
|
|
9230
8857
|
if (!body) return null;
|
|
8858
|
+
const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
|
|
9231
8859
|
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
8860
|
walkAst(body, (child) => {
|
|
9236
8861
|
if (leak !== null) return false;
|
|
9237
8862
|
if (isFunctionLike$1(child)) return false;
|
|
9238
|
-
if (
|
|
9239
|
-
|
|
8863
|
+
if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
|
|
8864
|
+
if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
|
|
8865
|
+
leak = {
|
|
9240
8866
|
kind: "socket",
|
|
9241
8867
|
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
|
|
8868
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
9248
8869
|
};
|
|
9249
|
-
|
|
9250
|
-
leak = socketUsage;
|
|
9251
|
-
return false;
|
|
9252
|
-
}
|
|
8870
|
+
return false;
|
|
9253
8871
|
}
|
|
9254
8872
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
9255
|
-
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && !
|
|
9256
|
-
|
|
8873
|
+
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
|
|
8874
|
+
leak = {
|
|
9257
8875
|
kind: "timer",
|
|
9258
8876
|
node: child,
|
|
9259
|
-
resourceName: "setInterval"
|
|
9260
|
-
handleKey: findAssignedResourceKey(child, context),
|
|
9261
|
-
receiverKey: null,
|
|
9262
|
-
registrationVerbName: "setInterval",
|
|
9263
|
-
eventKey: null,
|
|
9264
|
-
handlerKey: null
|
|
8877
|
+
resourceName: "setInterval"
|
|
9265
8878
|
};
|
|
9266
|
-
|
|
9267
|
-
leak = timerUsage;
|
|
9268
|
-
return false;
|
|
9269
|
-
}
|
|
8879
|
+
return false;
|
|
9270
8880
|
}
|
|
9271
|
-
if (isSubscribeOrObserveCall(child) &&
|
|
9272
|
-
const
|
|
9273
|
-
|
|
8881
|
+
if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
|
|
8882
|
+
const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
|
|
8883
|
+
if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
|
|
9274
8884
|
kind: "subscribe",
|
|
9275
8885
|
node: child,
|
|
9276
|
-
resourceName:
|
|
9277
|
-
handleKey: findAssignedResourceKey(child, context),
|
|
9278
|
-
...registrationDetails
|
|
8886
|
+
resourceName: registrationVerbName
|
|
9279
8887
|
};
|
|
9280
|
-
if (!hasSelfReleasingListenerOptions(child, context) && !hasReleaseForUsage(subscriptionUsage)) leak = subscriptionUsage;
|
|
9281
8888
|
return false;
|
|
9282
8889
|
}
|
|
9283
8890
|
});
|
|
@@ -9289,26 +8896,6 @@ const isRetainedComponentScopeFunction = (functionNode) => {
|
|
|
9289
8896
|
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
9290
8897
|
return enclosingComponentOrHookName(functionNode) !== null;
|
|
9291
8898
|
};
|
|
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
8899
|
const effectNeedsCleanup = defineRule({
|
|
9313
8900
|
id: "effect-needs-cleanup",
|
|
9314
8901
|
title: "Effect subscription or timer never cleaned up",
|
|
@@ -9317,8 +8904,7 @@ const effectNeedsCleanup = defineRule({
|
|
|
9317
8904
|
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
8905
|
create: (context) => {
|
|
9319
8906
|
const reportRetainedLeak = (retainedFunction) => {
|
|
9320
|
-
|
|
9321
|
-
const leak = findRetainedFunctionLeak(retainedFunction, context);
|
|
8907
|
+
const leak = findRetainedFunctionLeak(retainedFunction);
|
|
9322
8908
|
if (!leak) return;
|
|
9323
8909
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
9324
8910
|
context.report({
|
|
@@ -9330,31 +8916,30 @@ const effectNeedsCleanup = defineRule({
|
|
|
9330
8916
|
CallExpression(node) {
|
|
9331
8917
|
if (isHookCall$2(node, "useCallback")) {
|
|
9332
8918
|
const retainedCallback = getEffectCallback(node);
|
|
9333
|
-
if (retainedCallback
|
|
8919
|
+
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
9334
8920
|
return;
|
|
9335
8921
|
}
|
|
9336
|
-
if (!isHookCall$2(node,
|
|
8922
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
9337
8923
|
const callback = getEffectCallback(node);
|
|
9338
8924
|
if (!callback) return;
|
|
9339
|
-
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback
|
|
8925
|
+
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
|
|
9340
8926
|
if (usages.length === 0) return;
|
|
9341
|
-
|
|
9342
|
-
|
|
8927
|
+
if (effectHasCleanupReturn(callback, usages)) return;
|
|
8928
|
+
const firstUsage = usages[0];
|
|
9343
8929
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
9344
|
-
const hookName = getCalleeName$2(node) ?? "effect";
|
|
9345
8930
|
context.report({
|
|
9346
8931
|
node,
|
|
9347
|
-
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in
|
|
8932
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
9348
8933
|
});
|
|
9349
8934
|
},
|
|
9350
8935
|
FunctionDeclaration(node) {
|
|
9351
8936
|
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9352
8937
|
},
|
|
9353
8938
|
ArrowFunctionExpression(node) {
|
|
9354
|
-
if (isRetainedComponentScopeFunction(node)
|
|
8939
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9355
8940
|
},
|
|
9356
8941
|
FunctionExpression(node) {
|
|
9357
|
-
if (isRetainedComponentScopeFunction(node)
|
|
8942
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9358
8943
|
}
|
|
9359
8944
|
};
|
|
9360
8945
|
}
|
|
@@ -10271,14 +9856,35 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
10271
9856
|
};
|
|
10272
9857
|
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
10273
9858
|
//#endregion
|
|
9859
|
+
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
9860
|
+
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
9861
|
+
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
9862
|
+
if (importSource === null) return false;
|
|
9863
|
+
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
9864
|
+
};
|
|
9865
|
+
//#endregion
|
|
9866
|
+
//#region src/plugin/utils/is-non-react-effect-event-callee.ts
|
|
9867
|
+
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
9868
|
+
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
9869
|
+
return Boolean(symbol && symbol.kind !== "import");
|
|
9870
|
+
};
|
|
9871
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
|
|
9872
|
+
if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
|
|
9873
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
|
|
9874
|
+
return false;
|
|
9875
|
+
};
|
|
9876
|
+
//#endregion
|
|
10274
9877
|
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
9878
|
+
const getUseEffectEventCalleeName = (callee) => {
|
|
9879
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
9880
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
9881
|
+
return null;
|
|
9882
|
+
};
|
|
10275
9883
|
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
10276
9884
|
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
10277
9885
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
10278
|
-
|
|
10279
|
-
|
|
10280
|
-
allowUnboundBareCalls: true
|
|
10281
|
-
});
|
|
9886
|
+
if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
|
|
9887
|
+
return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
|
|
10282
9888
|
};
|
|
10283
9889
|
//#endregion
|
|
10284
9890
|
//#region src/plugin/rules/react-builtins/exhaustive-deps.ts
|
|
@@ -12738,6 +12344,20 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
12738
12344
|
return names;
|
|
12739
12345
|
};
|
|
12740
12346
|
//#endregion
|
|
12347
|
+
//#region src/plugin/utils/get-function-binding-name.ts
|
|
12348
|
+
const getFunctionBindingIdentifier = (functionNode) => {
|
|
12349
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
12350
|
+
const parent = functionNode.parent;
|
|
12351
|
+
if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
|
|
12352
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionNode && isNodeOfType(parent.left, "Identifier")) return parent.left;
|
|
12353
|
+
if (isNodeOfType(parent, "CallExpression")) {
|
|
12354
|
+
const callParent = parent.parent;
|
|
12355
|
+
if (isNodeOfType(callParent, "VariableDeclarator") && isNodeOfType(callParent.id, "Identifier")) return callParent.id;
|
|
12356
|
+
}
|
|
12357
|
+
return null;
|
|
12358
|
+
};
|
|
12359
|
+
const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(functionNode)?.name ?? null;
|
|
12360
|
+
//#endregion
|
|
12741
12361
|
//#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
|
|
12742
12362
|
const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
|
|
12743
12363
|
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
@@ -13882,7 +13502,7 @@ const jsHoistIntl = defineRule({
|
|
|
13882
13502
|
});
|
|
13883
13503
|
//#endregion
|
|
13884
13504
|
//#region src/plugin/utils/create-loop-aware-visitors.ts
|
|
13885
|
-
const ITERATOR_CALLBACK_METHOD_NAMES
|
|
13505
|
+
const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
13886
13506
|
"map",
|
|
13887
13507
|
"flatMap",
|
|
13888
13508
|
"forEach",
|
|
@@ -13901,7 +13521,7 @@ const isIteratorCallback = (node) => {
|
|
|
13901
13521
|
const parent = node.parent;
|
|
13902
13522
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
13903
13523
|
if (!parent.arguments.includes(node)) return false;
|
|
13904
|
-
return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES
|
|
13524
|
+
return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(parent.callee.property.name);
|
|
13905
13525
|
};
|
|
13906
13526
|
const createLoopAwareVisitors = (innerVisitors, options = {}) => {
|
|
13907
13527
|
let loopDepth = 0;
|
|
@@ -14130,7 +13750,7 @@ const findIndexedArrayObject = (callbackBody, indexParameterName) => {
|
|
|
14130
13750
|
});
|
|
14131
13751
|
return indexedArrayObject;
|
|
14132
13752
|
};
|
|
14133
|
-
const unwrapChainExpression$
|
|
13753
|
+
const unwrapChainExpression$2 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
14134
13754
|
const LENGTH_EQUALITY_OPERATORS = new Set(["===", "=="]);
|
|
14135
13755
|
const LENGTH_MISMATCH_OPERATORS = new Set([
|
|
14136
13756
|
"!==",
|
|
@@ -14142,17 +13762,17 @@ const LENGTH_MISMATCH_OPERATORS = new Set([
|
|
|
14142
13762
|
]);
|
|
14143
13763
|
const LENGTH_ANY_COMPARISON_OPERATORS = new Set([...LENGTH_EQUALITY_OPERATORS, ...LENGTH_MISMATCH_OPERATORS]);
|
|
14144
13764
|
const isLengthComparison = (candidate, receiverArray, indexedArray, operators) => {
|
|
14145
|
-
const binaryGuard = unwrapChainExpression$
|
|
13765
|
+
const binaryGuard = unwrapChainExpression$2(candidate);
|
|
14146
13766
|
if (!isNodeOfType(binaryGuard, "BinaryExpression")) return false;
|
|
14147
13767
|
if (!operators.has(binaryGuard.operator)) return false;
|
|
14148
|
-
const leftSide = unwrapChainExpression$
|
|
14149
|
-
const rightSide = unwrapChainExpression$
|
|
13768
|
+
const leftSide = unwrapChainExpression$2(binaryGuard.left);
|
|
13769
|
+
const rightSide = unwrapChainExpression$2(binaryGuard.right);
|
|
14150
13770
|
if (!isMemberProperty(leftSide, "length")) return false;
|
|
14151
13771
|
if (!isMemberProperty(rightSide, "length")) return false;
|
|
14152
|
-
const leftLengthObject = unwrapChainExpression$
|
|
14153
|
-
const rightLengthObject = unwrapChainExpression$
|
|
14154
|
-
const normalizedReceiver = unwrapChainExpression$
|
|
14155
|
-
const normalizedIndexed = unwrapChainExpression$
|
|
13772
|
+
const leftLengthObject = unwrapChainExpression$2(leftSide.object);
|
|
13773
|
+
const rightLengthObject = unwrapChainExpression$2(rightSide.object);
|
|
13774
|
+
const normalizedReceiver = unwrapChainExpression$2(receiverArray);
|
|
13775
|
+
const normalizedIndexed = unwrapChainExpression$2(indexedArray);
|
|
14156
13776
|
const matchesReceiverThenIndexed = areExpressionsStructurallyEqual(leftLengthObject, normalizedReceiver) && areExpressionsStructurallyEqual(rightLengthObject, normalizedIndexed);
|
|
14157
13777
|
const matchesIndexedThenReceiver = areExpressionsStructurallyEqual(leftLengthObject, normalizedIndexed) && areExpressionsStructurallyEqual(rightLengthObject, normalizedReceiver);
|
|
14158
13778
|
return matchesReceiverThenIndexed || matchesIndexedThenReceiver;
|
|
@@ -14239,18 +13859,18 @@ const LENGTH_PRESERVING_METHOD_NAMES = new Set([
|
|
|
14239
13859
|
"toReversed"
|
|
14240
13860
|
]);
|
|
14241
13861
|
const peelLengthPreservingDerivation = (expression) => {
|
|
14242
|
-
let current = unwrapChainExpression$
|
|
13862
|
+
let current = unwrapChainExpression$2(expression);
|
|
14243
13863
|
for (;;) {
|
|
14244
13864
|
if (isNodeOfType(current, "ArrayExpression") && current.elements?.length === 1 && isNodeOfType(current.elements[0], "SpreadElement")) {
|
|
14245
|
-
current = unwrapChainExpression$
|
|
13865
|
+
current = unwrapChainExpression$2(current.elements[0].argument);
|
|
14246
13866
|
continue;
|
|
14247
13867
|
}
|
|
14248
13868
|
if (isNodeOfType(current, "CallExpression")) {
|
|
14249
|
-
const callee = unwrapChainExpression$
|
|
13869
|
+
const callee = unwrapChainExpression$2(current.callee);
|
|
14250
13870
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
14251
|
-
const calleeObject = unwrapChainExpression$
|
|
13871
|
+
const calleeObject = unwrapChainExpression$2(callee.object);
|
|
14252
13872
|
if (isNodeOfType(calleeObject, "Identifier") && calleeObject.name === "Array" && callee.property.name === "from" && current.arguments?.length === 1) {
|
|
14253
|
-
current = unwrapChainExpression$
|
|
13873
|
+
current = unwrapChainExpression$2(current.arguments[0]);
|
|
14254
13874
|
continue;
|
|
14255
13875
|
}
|
|
14256
13876
|
if (LENGTH_PRESERVING_METHOD_NAMES.has(callee.property.name)) {
|
|
@@ -14295,7 +13915,7 @@ const resolveComparedArraySource = (expression, scopeNode) => {
|
|
|
14295
13915
|
const initializer = findConstInitializer(current.name, scopeNode);
|
|
14296
13916
|
if (!initializer) return current;
|
|
14297
13917
|
const peeledInitializer = peelLengthPreservingDerivation(initializer);
|
|
14298
|
-
if (peeledInitializer === unwrapChainExpression$
|
|
13918
|
+
if (peeledInitializer === unwrapChainExpression$2(initializer)) return current;
|
|
14299
13919
|
current = peeledInitializer;
|
|
14300
13920
|
}
|
|
14301
13921
|
return current;
|
|
@@ -16311,190 +15931,6 @@ const memoStatusForJsxOpeningName = (registry, openingName) => {
|
|
|
16311
15931
|
return registry.get(openingName.name) ?? "unknown";
|
|
16312
15932
|
};
|
|
16313
15933
|
//#endregion
|
|
16314
|
-
//#region src/plugin/utils/unwrap-react-hoc-function.ts
|
|
16315
|
-
/**
|
|
16316
|
-
* Resolves a `VariableDeclarator.init` (or any expression) to the inline
|
|
16317
|
-
* function expression it binds, seeing through chains of `memo` /
|
|
16318
|
-
* `forwardRef` / `React.memo` / `React.forwardRef` wrappers:
|
|
16319
|
-
*
|
|
16320
|
-
* `() => {}` → the arrow
|
|
16321
|
-
* `memo(function Foo() {})` → the named function expression
|
|
16322
|
-
* `React.memo(forwardRef(() => {}))` → the inner arrow
|
|
16323
|
-
* `memo(SomeIdentifier)` → the identifier's same-file function when scopes are provided
|
|
16324
|
-
*
|
|
16325
|
-
* Component-shaped rules that previously gated on a direct function init
|
|
16326
|
-
* (via `isComponentAssignment`) use this so memo-wrapped components are
|
|
16327
|
-
* not silently skipped.
|
|
16328
|
-
*/
|
|
16329
|
-
const resolveReactHocFunction = (node, scopes, visitedSymbolIds) => {
|
|
16330
|
-
if (!node) return null;
|
|
16331
|
-
const current = stripParenExpression(node);
|
|
16332
|
-
if (isInlineFunctionExpression(current)) return current;
|
|
16333
|
-
if (isNodeOfType(current, "Identifier") && scopes) {
|
|
16334
|
-
const symbol = scopes.symbolFor(current);
|
|
16335
|
-
if (!symbol || visitedSymbolIds.has(symbol.id) || !symbol.initializer || symbol.kind !== "const" && symbol.kind !== "function") return null;
|
|
16336
|
-
if (symbol.kind === "const" && (!isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier)) return null;
|
|
16337
|
-
visitedSymbolIds.add(symbol.id);
|
|
16338
|
-
return resolveReactHocFunction(symbol.initializer, scopes, visitedSymbolIds);
|
|
16339
|
-
}
|
|
16340
|
-
if (isNodeOfType(current, "CallExpression")) {
|
|
16341
|
-
const calleeName = flattenCalleeName(current.callee);
|
|
16342
|
-
if (!calleeName || !REACT_HOC_NAMES.has(calleeName)) return null;
|
|
16343
|
-
const firstArgument = current.arguments[0];
|
|
16344
|
-
if (!firstArgument || isNodeOfType(firstArgument, "SpreadElement")) return null;
|
|
16345
|
-
return resolveReactHocFunction(firstArgument, scopes, visitedSymbolIds);
|
|
16346
|
-
}
|
|
16347
|
-
return null;
|
|
16348
|
-
};
|
|
16349
|
-
const unwrapReactHocFunction = (node, scopes) => resolveReactHocFunction(node, scopes, /* @__PURE__ */ new Set());
|
|
16350
|
-
//#endregion
|
|
16351
|
-
//#region src/plugin/utils/build-same-file-jsx-slot-prop-registry.ts
|
|
16352
|
-
const REACT_SLOT_TYPE_NAMES = new Set(["ReactNode", "ReactElement"]);
|
|
16353
|
-
const unwrapTopLevelDeclaration = (statement) => {
|
|
16354
|
-
if (isNodeOfType(statement, "ExportNamedDeclaration")) return statement.declaration;
|
|
16355
|
-
if (isNodeOfType(statement, "ExportDefaultDeclaration")) return statement.declaration;
|
|
16356
|
-
return statement;
|
|
16357
|
-
};
|
|
16358
|
-
const buildReactSlotTypeEnvironment = (program) => {
|
|
16359
|
-
const reactSlotTypeBindings = /* @__PURE__ */ new Set();
|
|
16360
|
-
const reactNamespaceBindings = /* @__PURE__ */ new Set();
|
|
16361
|
-
const reactJsxNamespaceBindings = /* @__PURE__ */ new Set();
|
|
16362
|
-
const nonReactJsxNamespaceBindings = /* @__PURE__ */ new Set();
|
|
16363
|
-
const sameFileTypeDeclarations = /* @__PURE__ */ new Map();
|
|
16364
|
-
let hasLocalJsxNamespace = false;
|
|
16365
|
-
if (!isNodeOfType(program, "Program")) return {
|
|
16366
|
-
reactSlotTypeBindings,
|
|
16367
|
-
reactNamespaceBindings,
|
|
16368
|
-
reactJsxNamespaceBindings,
|
|
16369
|
-
nonReactJsxNamespaceBindings,
|
|
16370
|
-
sameFileTypeDeclarations,
|
|
16371
|
-
hasLocalJsxNamespace
|
|
16372
|
-
};
|
|
16373
|
-
for (const statement of program.body) {
|
|
16374
|
-
if (isNodeOfType(statement, "ImportDeclaration")) {
|
|
16375
|
-
const isReactImport = statement.source.value === "react";
|
|
16376
|
-
for (const specifier of statement.specifiers) {
|
|
16377
|
-
if (isReactImport && (isNodeOfType(specifier, "ImportDefaultSpecifier") || isNodeOfType(specifier, "ImportNamespaceSpecifier"))) {
|
|
16378
|
-
reactNamespaceBindings.add(specifier.local.name);
|
|
16379
|
-
continue;
|
|
16380
|
-
}
|
|
16381
|
-
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
16382
|
-
const importedName = getImportedName(specifier);
|
|
16383
|
-
if (!importedName) continue;
|
|
16384
|
-
if (isReactImport && REACT_SLOT_TYPE_NAMES.has(importedName)) reactSlotTypeBindings.add(specifier.local.name);
|
|
16385
|
-
if (importedName === "JSX") if (isReactImport) reactJsxNamespaceBindings.add(specifier.local.name);
|
|
16386
|
-
else nonReactJsxNamespaceBindings.add(specifier.local.name);
|
|
16387
|
-
}
|
|
16388
|
-
continue;
|
|
16389
|
-
}
|
|
16390
|
-
const declaration = unwrapTopLevelDeclaration(statement);
|
|
16391
|
-
if (!declaration) continue;
|
|
16392
|
-
if ((isNodeOfType(declaration, "TSInterfaceDeclaration") || isNodeOfType(declaration, "TSTypeAliasDeclaration")) && isNodeOfType(declaration.id, "Identifier")) sameFileTypeDeclarations.set(declaration.id.name, declaration);
|
|
16393
|
-
if (isNodeOfType(declaration, "TSModuleDeclaration") && isNodeOfType(declaration.id, "Identifier") && declaration.id.name === "JSX") hasLocalJsxNamespace = true;
|
|
16394
|
-
}
|
|
16395
|
-
return {
|
|
16396
|
-
reactSlotTypeBindings,
|
|
16397
|
-
reactNamespaceBindings,
|
|
16398
|
-
reactJsxNamespaceBindings,
|
|
16399
|
-
nonReactJsxNamespaceBindings,
|
|
16400
|
-
sameFileTypeDeclarations,
|
|
16401
|
-
hasLocalJsxNamespace
|
|
16402
|
-
};
|
|
16403
|
-
};
|
|
16404
|
-
const isJsxElementTypeName = (typeName, environment) => {
|
|
16405
|
-
if (!isNodeOfType(typeName, "TSQualifiedName")) return false;
|
|
16406
|
-
if (!isNodeOfType(typeName.right, "Identifier") || typeName.right.name !== "Element") return false;
|
|
16407
|
-
if (isNodeOfType(typeName.left, "Identifier")) {
|
|
16408
|
-
const namespaceName = typeName.left.name;
|
|
16409
|
-
if (environment.reactNamespaceBindings.has(namespaceName)) return true;
|
|
16410
|
-
if (environment.reactJsxNamespaceBindings.has(namespaceName)) return true;
|
|
16411
|
-
if (environment.nonReactJsxNamespaceBindings.has(namespaceName)) return false;
|
|
16412
|
-
return namespaceName === "JSX" && !environment.hasLocalJsxNamespace;
|
|
16413
|
-
}
|
|
16414
|
-
return isNodeOfType(typeName.left, "TSQualifiedName") && isNodeOfType(typeName.left.left, "Identifier") && environment.reactNamespaceBindings.has(typeName.left.left.name) && isNodeOfType(typeName.left.right, "Identifier") && typeName.left.right.name === "JSX";
|
|
16415
|
-
};
|
|
16416
|
-
const isReactSlotType = (typeNode, environment, activeDeclarations) => {
|
|
16417
|
-
if (isNodeOfType(typeNode, "TSUnionType")) return typeNode.types.some((unionMember) => isReactSlotType(unionMember, environment, activeDeclarations));
|
|
16418
|
-
if (!isNodeOfType(typeNode, "TSTypeReference")) return false;
|
|
16419
|
-
if (isJsxElementTypeName(typeNode.typeName, environment)) return true;
|
|
16420
|
-
if (isNodeOfType(typeNode.typeName, "TSQualifiedName") && isNodeOfType(typeNode.typeName.left, "Identifier") && environment.reactNamespaceBindings.has(typeNode.typeName.left.name) && isNodeOfType(typeNode.typeName.right, "Identifier") && REACT_SLOT_TYPE_NAMES.has(typeNode.typeName.right.name)) return true;
|
|
16421
|
-
if (!isNodeOfType(typeNode.typeName, "Identifier")) return false;
|
|
16422
|
-
const typeName = typeNode.typeName.name;
|
|
16423
|
-
const sameFileDeclaration = environment.sameFileTypeDeclarations.get(typeName);
|
|
16424
|
-
if (sameFileDeclaration) {
|
|
16425
|
-
if (!isNodeOfType(sameFileDeclaration, "TSTypeAliasDeclaration") || activeDeclarations.has(sameFileDeclaration)) return false;
|
|
16426
|
-
activeDeclarations.add(sameFileDeclaration);
|
|
16427
|
-
const isSlot = isReactSlotType(sameFileDeclaration.typeAnnotation, environment, activeDeclarations);
|
|
16428
|
-
activeDeclarations.delete(sameFileDeclaration);
|
|
16429
|
-
return isSlot;
|
|
16430
|
-
}
|
|
16431
|
-
return environment.reactSlotTypeBindings.has(typeName);
|
|
16432
|
-
};
|
|
16433
|
-
const getPropertyName = (property) => {
|
|
16434
|
-
if (!isNodeOfType(property, "TSPropertySignature") || property.computed) return null;
|
|
16435
|
-
if (isNodeOfType(property.key, "Identifier")) return property.key.name;
|
|
16436
|
-
if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") return property.key.value;
|
|
16437
|
-
return null;
|
|
16438
|
-
};
|
|
16439
|
-
const collectSlotPropertyNames = (propsType, environment, slotPropertyNames, activeDeclarations) => {
|
|
16440
|
-
let members = null;
|
|
16441
|
-
if (isNodeOfType(propsType, "TSTypeLiteral")) members = propsType.members;
|
|
16442
|
-
if (isNodeOfType(propsType, "TSInterfaceDeclaration")) members = propsType.body.body;
|
|
16443
|
-
if (members) {
|
|
16444
|
-
for (const member of members) {
|
|
16445
|
-
const propertyName = getPropertyName(member);
|
|
16446
|
-
if (!propertyName || !isNodeOfType(member, "TSPropertySignature")) continue;
|
|
16447
|
-
const annotation = member.typeAnnotation;
|
|
16448
|
-
if (annotation && isNodeOfType(annotation, "TSTypeAnnotation") && isReactSlotType(annotation.typeAnnotation, environment, /* @__PURE__ */ new Set())) slotPropertyNames.add(propertyName);
|
|
16449
|
-
}
|
|
16450
|
-
return;
|
|
16451
|
-
}
|
|
16452
|
-
if (isNodeOfType(propsType, "TSIntersectionType")) {
|
|
16453
|
-
for (const intersectionMember of propsType.types) collectSlotPropertyNames(intersectionMember, environment, slotPropertyNames, activeDeclarations);
|
|
16454
|
-
return;
|
|
16455
|
-
}
|
|
16456
|
-
if (isNodeOfType(propsType, "TSTypeAliasDeclaration")) {
|
|
16457
|
-
collectSlotPropertyNames(propsType.typeAnnotation, environment, slotPropertyNames, activeDeclarations);
|
|
16458
|
-
return;
|
|
16459
|
-
}
|
|
16460
|
-
if (!isNodeOfType(propsType, "TSTypeReference") || !isNodeOfType(propsType.typeName, "Identifier")) return;
|
|
16461
|
-
const declaration = environment.sameFileTypeDeclarations.get(propsType.typeName.name);
|
|
16462
|
-
if (!declaration || activeDeclarations.has(declaration)) return;
|
|
16463
|
-
activeDeclarations.add(declaration);
|
|
16464
|
-
collectSlotPropertyNames(declaration, environment, slotPropertyNames, activeDeclarations);
|
|
16465
|
-
activeDeclarations.delete(declaration);
|
|
16466
|
-
};
|
|
16467
|
-
const getMemoizedComponentPropsType = (initializer, scopes) => {
|
|
16468
|
-
const componentFunction = unwrapReactHocFunction(initializer, scopes);
|
|
16469
|
-
if (componentFunction) {
|
|
16470
|
-
const firstParameter = componentFunction.params[0];
|
|
16471
|
-
const annotation = firstParameter && "typeAnnotation" in firstParameter ? firstParameter.typeAnnotation : null;
|
|
16472
|
-
if (annotation && isNodeOfType(annotation, "TSTypeAnnotation")) return annotation.typeAnnotation;
|
|
16473
|
-
}
|
|
16474
|
-
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
16475
|
-
return initializer.typeArguments?.params[0] ?? null;
|
|
16476
|
-
};
|
|
16477
|
-
const buildSameFileJsxSlotPropRegistry = (program, memoRegistry, scopes) => {
|
|
16478
|
-
const registry = /* @__PURE__ */ new Map();
|
|
16479
|
-
if (!isNodeOfType(program, "Program")) return registry;
|
|
16480
|
-
const environment = buildReactSlotTypeEnvironment(program);
|
|
16481
|
-
for (const statement of program.body) {
|
|
16482
|
-
const declaration = unwrapTopLevelDeclaration(statement);
|
|
16483
|
-
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) continue;
|
|
16484
|
-
for (const declarator of declaration.declarations) {
|
|
16485
|
-
if (!isNodeOfType(declarator.id, "Identifier") || memoRegistry.get(declarator.id.name) !== "memoised" || !declarator.init) continue;
|
|
16486
|
-
const componentSymbol = scopes.symbolFor(declarator.id);
|
|
16487
|
-
if (!componentSymbol) continue;
|
|
16488
|
-
const propsType = getMemoizedComponentPropsType(declarator.init, scopes);
|
|
16489
|
-
if (!propsType) continue;
|
|
16490
|
-
const slotPropertyNames = /* @__PURE__ */ new Set();
|
|
16491
|
-
collectSlotPropertyNames(propsType, environment, slotPropertyNames, /* @__PURE__ */ new Set());
|
|
16492
|
-
if (slotPropertyNames.size > 0) registry.set(componentSymbol.id, slotPropertyNames);
|
|
16493
|
-
}
|
|
16494
|
-
}
|
|
16495
|
-
return registry;
|
|
16496
|
-
};
|
|
16497
|
-
//#endregion
|
|
16498
15934
|
//#region src/plugin/utils/is-on-intrinsic-html-element.ts
|
|
16499
15935
|
const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
16500
15936
|
const openingElement = attribute.parent;
|
|
@@ -16768,11 +16204,9 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
16768
16204
|
create: (context) => {
|
|
16769
16205
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
16770
16206
|
let memoRegistry = null;
|
|
16771
|
-
let jsxSlotPropRegistry = null;
|
|
16772
16207
|
return {
|
|
16773
16208
|
Program(node) {
|
|
16774
16209
|
memoRegistry = buildSameFileMemoRegistry(node);
|
|
16775
|
-
jsxSlotPropRegistry = buildSameFileJsxSlotPropRegistry(node, memoRegistry, context.scopes);
|
|
16776
16210
|
},
|
|
16777
16211
|
JSXAttribute(node) {
|
|
16778
16212
|
if (isTestlikeFile) return;
|
|
@@ -16780,8 +16214,6 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
16780
16214
|
const parentJsxOpening = node.parent;
|
|
16781
16215
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
16782
16216
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
16783
|
-
const openingSymbol = openingName && isNodeOfType(openingName, "JSXIdentifier") ? context.scopes.symbolFor(openingName) : null;
|
|
16784
|
-
if (openingSymbol && isNodeOfType(node.name, "JSXIdentifier") && jsxSlotPropRegistry?.get(openingSymbol.id)?.has(node.name.name)) return;
|
|
16785
16217
|
if (isNodeOfType(node.name, "JSXIdentifier") && isSlotPropName(node.name.name)) return;
|
|
16786
16218
|
if (!isInsideFunctionScope(node)) return;
|
|
16787
16219
|
const value = node.value;
|
|
@@ -20025,6 +19457,54 @@ const nextjsNoAElement = defineRule({
|
|
|
20025
19457
|
} })
|
|
20026
19458
|
});
|
|
20027
19459
|
//#endregion
|
|
19460
|
+
//#region src/plugin/utils/collect-effect-invoked-functions.ts
|
|
19461
|
+
const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
|
|
19462
|
+
"then",
|
|
19463
|
+
"catch",
|
|
19464
|
+
"finally"
|
|
19465
|
+
]);
|
|
19466
|
+
const collectEffectInvokedFunctions = (effectCallback) => {
|
|
19467
|
+
const invokedFunctions = new Set([effectCallback]);
|
|
19468
|
+
const localFunctionBindings = /* @__PURE__ */ new Map();
|
|
19469
|
+
const calledBindingNames = /* @__PURE__ */ new Set();
|
|
19470
|
+
const pendingFunctions = [effectCallback];
|
|
19471
|
+
const enqueue = (candidate) => {
|
|
19472
|
+
const strippedCandidate = candidate ? stripParenExpression(candidate) : candidate;
|
|
19473
|
+
if (!isFunctionLike$1(strippedCandidate) || invokedFunctions.has(strippedCandidate)) return;
|
|
19474
|
+
invokedFunctions.add(strippedCandidate);
|
|
19475
|
+
pendingFunctions.push(strippedCandidate);
|
|
19476
|
+
};
|
|
19477
|
+
const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
|
|
19478
|
+
while (pendingFunctions.length > 0) {
|
|
19479
|
+
const currentFunction = pendingFunctions.pop();
|
|
19480
|
+
if (!currentFunction) break;
|
|
19481
|
+
walkAst(currentFunction, (child) => {
|
|
19482
|
+
if (child !== currentFunction && isFunctionLike$1(child)) {
|
|
19483
|
+
if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) localFunctionBindings.set(child.id.name, child);
|
|
19484
|
+
return false;
|
|
19485
|
+
}
|
|
19486
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
19487
|
+
const initializer = child.init ? stripParenExpression(child.init) : null;
|
|
19488
|
+
if (isFunctionLike$1(initializer)) localFunctionBindings.set(child.id.name, initializer);
|
|
19489
|
+
return;
|
|
19490
|
+
}
|
|
19491
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
19492
|
+
const callee = stripParenExpression(child.callee);
|
|
19493
|
+
if (isFunctionLike$1(callee)) {
|
|
19494
|
+
enqueue(callee);
|
|
19495
|
+
return;
|
|
19496
|
+
}
|
|
19497
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
19498
|
+
calledBindingNames.add(callee.name);
|
|
19499
|
+
return;
|
|
19500
|
+
}
|
|
19501
|
+
if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
19502
|
+
});
|
|
19503
|
+
for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
|
|
19504
|
+
}
|
|
19505
|
+
return invokedFunctions;
|
|
19506
|
+
};
|
|
19507
|
+
//#endregion
|
|
20028
19508
|
//#region src/plugin/utils/contains-fetch-call.ts
|
|
20029
19509
|
const isFetchCall$1 = (node) => {
|
|
20030
19510
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
@@ -25151,7 +24631,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
25151
24631
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
25152
24632
|
return false;
|
|
25153
24633
|
};
|
|
25154
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
24634
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
|
|
25155
24635
|
"forEach",
|
|
25156
24636
|
"map",
|
|
25157
24637
|
"filter",
|
|
@@ -25172,7 +24652,7 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
25172
24652
|
if (parent.callee === functionNode) return true;
|
|
25173
24653
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
25174
24654
|
const callee = parent.callee;
|
|
25175
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
24655
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
|
|
25176
24656
|
};
|
|
25177
24657
|
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
25178
24658
|
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
@@ -27805,14 +27285,14 @@ const hasDocumentClassListMutation = (node) => {
|
|
|
27805
27285
|
//#endregion
|
|
27806
27286
|
//#region src/plugin/rules/state-and-effects/no-effect-event-handler.ts
|
|
27807
27287
|
const hasEventLikeNode = (node) => findTriggeredSideEffectCalleeName(node) !== null || hasDocumentClassListMutation(node);
|
|
27808
|
-
const unwrapChainExpression$
|
|
27288
|
+
const unwrapChainExpression$1 = (node) => {
|
|
27809
27289
|
if (!node) return null;
|
|
27810
27290
|
if (isNodeOfType(node, "ChainExpression")) return node.expression;
|
|
27811
27291
|
return node;
|
|
27812
27292
|
};
|
|
27813
27293
|
const collectGuardExpressions = (node, into) => {
|
|
27814
27294
|
if (!node) return;
|
|
27815
|
-
const unwrappedNode = unwrapChainExpression$
|
|
27295
|
+
const unwrappedNode = unwrapChainExpression$1(node);
|
|
27816
27296
|
if (!unwrappedNode) return;
|
|
27817
27297
|
const rootIdentifierName = getRootIdentifierName(unwrappedNode);
|
|
27818
27298
|
if (rootIdentifierName) {
|
|
@@ -27843,7 +27323,7 @@ const isReturnOnlyStatement = (node) => {
|
|
|
27843
27323
|
};
|
|
27844
27324
|
const hasEventLikeRemainingStatements = (statements) => statements.some((statement) => !isNodeOfType(statement, "ReturnStatement") && hasEventLikeNode(statement));
|
|
27845
27325
|
const doesGuardMatchDependency = (guardExpression, dependencyExpression) => {
|
|
27846
|
-
const unwrappedDependencyExpression = unwrapChainExpression$
|
|
27326
|
+
const unwrappedDependencyExpression = unwrapChainExpression$1(dependencyExpression);
|
|
27847
27327
|
if (!unwrappedDependencyExpression) return false;
|
|
27848
27328
|
if (areExpressionsStructurallyEqual(guardExpression.expression, unwrappedDependencyExpression)) return true;
|
|
27849
27329
|
return isNodeOfType(unwrappedDependencyExpression, "Identifier") && unwrappedDependencyExpression.name === guardExpression.rootIdentifierName;
|
|
@@ -27934,24 +27414,6 @@ const noEffectEventHandler = defineRule({
|
|
|
27934
27414
|
}
|
|
27935
27415
|
});
|
|
27936
27416
|
//#endregion
|
|
27937
|
-
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
27938
|
-
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
27939
|
-
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
27940
|
-
if (importSource === null) return false;
|
|
27941
|
-
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
27942
|
-
};
|
|
27943
|
-
//#endregion
|
|
27944
|
-
//#region src/plugin/utils/is-non-react-effect-event-callee.ts
|
|
27945
|
-
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
27946
|
-
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
27947
|
-
return Boolean(symbol && symbol.kind !== "import");
|
|
27948
|
-
};
|
|
27949
|
-
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
|
|
27950
|
-
if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
|
|
27951
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
|
|
27952
|
-
return false;
|
|
27953
|
-
};
|
|
27954
|
-
//#endregion
|
|
27955
27417
|
//#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
|
|
27956
27418
|
const createComponentBindingStackTracker = (callbacks) => {
|
|
27957
27419
|
const componentBindingStack = [];
|
|
@@ -29033,61 +28495,13 @@ const noEventTriggerState = defineRule({
|
|
|
29033
28495
|
}
|
|
29034
28496
|
});
|
|
29035
28497
|
//#endregion
|
|
29036
|
-
//#region src/plugin/utils/resolve-expression-key.ts
|
|
29037
|
-
const resolveExpressionKey = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
29038
|
-
if (!expression) return null;
|
|
29039
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
29040
|
-
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
29041
|
-
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
29042
|
-
if (!symbol) return context.scopes.isGlobalReference(unwrappedExpression) ? `global:${unwrappedExpression.name}` : null;
|
|
29043
|
-
if (visitedSymbolIds.has(symbol.id)) return `symbol:${symbol.id}`;
|
|
29044
|
-
visitedSymbolIds.add(symbol.id);
|
|
29045
|
-
const bindingProperty = symbol.bindingIdentifier.parent;
|
|
29046
|
-
const bindingPattern = bindingProperty?.parent;
|
|
29047
|
-
const variableDeclarator = bindingPattern?.parent;
|
|
29048
|
-
const bindingPropertyName = isNodeOfType(bindingProperty, "Property") ? getStaticPropertyKeyName(bindingProperty) : null;
|
|
29049
|
-
if (bindingPropertyName && isNodeOfType(bindingPattern, "ObjectPattern") && isNodeOfType(variableDeclarator, "VariableDeclarator") && variableDeclarator.id === bindingPattern) {
|
|
29050
|
-
const objectKey = resolveExpressionKey(variableDeclarator.init, context, visitedSymbolIds);
|
|
29051
|
-
return objectKey ? `${objectKey}.${bindingPropertyName}` : `symbol:${symbol.id}`;
|
|
29052
|
-
}
|
|
29053
|
-
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
29054
|
-
if (symbol.kind === "const" && initializer && (isNodeOfType(initializer, "Identifier") || isNodeOfType(initializer, "MemberExpression"))) return resolveExpressionKey(initializer, context, visitedSymbolIds) ?? `symbol:${symbol.id}`;
|
|
29055
|
-
return `symbol:${symbol.id}`;
|
|
29056
|
-
}
|
|
29057
|
-
if (isNodeOfType(unwrappedExpression, "MemberExpression") && !unwrappedExpression.computed) {
|
|
29058
|
-
if (!isNodeOfType(unwrappedExpression.property, "Identifier")) return null;
|
|
29059
|
-
const objectKey = resolveExpressionKey(unwrappedExpression.object, context, visitedSymbolIds);
|
|
29060
|
-
return objectKey ? `${objectKey}.${unwrappedExpression.property.name}` : null;
|
|
29061
|
-
}
|
|
29062
|
-
if (isNodeOfType(unwrappedExpression, "ThisExpression")) return "this";
|
|
29063
|
-
if (isNodeOfType(unwrappedExpression, "Literal") && (typeof unwrappedExpression.value === "string" || typeof unwrappedExpression.value === "number")) return `literal:${String(unwrappedExpression.value)}`;
|
|
29064
|
-
if (isFunctionLike$1(unwrappedExpression)) {
|
|
29065
|
-
const rangeStart = getRangeStart(unwrappedExpression);
|
|
29066
|
-
return rangeStart === null ? null : `function:${rangeStart}`;
|
|
29067
|
-
}
|
|
29068
|
-
return null;
|
|
29069
|
-
};
|
|
29070
|
-
//#endregion
|
|
29071
|
-
//#region src/plugin/utils/statement-always-exits.ts
|
|
29072
|
-
const statementAlwaysExits = (statement) => {
|
|
29073
|
-
if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement")) return true;
|
|
29074
|
-
if (isNodeOfType(statement, "IfStatement")) return Boolean(statement.alternate && statementAlwaysExits(statement.consequent) && statementAlwaysExits(statement.alternate));
|
|
29075
|
-
if (!isNodeOfType(statement, "BlockStatement")) return false;
|
|
29076
|
-
return statement.body.some((childStatement) => statementAlwaysExits(childStatement));
|
|
29077
|
-
};
|
|
29078
|
-
//#endregion
|
|
29079
28498
|
//#region src/plugin/rules/state-and-effects/no-fetch-in-effect.ts
|
|
29080
28499
|
const IMPORT_INITIALIZER_TYPES = new Set([
|
|
29081
28500
|
"ImportSpecifier",
|
|
29082
28501
|
"ImportDefaultSpecifier",
|
|
29083
28502
|
"ImportNamespaceSpecifier"
|
|
29084
28503
|
]);
|
|
29085
|
-
const CANCELLATION_FLAG_NAME_PATTERN = /cancel
|
|
29086
|
-
const PROMISE_CONTINUATION_METHOD_NAMES = new Set([
|
|
29087
|
-
"then",
|
|
29088
|
-
"catch",
|
|
29089
|
-
"finally"
|
|
29090
|
-
]);
|
|
28504
|
+
const CANCELLATION_FLAG_NAME_PATTERN = /cancel/i;
|
|
29091
28505
|
const isShadowedByLocalBinding = (identifier) => {
|
|
29092
28506
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
29093
28507
|
const binding = findVariableInitializer(identifier, identifier.name);
|
|
@@ -29103,54 +28517,49 @@ const isRealFetchCall = (node) => {
|
|
|
29103
28517
|
};
|
|
29104
28518
|
const isXmlHttpRequestConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "XMLHttpRequest";
|
|
29105
28519
|
const isNetworkRequest = (node) => isRealFetchCall(node) || isXmlHttpRequestConstruction(node);
|
|
29106
|
-
const
|
|
29107
|
-
|
|
29108
|
-
|
|
29109
|
-
|
|
29110
|
-
|
|
29111
|
-
|
|
29112
|
-
|
|
29113
|
-
|
|
29114
|
-
|
|
28520
|
+
const containsNetworkRequest = (root) => {
|
|
28521
|
+
let found = false;
|
|
28522
|
+
walkAst(root, (child) => {
|
|
28523
|
+
if (found) return false;
|
|
28524
|
+
if (isNetworkRequest(child)) {
|
|
28525
|
+
found = true;
|
|
28526
|
+
return false;
|
|
28527
|
+
}
|
|
28528
|
+
});
|
|
28529
|
+
return found;
|
|
29115
28530
|
};
|
|
29116
|
-
const
|
|
29117
|
-
const
|
|
29118
|
-
|
|
29119
|
-
|
|
29120
|
-
|
|
29121
|
-
|
|
29122
|
-
|
|
29123
|
-
|
|
28531
|
+
const collectComponentScopeFunctionBindings = (componentScope, effectCallback) => {
|
|
28532
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
28533
|
+
walkAst(componentScope, (child) => {
|
|
28534
|
+
if (child === effectCallback) return false;
|
|
28535
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
28536
|
+
const initializer = child.init ? stripParenExpression(child.init) : null;
|
|
28537
|
+
if (isFunctionLike$1(initializer)) bindings.set(child.id.name, initializer);
|
|
28538
|
+
return;
|
|
29124
28539
|
}
|
|
29125
|
-
|
|
29126
|
-
|
|
29127
|
-
|
|
29128
|
-
const currentFunction = pendingFunctions.pop();
|
|
29129
|
-
if (!currentFunction) break;
|
|
29130
|
-
walkAst(currentFunction, (child) => {
|
|
29131
|
-
if (child !== currentFunction && isFunctionLike$1(child)) return false;
|
|
29132
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
29133
|
-
const calledFunction = resolveLocalFunction(child.callee, context);
|
|
29134
|
-
if (calledFunction) enqueueFunction(calledFunction);
|
|
29135
|
-
for (const callArgument of child.arguments ?? []) {
|
|
29136
|
-
const callbackFunction = resolveLocalFunction(callArgument, context);
|
|
29137
|
-
if (callbackFunction) enqueueFunction(callbackFunction);
|
|
29138
|
-
}
|
|
29139
|
-
});
|
|
29140
|
-
}
|
|
29141
|
-
return analysisFunctions;
|
|
28540
|
+
if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) bindings.set(child.id.name, child);
|
|
28541
|
+
});
|
|
28542
|
+
return bindings;
|
|
29142
28543
|
};
|
|
29143
|
-
const
|
|
29144
|
-
const
|
|
29145
|
-
const
|
|
29146
|
-
|
|
29147
|
-
|
|
29148
|
-
|
|
29149
|
-
|
|
29150
|
-
|
|
28544
|
+
const containsNetworkRequestInEffect = (effectCallback) => {
|
|
28545
|
+
const invokedFunctions = collectEffectInvokedFunctions(effectCallback);
|
|
28546
|
+
for (const invokedFunction of invokedFunctions) if (containsNetworkRequest(invokedFunction)) return true;
|
|
28547
|
+
const enclosingComponent = findEnclosingFunction(effectCallback);
|
|
28548
|
+
if (!enclosingComponent) return false;
|
|
28549
|
+
const scopeFunctions = collectComponentScopeFunctionBindings(enclosingComponent, effectCallback);
|
|
28550
|
+
let found = false;
|
|
28551
|
+
walkAst(effectCallback, (child) => {
|
|
28552
|
+
if (found) return false;
|
|
28553
|
+
if (child !== effectCallback && isFunctionLike$1(child) && invokedFunctions.has(child)) return false;
|
|
28554
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier")) {
|
|
28555
|
+
const scopeFunction = scopeFunctions.get(child.callee.name);
|
|
28556
|
+
if (scopeFunction && containsNetworkRequest(scopeFunction)) {
|
|
28557
|
+
found = true;
|
|
28558
|
+
return false;
|
|
28559
|
+
}
|
|
29151
28560
|
}
|
|
29152
28561
|
});
|
|
29153
|
-
return
|
|
28562
|
+
return found;
|
|
29154
28563
|
};
|
|
29155
28564
|
const findEffectCleanupFunction = (callback) => {
|
|
29156
28565
|
if (!isFunctionLike$1(callback)) return null;
|
|
@@ -29163,177 +28572,36 @@ const findEffectCleanupFunction = (callback) => {
|
|
|
29163
28572
|
});
|
|
29164
28573
|
return cleanup;
|
|
29165
28574
|
};
|
|
29166
|
-
const
|
|
29167
|
-
const
|
|
29168
|
-
if (!isFunctionLike$1(effectCallback)) return
|
|
28575
|
+
const collectEffectCancellationFlagNames = (effectCallback) => {
|
|
28576
|
+
const flagNames = /* @__PURE__ */ new Set();
|
|
28577
|
+
if (!isFunctionLike$1(effectCallback)) return flagNames;
|
|
29169
28578
|
const body = effectCallback.body;
|
|
29170
|
-
if (!isNodeOfType(body, "BlockStatement")) return
|
|
28579
|
+
if (!isNodeOfType(body, "BlockStatement")) return flagNames;
|
|
29171
28580
|
for (const statement of body.body ?? []) {
|
|
29172
28581
|
if (!isNodeOfType(statement, "VariableDeclaration") || statement.kind !== "let") continue;
|
|
29173
28582
|
for (const declarator of statement.declarations ?? []) {
|
|
29174
28583
|
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier")) continue;
|
|
29175
28584
|
const initializer = declarator.init;
|
|
29176
|
-
if (isNodeOfType(initializer, "Literal") && initializer.value === false && CANCELLATION_FLAG_NAME_PATTERN.test(declarator.id.name))
|
|
29177
|
-
const flagKey = resolveExpressionKey(declarator.id, context);
|
|
29178
|
-
if (flagKey) flagKeys.add(flagKey);
|
|
29179
|
-
}
|
|
29180
|
-
}
|
|
29181
|
-
}
|
|
29182
|
-
return flagKeys;
|
|
29183
|
-
};
|
|
29184
|
-
const resolveConstValue = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
29185
|
-
if (!expression) return null;
|
|
29186
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
29187
|
-
if (!isNodeOfType(unwrappedExpression, "Identifier")) return unwrappedExpression;
|
|
29188
|
-
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
29189
|
-
if (!symbol || symbol.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return unwrappedExpression;
|
|
29190
|
-
visitedSymbolIds.add(symbol.id);
|
|
29191
|
-
return resolveConstValue(symbol.initializer, context, visitedSymbolIds);
|
|
29192
|
-
};
|
|
29193
|
-
const resolveRequestAbortControllerKey = (request, context) => {
|
|
29194
|
-
if (isNodeOfType(request, "NewExpression")) {
|
|
29195
|
-
const declarator = request.parent;
|
|
29196
|
-
return isNodeOfType(declarator, "VariableDeclarator") && declarator.init === request ? resolveExpressionKey(declarator.id, context) : null;
|
|
29197
|
-
}
|
|
29198
|
-
if (!isNodeOfType(request, "CallExpression")) return null;
|
|
29199
|
-
for (const requestArgument of request.arguments ?? []) {
|
|
29200
|
-
const options = resolveConstValue(requestArgument, context);
|
|
29201
|
-
if (!isNodeOfType(options, "ObjectExpression")) continue;
|
|
29202
|
-
for (const property of options.properties ?? []) {
|
|
29203
|
-
if (!isNodeOfType(property, "Property") || getStaticPropertyKeyName(property, { allowComputedString: true }) !== "signal") continue;
|
|
29204
|
-
const signalKey = resolveExpressionKey(property.value, context);
|
|
29205
|
-
return signalKey?.endsWith(".signal") ? signalKey.slice(0, -7) : null;
|
|
28585
|
+
if (isNodeOfType(initializer, "Literal") && initializer.value === false && CANCELLATION_FLAG_NAME_PATTERN.test(declarator.id.name)) flagNames.add(declarator.id.name);
|
|
29206
28586
|
}
|
|
29207
28587
|
}
|
|
29208
|
-
return
|
|
28588
|
+
return flagNames;
|
|
29209
28589
|
};
|
|
29210
|
-
const
|
|
29211
|
-
const
|
|
29212
|
-
|
|
29213
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "MemberExpression") && !child.callee.computed && isNodeOfType(child.callee.property, "Identifier") && child.callee.property.name === "abort") {
|
|
29214
|
-
const controllerKey = resolveExpressionKey(child.callee.object, context);
|
|
29215
|
-
if (controllerKey) controllerKeys.add(controllerKey);
|
|
29216
|
-
}
|
|
29217
|
-
});
|
|
29218
|
-
return controllerKeys;
|
|
29219
|
-
};
|
|
29220
|
-
const collectCleanupAssignedCancellationFlagKeys = (cleanup, cancellationFlagKeys, context) => {
|
|
29221
|
-
const assignedFlagKeys = /* @__PURE__ */ new Set();
|
|
28590
|
+
const isCancellationCleanup = (cleanup, effectCallback) => {
|
|
28591
|
+
const cancellationFlagNames = collectEffectCancellationFlagNames(effectCallback);
|
|
28592
|
+
let found = false;
|
|
29222
28593
|
walkAst(cleanup, (child) => {
|
|
29223
|
-
if (
|
|
29224
|
-
|
|
29225
|
-
|
|
29226
|
-
|
|
29227
|
-
});
|
|
29228
|
-
return assignedFlagKeys;
|
|
29229
|
-
};
|
|
29230
|
-
const readCancellationCondition = (expression, cancellationFlagKey, context) => {
|
|
29231
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
29232
|
-
if (resolveExpressionKey(unwrappedExpression, context) === cancellationFlagKey) return true;
|
|
29233
|
-
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
29234
|
-
const argumentResult = readCancellationCondition(unwrappedExpression.argument, cancellationFlagKey, context);
|
|
29235
|
-
return argumentResult === null ? null : !argumentResult;
|
|
29236
|
-
}
|
|
29237
|
-
if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
|
|
29238
|
-
const leftResult = readCancellationCondition(unwrappedExpression.left, cancellationFlagKey, context);
|
|
29239
|
-
const rightResult = readCancellationCondition(unwrappedExpression.right, cancellationFlagKey, context);
|
|
29240
|
-
const leftBoolean = isNodeOfType(unwrappedExpression.left, "Literal") && typeof unwrappedExpression.left.value === "boolean" ? unwrappedExpression.left.value : null;
|
|
29241
|
-
const rightBoolean = isNodeOfType(unwrappedExpression.right, "Literal") && typeof unwrappedExpression.right.value === "boolean" ? unwrappedExpression.right.value : null;
|
|
29242
|
-
const comparedCancellationValue = leftResult !== null && rightBoolean !== null ? rightBoolean : rightResult !== null && leftBoolean !== null ? leftBoolean : null;
|
|
29243
|
-
if (comparedCancellationValue === null) return null;
|
|
29244
|
-
if (unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==") return comparedCancellationValue;
|
|
29245
|
-
if (unwrappedExpression.operator === "!==" || unwrappedExpression.operator === "!=") return !comparedCancellationValue;
|
|
29246
|
-
return null;
|
|
29247
|
-
};
|
|
29248
|
-
const isCompletionSinkInsideCancellationGuard = (completionSink, cancellationFlagKey, context) => {
|
|
29249
|
-
let currentNode = completionSink;
|
|
29250
|
-
let parentNode = currentNode.parent;
|
|
29251
|
-
while (parentNode) {
|
|
29252
|
-
if (isFunctionLike$1(parentNode)) return false;
|
|
29253
|
-
if (isNodeOfType(parentNode, "IfStatement")) {
|
|
29254
|
-
const cancellationResult = readCancellationCondition(parentNode.test, cancellationFlagKey, context);
|
|
29255
|
-
if (currentNode === parentNode.consequent && cancellationResult === false) return true;
|
|
29256
|
-
if (currentNode === parentNode.alternate && cancellationResult === true) return true;
|
|
29257
|
-
}
|
|
29258
|
-
if (isNodeOfType(parentNode, "ConditionalExpression")) {
|
|
29259
|
-
const cancellationResult = readCancellationCondition(parentNode.test, cancellationFlagKey, context);
|
|
29260
|
-
if (currentNode === parentNode.consequent && cancellationResult === false) return true;
|
|
29261
|
-
if (currentNode === parentNode.alternate && cancellationResult === true) return true;
|
|
29262
|
-
}
|
|
29263
|
-
if (isNodeOfType(parentNode, "LogicalExpression") && currentNode === parentNode.right) {
|
|
29264
|
-
const cancellationResult = readCancellationCondition(parentNode.left, cancellationFlagKey, context);
|
|
29265
|
-
if (parentNode.operator === "&&" && cancellationResult === false) return true;
|
|
29266
|
-
if (parentNode.operator === "||" && cancellationResult === true) return true;
|
|
28594
|
+
if (found) return false;
|
|
28595
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && child.callee.property.name === "abort") {
|
|
28596
|
+
found = true;
|
|
28597
|
+
return false;
|
|
29267
28598
|
}
|
|
29268
|
-
|
|
29269
|
-
|
|
29270
|
-
|
|
29271
|
-
return false;
|
|
29272
|
-
};
|
|
29273
|
-
const isCompletionSinkAfterCancellationEarlyExit = (completionSink, cancellationFlagKey, context) => {
|
|
29274
|
-
let currentNode = completionSink;
|
|
29275
|
-
let parentNode = currentNode.parent;
|
|
29276
|
-
while (parentNode) {
|
|
29277
|
-
if (isFunctionLike$1(parentNode)) return false;
|
|
29278
|
-
if (isNodeOfType(parentNode, "BlockStatement")) {
|
|
29279
|
-
const statementIndex = parentNode.body.findIndex((statement) => statement === currentNode);
|
|
29280
|
-
if (statementIndex >= 0) for (const statement of parentNode.body.slice(0, statementIndex)) {
|
|
29281
|
-
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
29282
|
-
const cancellationResult = readCancellationCondition(statement.test, cancellationFlagKey, context);
|
|
29283
|
-
if (cancellationResult === true && statementAlwaysExits(statement.consequent) || cancellationResult === false && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
|
|
29284
|
-
}
|
|
28599
|
+
if (isNodeOfType(child, "AssignmentExpression") && isNodeOfType(child.left, "Identifier") && cancellationFlagNames.has(child.left.name) && isNodeOfType(child.right, "Literal") && child.right.value === true) {
|
|
28600
|
+
found = true;
|
|
28601
|
+
return false;
|
|
29285
28602
|
}
|
|
29286
|
-
currentNode = parentNode;
|
|
29287
|
-
parentNode = currentNode.parent;
|
|
29288
|
-
}
|
|
29289
|
-
return false;
|
|
29290
|
-
};
|
|
29291
|
-
const isCompletionSinkGuardedByCancellationFlag = (completionSink, cancellationFlagKey, context) => isCompletionSinkInsideCancellationGuard(completionSink, cancellationFlagKey, context) || isCompletionSinkAfterCancellationEarlyExit(completionSink, cancellationFlagKey, context);
|
|
29292
|
-
const isDescendantOf = (node, ancestor) => {
|
|
29293
|
-
let currentNode = node;
|
|
29294
|
-
while (currentNode) {
|
|
29295
|
-
if (currentNode === ancestor) return true;
|
|
29296
|
-
currentNode = currentNode.parent ?? null;
|
|
29297
|
-
}
|
|
29298
|
-
return false;
|
|
29299
|
-
};
|
|
29300
|
-
const isPromiseContinuationForRequest = (functionNode, request) => {
|
|
29301
|
-
const callNode = functionNode.parent;
|
|
29302
|
-
if (!isNodeOfType(callNode, "CallExpression") || !callNode.arguments?.some((callArgument) => callArgument === functionNode) || !isNodeOfType(callNode.callee, "MemberExpression") || callNode.callee.computed || !isNodeOfType(callNode.callee.property, "Identifier") || !PROMISE_CONTINUATION_METHOD_NAMES.has(callNode.callee.property.name)) return false;
|
|
29303
|
-
return isDescendantOf(request, callNode.callee.object);
|
|
29304
|
-
};
|
|
29305
|
-
const isAwaitedInFunction = (request, functionNode) => {
|
|
29306
|
-
let currentNode = request.parent;
|
|
29307
|
-
while (currentNode && currentNode !== functionNode) {
|
|
29308
|
-
if (isNodeOfType(currentNode, "AwaitExpression")) return true;
|
|
29309
|
-
currentNode = currentNode.parent ?? null;
|
|
29310
|
-
}
|
|
29311
|
-
return false;
|
|
29312
|
-
};
|
|
29313
|
-
const isCompletionSinkForRequest = (completionSink, request) => {
|
|
29314
|
-
const requestFunction = findEnclosingFunction(request);
|
|
29315
|
-
const completionSinkFunction = findEnclosingFunction(completionSink);
|
|
29316
|
-
if (!requestFunction || !completionSinkFunction) return false;
|
|
29317
|
-
if (requestFunction === completionSinkFunction) {
|
|
29318
|
-
if (!isAwaitedInFunction(request, requestFunction)) return false;
|
|
29319
|
-
const requestStart = getRangeStart(request);
|
|
29320
|
-
const completionSinkStart = getRangeStart(completionSink);
|
|
29321
|
-
return requestStart === null || completionSinkStart === null || completionSinkStart > requestStart;
|
|
29322
|
-
}
|
|
29323
|
-
return isPromiseContinuationForRequest(completionSinkFunction, request);
|
|
29324
|
-
};
|
|
29325
|
-
const requestHasGuardedCompletionSinks = (request, completionSinks, assignedCancellationFlagKeys, context) => {
|
|
29326
|
-
const followingCompletionSinks = completionSinks.filter((completionSink) => isCompletionSinkForRequest(completionSink, request));
|
|
29327
|
-
return followingCompletionSinks.length > 0 && followingCompletionSinks.every((completionSink) => [...assignedCancellationFlagKeys].some((cancellationFlagKey) => isCompletionSinkGuardedByCancellationFlag(completionSink, cancellationFlagKey, context)));
|
|
29328
|
-
};
|
|
29329
|
-
const allRequestsHaveCorrelatedCancellation = (requests, completionSinks, cleanup, effectCallback, context) => {
|
|
29330
|
-
const abortedControllerKeys = collectCleanupAbortedControllerKeys(cleanup, context);
|
|
29331
|
-
const assignedCancellationFlagKeys = collectCleanupAssignedCancellationFlagKeys(cleanup, collectEffectCancellationFlagKeys(effectCallback, context), context);
|
|
29332
|
-
return requests.every((request) => {
|
|
29333
|
-
const requestControllerKey = resolveRequestAbortControllerKey(request, context);
|
|
29334
|
-
if (requestControllerKey && abortedControllerKeys.has(requestControllerKey)) return true;
|
|
29335
|
-
return requestHasGuardedCompletionSinks(request, completionSinks, assignedCancellationFlagKeys, context);
|
|
29336
28603
|
});
|
|
28604
|
+
return found;
|
|
29337
28605
|
};
|
|
29338
28606
|
const noFetchInEffect = defineRule({
|
|
29339
28607
|
id: "no-fetch-in-effect",
|
|
@@ -29344,13 +28612,9 @@ const noFetchInEffect = defineRule({
|
|
|
29344
28612
|
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
29345
28613
|
const callback = getEffectCallback(node);
|
|
29346
28614
|
if (!callback) return;
|
|
29347
|
-
|
|
29348
|
-
const requests = collectNodesFromAnalysisFunctions(analysisFunctions, isNetworkRequest);
|
|
29349
|
-
if (requests.length === 0) return;
|
|
28615
|
+
if (!containsNetworkRequestInEffect(callback)) return;
|
|
29350
28616
|
const cleanup = findEffectCleanupFunction(callback);
|
|
29351
|
-
if (cleanup)
|
|
29352
|
-
if (allRequestsHaveCorrelatedCancellation(requests, collectNodesFromAnalysisFunctions(analysisFunctions, isSetterCall), cleanup, callback, context)) return;
|
|
29353
|
-
}
|
|
28617
|
+
if (cleanup && isCancellationCleanup(cleanup, callback)) return;
|
|
29354
28618
|
context.report({
|
|
29355
28619
|
node,
|
|
29356
28620
|
message: "fetch() inside useEffect can race, double-fire, or leak. Use a data-fetching layer or Server Component instead."
|
|
@@ -29650,6 +28914,34 @@ const noGenericHandlerNames = defineRule({
|
|
|
29650
28914
|
} })
|
|
29651
28915
|
});
|
|
29652
28916
|
//#endregion
|
|
28917
|
+
//#region src/plugin/utils/unwrap-react-hoc-function.ts
|
|
28918
|
+
/**
|
|
28919
|
+
* Resolves a `VariableDeclarator.init` (or any expression) to the inline
|
|
28920
|
+
* function expression it binds, seeing through chains of `memo` /
|
|
28921
|
+
* `forwardRef` / `React.memo` / `React.forwardRef` wrappers:
|
|
28922
|
+
*
|
|
28923
|
+
* `() => {}` → the arrow
|
|
28924
|
+
* `memo(function Foo() {})` → the named function expression
|
|
28925
|
+
* `React.memo(forwardRef(() => {}))` → the inner arrow
|
|
28926
|
+
* `memo(SomeIdentifier)` → `null` (no inline function)
|
|
28927
|
+
*
|
|
28928
|
+
* Component-shaped rules that previously gated on a direct function init
|
|
28929
|
+
* (via `isComponentAssignment`) use this so memo-wrapped components are
|
|
28930
|
+
* not silently skipped.
|
|
28931
|
+
*/
|
|
28932
|
+
const unwrapReactHocFunction = (node) => {
|
|
28933
|
+
if (!node) return null;
|
|
28934
|
+
let current = stripParenExpression(node);
|
|
28935
|
+
while (isNodeOfType(current, "CallExpression")) {
|
|
28936
|
+
const calleeName = flattenCalleeName(current.callee);
|
|
28937
|
+
if (!calleeName || !REACT_HOC_NAMES.has(calleeName)) return null;
|
|
28938
|
+
const firstArgument = current.arguments[0];
|
|
28939
|
+
if (!firstArgument) return null;
|
|
28940
|
+
current = stripParenExpression(firstArgument);
|
|
28941
|
+
}
|
|
28942
|
+
return isInlineFunctionExpression(current) ? current : null;
|
|
28943
|
+
};
|
|
28944
|
+
//#endregion
|
|
29653
28945
|
//#region src/plugin/rules/architecture/no-giant-component.ts
|
|
29654
28946
|
const noGiantComponent = defineRule({
|
|
29655
28947
|
id: "no-giant-component",
|
|
@@ -29902,405 +29194,6 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
29902
29194
|
} })
|
|
29903
29195
|
});
|
|
29904
29196
|
//#endregion
|
|
29905
|
-
//#region src/plugin/utils/find-enclosing-jsx-opening-element.ts
|
|
29906
|
-
const findEnclosingJsxOpeningElement = (node) => {
|
|
29907
|
-
let cursor = node.parent;
|
|
29908
|
-
while (cursor) {
|
|
29909
|
-
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
29910
|
-
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
29911
|
-
cursor = cursor.parent ?? null;
|
|
29912
|
-
}
|
|
29913
|
-
return null;
|
|
29914
|
-
};
|
|
29915
|
-
//#endregion
|
|
29916
|
-
//#region src/plugin/utils/has-client-render-evidence.ts
|
|
29917
|
-
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
29918
|
-
if (fileHasUseClientDirective) return true;
|
|
29919
|
-
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
29920
|
-
if (displayName && isReactHookName(displayName)) return true;
|
|
29921
|
-
let callsHook = false;
|
|
29922
|
-
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
29923
|
-
if (callsHook) return false;
|
|
29924
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
29925
|
-
callsHook = true;
|
|
29926
|
-
return false;
|
|
29927
|
-
}
|
|
29928
|
-
});
|
|
29929
|
-
return callsHook;
|
|
29930
|
-
};
|
|
29931
|
-
//#endregion
|
|
29932
|
-
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
29933
|
-
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
29934
|
-
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
29935
|
-
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
29936
|
-
return false;
|
|
29937
|
-
};
|
|
29938
|
-
//#endregion
|
|
29939
|
-
//#region src/plugin/utils/read-initial-state-boolean.ts
|
|
29940
|
-
const readLogicalResult = (operator, leftResult, rightResult) => {
|
|
29941
|
-
if (operator === "&&") {
|
|
29942
|
-
if (leftResult === false || rightResult === false) return false;
|
|
29943
|
-
if (leftResult === true && rightResult === true) return true;
|
|
29944
|
-
return null;
|
|
29945
|
-
}
|
|
29946
|
-
if (leftResult === true || rightResult === true) return true;
|
|
29947
|
-
if (leftResult === false && rightResult === false) return false;
|
|
29948
|
-
return null;
|
|
29949
|
-
};
|
|
29950
|
-
const readIdentifierInitialStateBoolean = (identifier, scopes, visitedBindings, allowLazyInitializer) => {
|
|
29951
|
-
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
29952
|
-
if (identifier.name === "undefined" && scopes.isGlobalReference(identifier)) return false;
|
|
29953
|
-
const binding = findVariableInitializer(identifier, identifier.name);
|
|
29954
|
-
if (!binding || visitedBindings.has(binding.bindingIdentifier)) return null;
|
|
29955
|
-
visitedBindings.add(binding.bindingIdentifier);
|
|
29956
|
-
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
29957
|
-
if (!declarator?.init || scopes.symbolFor(identifier)?.declarationNode !== declarator) return null;
|
|
29958
|
-
const initializer = stripParenExpression(declarator.init);
|
|
29959
|
-
if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements?.[0] === binding.bindingIdentifier && isReactApiCall(initializer, "useState", scopes, { allowGlobalReactNamespace: true }) && isNodeOfType(initializer, "CallExpression")) {
|
|
29960
|
-
const initialState = initializer.arguments?.[0];
|
|
29961
|
-
if (!initialState) return false;
|
|
29962
|
-
if (initialState.type === "SpreadElement") return null;
|
|
29963
|
-
return readInitialStateBooleanInternal(initialState, scopes, visitedBindings, true);
|
|
29964
|
-
}
|
|
29965
|
-
const declaration = declarator.parent;
|
|
29966
|
-
if (!isNodeOfType(declarator.id, "Identifier") || declarator.id !== binding.bindingIdentifier || !isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const") return null;
|
|
29967
|
-
return readInitialStateBooleanInternal(initializer, scopes, visitedBindings, allowLazyInitializer);
|
|
29968
|
-
};
|
|
29969
|
-
const readInitialStateBooleanInternal = (expression, scopes, visitedBindings, allowLazyInitializer) => {
|
|
29970
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
29971
|
-
if (isNodeOfType(unwrappedExpression, "Literal")) return Boolean(unwrappedExpression.value);
|
|
29972
|
-
if (isNodeOfType(unwrappedExpression, "Identifier")) return readIdentifierInitialStateBoolean(unwrappedExpression, scopes, visitedBindings, allowLazyInitializer);
|
|
29973
|
-
if (allowLazyInitializer && (isNodeOfType(unwrappedExpression, "ArrowFunctionExpression") || isNodeOfType(unwrappedExpression, "FunctionExpression"))) {
|
|
29974
|
-
if (unwrappedExpression.async || isNodeOfType(unwrappedExpression, "FunctionExpression") && unwrappedExpression.generator) return null;
|
|
29975
|
-
if (!isNodeOfType(unwrappedExpression.body, "BlockStatement")) return readInitialStateBooleanInternal(unwrappedExpression.body, scopes, visitedBindings, false);
|
|
29976
|
-
if (unwrappedExpression.body.body.length !== 1) return null;
|
|
29977
|
-
const returnStatement = unwrappedExpression.body.body[0];
|
|
29978
|
-
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return null;
|
|
29979
|
-
return readInitialStateBooleanInternal(returnStatement.argument, scopes, visitedBindings, false);
|
|
29980
|
-
}
|
|
29981
|
-
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
29982
|
-
const argumentResult = readInitialStateBooleanInternal(unwrappedExpression.argument, scopes, visitedBindings, false);
|
|
29983
|
-
return argumentResult === null ? null : !argumentResult;
|
|
29984
|
-
}
|
|
29985
|
-
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));
|
|
29986
|
-
return null;
|
|
29987
|
-
};
|
|
29988
|
-
const readInitialStateBoolean = (expression, scopes) => readInitialStateBooleanInternal(expression, scopes, /* @__PURE__ */ new Set(), false);
|
|
29989
|
-
//#endregion
|
|
29990
|
-
//#region src/plugin/utils/is-after-client-only-early-return.ts
|
|
29991
|
-
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode, scopes) => {
|
|
29992
|
-
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
29993
|
-
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
29994
|
-
const ancestors = /* @__PURE__ */ new Set();
|
|
29995
|
-
let currentNode = node;
|
|
29996
|
-
while (currentNode) {
|
|
29997
|
-
ancestors.add(currentNode);
|
|
29998
|
-
currentNode = currentNode.parent ?? null;
|
|
29999
|
-
}
|
|
30000
|
-
for (const statement of body.body ?? []) {
|
|
30001
|
-
if (ancestors.has(statement)) return false;
|
|
30002
|
-
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
30003
|
-
const initialConditionResult = readInitialStateBoolean(statement.test, scopes);
|
|
30004
|
-
if (initialConditionResult === true && statementAlwaysExits(statement.consequent)) return true;
|
|
30005
|
-
if (initialConditionResult === false && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
|
|
30006
|
-
}
|
|
30007
|
-
return false;
|
|
30008
|
-
};
|
|
30009
|
-
//#endregion
|
|
30010
|
-
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
30011
|
-
const isGatedByFalsyInitialState = (node, scopes) => {
|
|
30012
|
-
let cursor = node;
|
|
30013
|
-
let parent = node.parent;
|
|
30014
|
-
while (parent) {
|
|
30015
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === cursor && (parent.operator === "&&" && readInitialStateBoolean(parent.left, scopes) === false || parent.operator === "||" && readInitialStateBoolean(parent.left, scopes) === true)) return true;
|
|
30016
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor && readInitialStateBoolean(parent.test, scopes) === false || parent.alternate === cursor && readInitialStateBoolean(parent.test, scopes) === true)) return true;
|
|
30017
|
-
if (isNodeOfType(parent, "IfStatement") && (parent.consequent === cursor && readInitialStateBoolean(parent.test, scopes) === false || parent.alternate === cursor && readInitialStateBoolean(parent.test, scopes) === true)) return true;
|
|
30018
|
-
cursor = parent;
|
|
30019
|
-
parent = parent.parent ?? null;
|
|
30020
|
-
}
|
|
30021
|
-
return false;
|
|
30022
|
-
};
|
|
30023
|
-
//#endregion
|
|
30024
|
-
//#region src/plugin/rules/performance/no-hydration-branch-on-browser-global.ts
|
|
30025
|
-
const evaluateEquality = (operator, left, right) => {
|
|
30026
|
-
if (operator === "===" || operator === "==") return left === right;
|
|
30027
|
-
if (operator === "!==" || operator === "!=") return left !== right;
|
|
30028
|
-
return null;
|
|
30029
|
-
};
|
|
30030
|
-
const readTypeofBrowserGlobal = (expression, context) => {
|
|
30031
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
30032
|
-
if (!isNodeOfType(unwrappedExpression, "UnaryExpression") || unwrappedExpression.operator !== "typeof") return null;
|
|
30033
|
-
const argument = stripParenExpression(unwrappedExpression.argument);
|
|
30034
|
-
if (isNodeOfType(argument, "Identifier")) return (argument.name === "window" || argument.name === "document") && context.scopes.isGlobalReference(argument) ? argument.name : null;
|
|
30035
|
-
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;
|
|
30036
|
-
return argument.property.name;
|
|
30037
|
-
};
|
|
30038
|
-
const matchBrowserPredicate = (expression, context) => {
|
|
30039
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
30040
|
-
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
30041
|
-
const innerMatch = matchBrowserPredicate(unwrappedExpression.argument, context);
|
|
30042
|
-
return innerMatch ? {
|
|
30043
|
-
browserGlobalName: innerMatch.browserGlobalName,
|
|
30044
|
-
clientResult: !innerMatch.clientResult,
|
|
30045
|
-
serverResult: !innerMatch.serverResult
|
|
30046
|
-
} : null;
|
|
30047
|
-
}
|
|
30048
|
-
if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
|
|
30049
|
-
const leftGlobalName = readTypeofBrowserGlobal(unwrappedExpression.left, context);
|
|
30050
|
-
const rightGlobalName = readTypeofBrowserGlobal(unwrappedExpression.right, context);
|
|
30051
|
-
const leftString = isNodeOfType(unwrappedExpression.left, "Literal") ? unwrappedExpression.left.value : null;
|
|
30052
|
-
const rightString = isNodeOfType(unwrappedExpression.right, "Literal") ? unwrappedExpression.right.value : null;
|
|
30053
|
-
const browserGlobalName = leftGlobalName && typeof rightString === "string" ? leftGlobalName : rightGlobalName && typeof leftString === "string" ? rightGlobalName : null;
|
|
30054
|
-
const comparedType = leftGlobalName && typeof rightString === "string" ? rightString : rightGlobalName && typeof leftString === "string" ? leftString : null;
|
|
30055
|
-
if (!browserGlobalName || !comparedType) return null;
|
|
30056
|
-
const clientResult = evaluateEquality(unwrappedExpression.operator, "object", comparedType);
|
|
30057
|
-
const serverResult = evaluateEquality(unwrappedExpression.operator, "undefined", comparedType);
|
|
30058
|
-
if (clientResult === null || serverResult === null || clientResult === serverResult) return null;
|
|
30059
|
-
return {
|
|
30060
|
-
browserGlobalName,
|
|
30061
|
-
clientResult,
|
|
30062
|
-
serverResult
|
|
30063
|
-
};
|
|
30064
|
-
};
|
|
30065
|
-
const readLogicalConditionResult = (operator, leftResult, rightResult) => {
|
|
30066
|
-
if (operator === "&&") {
|
|
30067
|
-
if (leftResult === false || rightResult === false) return false;
|
|
30068
|
-
if (leftResult === true && rightResult === true) return true;
|
|
30069
|
-
return null;
|
|
30070
|
-
}
|
|
30071
|
-
if (leftResult === true || rightResult === true) return true;
|
|
30072
|
-
if (leftResult === false && rightResult === false) return false;
|
|
30073
|
-
return null;
|
|
30074
|
-
};
|
|
30075
|
-
const readHydrationConditionResult = (expression, context, runtime) => {
|
|
30076
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
30077
|
-
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
30078
|
-
if (predicateMatch) return predicateMatch[`${runtime}Result`];
|
|
30079
|
-
const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
|
|
30080
|
-
if (staticResult !== null) return staticResult;
|
|
30081
|
-
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
30082
|
-
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
|
|
30083
|
-
return argumentResult === null ? null : !argumentResult;
|
|
30084
|
-
}
|
|
30085
|
-
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
30086
|
-
return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime), readHydrationConditionResult(unwrappedExpression.right, context, runtime));
|
|
30087
|
-
};
|
|
30088
|
-
const matchHydrationCondition = (expression, context) => {
|
|
30089
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
30090
|
-
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
30091
|
-
if (predicateMatch) return {
|
|
30092
|
-
predicateMatch,
|
|
30093
|
-
predicateNode: unwrappedExpression
|
|
30094
|
-
};
|
|
30095
|
-
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationCondition(unwrappedExpression.argument, context);
|
|
30096
|
-
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
30097
|
-
const leftMatch = matchHydrationCondition(unwrappedExpression.left, context);
|
|
30098
|
-
const rightMatch = matchHydrationCondition(unwrappedExpression.right, context);
|
|
30099
|
-
if (leftMatch && rightMatch) {
|
|
30100
|
-
const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client");
|
|
30101
|
-
const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server");
|
|
30102
|
-
return clientResult !== null && serverResult !== null && clientResult !== serverResult ? leftMatch : null;
|
|
30103
|
-
}
|
|
30104
|
-
const nestedMatch = leftMatch ?? rightMatch;
|
|
30105
|
-
if (!nestedMatch) return null;
|
|
30106
|
-
const otherResult = readInitialStateBoolean(leftMatch ? unwrappedExpression.right : unwrappedExpression.left, context.scopes);
|
|
30107
|
-
if (unwrappedExpression.operator === "&&" && otherResult === false || unwrappedExpression.operator === "||" && otherResult === true) return null;
|
|
30108
|
-
return nestedMatch;
|
|
30109
|
-
};
|
|
30110
|
-
const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
|
|
30111
|
-
const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
|
|
30112
|
-
if (!leftNode || !rightNode) return leftNode === rightNode;
|
|
30113
|
-
const left = stripParenExpression(leftNode);
|
|
30114
|
-
const right = stripParenExpression(rightNode);
|
|
30115
|
-
if (areExpressionsStructurallyEqual(left, right)) return true;
|
|
30116
|
-
if (left.type !== right.type) return false;
|
|
30117
|
-
if (isNodeOfType(left, "JSXText") && isNodeOfType(right, "JSXText")) return left.value === right.value;
|
|
30118
|
-
if (isNodeOfType(left, "JSXExpressionContainer") && isNodeOfType(right, "JSXExpressionContainer")) {
|
|
30119
|
-
if (!isAstNode(left.expression) || !isAstNode(right.expression)) return left.expression.type === right.expression.type;
|
|
30120
|
-
return areRenderedBranchesEquivalent(left.expression, right.expression);
|
|
30121
|
-
}
|
|
30122
|
-
if (isNodeOfType(left, "JSXElement") && isNodeOfType(right, "JSXElement")) {
|
|
30123
|
-
if (flattenJsxName$1(left.openingElement.name) !== flattenJsxName$1(right.openingElement.name)) return false;
|
|
30124
|
-
if (!areNodeArraysEquivalent(left.openingElement.attributes, right.openingElement.attributes)) return false;
|
|
30125
|
-
return areNodeArraysEquivalent(left.children, right.children);
|
|
30126
|
-
}
|
|
30127
|
-
if (isNodeOfType(left, "JSXFragment") && isNodeOfType(right, "JSXFragment")) return areNodeArraysEquivalent(left.children, right.children);
|
|
30128
|
-
if (isNodeOfType(left, "JSXAttribute") && isNodeOfType(right, "JSXAttribute")) {
|
|
30129
|
-
if (flattenJsxName$1(left.name) !== flattenJsxName$1(right.name)) return false;
|
|
30130
|
-
return areRenderedBranchesEquivalent(left.value, right.value);
|
|
30131
|
-
}
|
|
30132
|
-
if (isNodeOfType(left, "JSXSpreadAttribute") && isNodeOfType(right, "JSXSpreadAttribute")) return areRenderedBranchesEquivalent(left.argument, right.argument);
|
|
30133
|
-
if (isNodeOfType(left, "TemplateLiteral") && isNodeOfType(right, "TemplateLiteral")) {
|
|
30134
|
-
if (left.quasis.length !== right.quasis.length) return false;
|
|
30135
|
-
if (!left.quasis.every((quasi, index) => quasi.value.cooked === right.quasis[index]?.value.cooked && quasi.value.raw === right.quasis[index]?.value.raw)) return false;
|
|
30136
|
-
return areNodeArraysEquivalent(left.expressions, right.expressions);
|
|
30137
|
-
}
|
|
30138
|
-
return false;
|
|
30139
|
-
};
|
|
30140
|
-
const isRenderedValue = (node) => {
|
|
30141
|
-
const unwrappedNode = stripParenExpression(node);
|
|
30142
|
-
if (isNodeOfType(unwrappedNode, "Literal")) return unwrappedNode.value !== null && unwrappedNode.value !== true && unwrappedNode.value !== false && unwrappedNode.value !== "";
|
|
30143
|
-
if (isNodeOfType(unwrappedNode, "TemplateLiteral")) return unwrappedNode.expressions.length > 0 || unwrappedNode.quasis[0]?.value.cooked !== "";
|
|
30144
|
-
return isNodeOfType(unwrappedNode, "JSXElement") || isNodeOfType(unwrappedNode, "JSXFragment");
|
|
30145
|
-
};
|
|
30146
|
-
const findRenderedValueInAndBranch = (node) => {
|
|
30147
|
-
const unwrappedNode = stripParenExpression(node);
|
|
30148
|
-
if (isRenderedValue(unwrappedNode)) return unwrappedNode;
|
|
30149
|
-
if (!isNodeOfType(unwrappedNode, "LogicalExpression") || unwrappedNode.operator !== "&&") return null;
|
|
30150
|
-
return findRenderedValueInAndBranch(unwrappedNode.right);
|
|
30151
|
-
};
|
|
30152
|
-
const findEnclosingJsxAttribute = (node) => {
|
|
30153
|
-
let currentNode = node.parent;
|
|
30154
|
-
while (currentNode) {
|
|
30155
|
-
if (isNodeOfType(currentNode, "JSXAttribute")) return currentNode;
|
|
30156
|
-
if (isNodeOfType(currentNode, "JSXElement") || isNodeOfType(currentNode, "JSXFragment") || isFunctionLike$1(currentNode)) return null;
|
|
30157
|
-
currentNode = currentNode.parent;
|
|
30158
|
-
}
|
|
30159
|
-
return null;
|
|
30160
|
-
};
|
|
30161
|
-
const isInRenderedOutput = (node, componentOrHookNode, scopes) => {
|
|
30162
|
-
let currentNode = node;
|
|
30163
|
-
let parentNode = currentNode.parent;
|
|
30164
|
-
while (parentNode) {
|
|
30165
|
-
if (isNodeOfType(parentNode, "JSXExpressionContainer")) {
|
|
30166
|
-
const attribute = findEnclosingJsxAttribute(parentNode);
|
|
30167
|
-
return attribute ? !isEventHandlerAttribute(attribute) : true;
|
|
30168
|
-
}
|
|
30169
|
-
if (isNodeOfType(parentNode, "ReturnStatement")) {
|
|
30170
|
-
if (findEnclosingFunction(parentNode) === componentOrHookNode) return true;
|
|
30171
|
-
}
|
|
30172
|
-
if (parentNode === componentOrHookNode) return isFunctionLike$1(componentOrHookNode) && !isNodeOfType(componentOrHookNode.body, "BlockStatement") && componentOrHookNode.body === currentNode;
|
|
30173
|
-
if (isFunctionLike$1(parentNode) && !executesDuringRender(parentNode, scopes)) return false;
|
|
30174
|
-
currentNode = parentNode;
|
|
30175
|
-
parentNode = currentNode.parent;
|
|
30176
|
-
}
|
|
30177
|
-
return false;
|
|
30178
|
-
};
|
|
30179
|
-
const getReturnedValues = (statement) => {
|
|
30180
|
-
if (!statement) return [];
|
|
30181
|
-
if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? [statement.argument] : [];
|
|
30182
|
-
if (isNodeOfType(statement, "IfStatement")) return [...getReturnedValues(statement.consequent), ...getReturnedValues(statement.alternate)];
|
|
30183
|
-
if (!isNodeOfType(statement, "BlockStatement")) return [];
|
|
30184
|
-
const returnedValues = [];
|
|
30185
|
-
for (const childStatement of statement.body) {
|
|
30186
|
-
returnedValues.push(...getReturnedValues(childStatement));
|
|
30187
|
-
if (statementAlwaysExits(childStatement)) break;
|
|
30188
|
-
}
|
|
30189
|
-
return returnedValues;
|
|
30190
|
-
};
|
|
30191
|
-
const findFollowingReturnedValues = (ifStatement) => {
|
|
30192
|
-
const parentNode = ifStatement.parent;
|
|
30193
|
-
if (!isNodeOfType(parentNode, "BlockStatement")) return [];
|
|
30194
|
-
const statementIndex = parentNode.body.findIndex((statement) => statement === ifStatement);
|
|
30195
|
-
if (statementIndex < 0) return [];
|
|
30196
|
-
const returnedValues = [];
|
|
30197
|
-
for (const statement of parentNode.body.slice(statementIndex + 1)) {
|
|
30198
|
-
returnedValues.push(...getReturnedValues(statement));
|
|
30199
|
-
if (statementAlwaysExits(statement)) break;
|
|
30200
|
-
}
|
|
30201
|
-
return returnedValues;
|
|
30202
|
-
};
|
|
30203
|
-
const areConditionExpressionsEquivalent = (leftExpression, rightExpression) => {
|
|
30204
|
-
const left = stripParenExpression(leftExpression);
|
|
30205
|
-
const right = stripParenExpression(rightExpression);
|
|
30206
|
-
if (areExpressionsStructurallyEqual(left, right)) return true;
|
|
30207
|
-
if (left.type !== right.type) return false;
|
|
30208
|
-
if (isNodeOfType(left, "UnaryExpression") && isNodeOfType(right, "UnaryExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.argument, right.argument);
|
|
30209
|
-
if (isNodeOfType(left, "LogicalExpression") && isNodeOfType(right, "LogicalExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.left, right.left) && areConditionExpressionsEquivalent(left.right, right.right);
|
|
30210
|
-
if (isNodeOfType(left, "BinaryExpression") && isNodeOfType(right, "BinaryExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.left, right.left) && areConditionExpressionsEquivalent(left.right, right.right);
|
|
30211
|
-
return false;
|
|
30212
|
-
};
|
|
30213
|
-
const areReturnTreesEquivalent = (leftStatement, rightStatement) => {
|
|
30214
|
-
if (!leftStatement || !rightStatement) return leftStatement === rightStatement;
|
|
30215
|
-
if (isNodeOfType(leftStatement, "ReturnStatement") && isNodeOfType(rightStatement, "ReturnStatement")) return areRenderedBranchesEquivalent(leftStatement.argument, rightStatement.argument);
|
|
30216
|
-
if (isNodeOfType(leftStatement, "IfStatement") && isNodeOfType(rightStatement, "IfStatement")) return areConditionExpressionsEquivalent(leftStatement.test, rightStatement.test) && areReturnTreesEquivalent(leftStatement.consequent, rightStatement.consequent) && areReturnTreesEquivalent(leftStatement.alternate, rightStatement.alternate);
|
|
30217
|
-
if (!isNodeOfType(leftStatement, "BlockStatement") || !isNodeOfType(rightStatement, "BlockStatement")) return false;
|
|
30218
|
-
const leftReturningStatements = leftStatement.body.filter((statement) => getReturnedValues(statement).length > 0);
|
|
30219
|
-
const rightReturningStatements = rightStatement.body.filter((statement) => getReturnedValues(statement).length > 0);
|
|
30220
|
-
return leftReturningStatements.length === rightReturningStatements.length && leftReturningStatements.every((statement, index) => areReturnTreesEquivalent(statement, rightReturningStatements[index]));
|
|
30221
|
-
};
|
|
30222
|
-
const isStructuralRenderedValue = (node) => {
|
|
30223
|
-
if (!node) return false;
|
|
30224
|
-
const unwrappedNode = stripParenExpression(node);
|
|
30225
|
-
return isNodeOfType(unwrappedNode, "JSXElement") || isNodeOfType(unwrappedNode, "JSXFragment");
|
|
30226
|
-
};
|
|
30227
|
-
const branchRootsSuppressSameElement = (leftBranch, rightBranch) => {
|
|
30228
|
-
if (!rightBranch) return false;
|
|
30229
|
-
const left = stripParenExpression(leftBranch);
|
|
30230
|
-
const right = stripParenExpression(rightBranch);
|
|
30231
|
-
return isNodeOfType(left, "JSXElement") && isNodeOfType(right, "JSXElement") && flattenJsxName$1(left.openingElement.name) === flattenJsxName$1(right.openingElement.name) && hasSuppressHydrationWarningAttribute(left.openingElement) && hasSuppressHydrationWarningAttribute(right.openingElement);
|
|
30232
|
-
};
|
|
30233
|
-
const noHydrationBranchOnBrowserGlobal = defineRule({
|
|
30234
|
-
id: "no-hydration-branch-on-browser-global",
|
|
30235
|
-
title: "Server and client render different branches",
|
|
30236
|
-
severity: "error",
|
|
30237
|
-
category: "Correctness",
|
|
30238
|
-
requires: ["ssr"],
|
|
30239
|
-
recommendation: "Render the same initial output on the server and client, then switch after mount or use useSyncExternalStore with a stable server snapshot.",
|
|
30240
|
-
create: (context) => {
|
|
30241
|
-
if (isTestlikeFilename(context.filename)) return {};
|
|
30242
|
-
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
30243
|
-
let fileHasUseClientDirective = false;
|
|
30244
|
-
let fileIsEmailTemplate = false;
|
|
30245
|
-
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30246
|
-
const reportHydrationBranch = (conditionNode, leftBranch, rightBranch, requiresRenderedContext) => {
|
|
30247
|
-
const conditionMatch = matchHydrationCondition(conditionNode, context);
|
|
30248
|
-
if (!conditionMatch) return;
|
|
30249
|
-
const { predicateMatch, predicateNode } = conditionMatch;
|
|
30250
|
-
if (reportedNodes.has(predicateNode)) return;
|
|
30251
|
-
if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
|
|
30252
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(predicateNode, context.scopes);
|
|
30253
|
-
if (!componentOrHookNode) return;
|
|
30254
|
-
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30255
|
-
if (requiresRenderedContext && !isInRenderedOutput(predicateNode, componentOrHookNode, context.scopes)) return;
|
|
30256
|
-
if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
|
|
30257
|
-
const attribute = findEnclosingJsxAttribute(predicateNode);
|
|
30258
|
-
if (!attribute || isEventHandlerAttribute(attribute)) return;
|
|
30259
|
-
}
|
|
30260
|
-
if (fileIsEmailTemplate || isGatedByFalsyInitialState(predicateNode, context.scopes)) return;
|
|
30261
|
-
if (isAfterClientOnlyEarlyReturn(predicateNode, componentOrHookNode, context.scopes)) return;
|
|
30262
|
-
const openingElement = findEnclosingJsxOpeningElement(predicateNode);
|
|
30263
|
-
if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
|
|
30264
|
-
if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
|
|
30265
|
-
if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
|
|
30266
|
-
reportedNodes.add(predicateNode);
|
|
30267
|
-
context.report({
|
|
30268
|
-
node: predicateNode,
|
|
30269
|
-
message: `\`typeof ${predicateMatch.browserGlobalName}\` selects different rendered output on the server and during hydration. Render the same initial output, then switch after mount.`
|
|
30270
|
-
});
|
|
30271
|
-
};
|
|
30272
|
-
return {
|
|
30273
|
-
Program(node) {
|
|
30274
|
-
fileHasUseClientDirective = hasDirective(node, "use client");
|
|
30275
|
-
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
30276
|
-
},
|
|
30277
|
-
ConditionalExpression(node) {
|
|
30278
|
-
reportHydrationBranch(node.test, node.consequent, node.alternate, true);
|
|
30279
|
-
},
|
|
30280
|
-
LogicalExpression(node) {
|
|
30281
|
-
if (node.operator !== "&&" && node.operator !== "||") return;
|
|
30282
|
-
const renderedValue = node.operator === "&&" ? findRenderedValueInAndBranch(node.right) : isRenderedValue(node.right) ? node.right : null;
|
|
30283
|
-
if (!renderedValue) return;
|
|
30284
|
-
reportHydrationBranch(node, renderedValue, null, true);
|
|
30285
|
-
},
|
|
30286
|
-
IfStatement(node) {
|
|
30287
|
-
if (node.alternate && areReturnTreesEquivalent(node.consequent, node.alternate)) return;
|
|
30288
|
-
const consequentValues = getReturnedValues(node.consequent);
|
|
30289
|
-
const alternateValues = node.alternate ? getReturnedValues(node.alternate) : findFollowingReturnedValues(node);
|
|
30290
|
-
if (consequentValues.length === 0 || alternateValues.length === 0) return;
|
|
30291
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(node.test, context.scopes);
|
|
30292
|
-
if (!componentOrHookNode) return;
|
|
30293
|
-
const enclosingFunction = findEnclosingFunction(node);
|
|
30294
|
-
if (enclosingFunction !== componentOrHookNode && (!enclosingFunction || !isInRenderedOutput(enclosingFunction, componentOrHookNode, context.scopes))) return;
|
|
30295
|
-
for (const consequentValue of consequentValues) for (const alternateValue of alternateValues) {
|
|
30296
|
-
if (!isRenderedValue(consequentValue) && !isRenderedValue(alternateValue)) continue;
|
|
30297
|
-
reportHydrationBranch(node.test, consequentValue, alternateValue, false);
|
|
30298
|
-
}
|
|
30299
|
-
}
|
|
30300
|
-
};
|
|
30301
|
-
}
|
|
30302
|
-
});
|
|
30303
|
-
//#endregion
|
|
30304
29197
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
30305
29198
|
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.";
|
|
30306
29199
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
@@ -31146,6 +30039,111 @@ const noLegacyContextApi = defineRule({
|
|
|
31146
30039
|
}
|
|
31147
30040
|
});
|
|
31148
30041
|
//#endregion
|
|
30042
|
+
//#region src/plugin/utils/executes-during-render.ts
|
|
30043
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
30044
|
+
"map",
|
|
30045
|
+
"filter",
|
|
30046
|
+
"forEach",
|
|
30047
|
+
"flatMap",
|
|
30048
|
+
"reduce",
|
|
30049
|
+
"reduceRight",
|
|
30050
|
+
"some",
|
|
30051
|
+
"every",
|
|
30052
|
+
"find",
|
|
30053
|
+
"findIndex",
|
|
30054
|
+
"findLast",
|
|
30055
|
+
"findLastIndex",
|
|
30056
|
+
"sort",
|
|
30057
|
+
"toSorted"
|
|
30058
|
+
]);
|
|
30059
|
+
const executesDuringRender = (functionNode) => {
|
|
30060
|
+
const parent = functionNode.parent;
|
|
30061
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
30062
|
+
if (parent.callee === functionNode) return true;
|
|
30063
|
+
if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
|
|
30064
|
+
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;
|
|
30065
|
+
};
|
|
30066
|
+
//#endregion
|
|
30067
|
+
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
30068
|
+
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
30069
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
30070
|
+
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
30071
|
+
return false;
|
|
30072
|
+
};
|
|
30073
|
+
//#endregion
|
|
30074
|
+
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
30075
|
+
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
30076
|
+
let ancestor = bindingIdentifier.parent;
|
|
30077
|
+
while (ancestor) {
|
|
30078
|
+
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
30079
|
+
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
30080
|
+
ancestor = ancestor.parent ?? null;
|
|
30081
|
+
}
|
|
30082
|
+
return null;
|
|
30083
|
+
};
|
|
30084
|
+
//#endregion
|
|
30085
|
+
//#region src/plugin/utils/references-falsy-initial-state.ts
|
|
30086
|
+
const isFalsyLiteral = (node) => {
|
|
30087
|
+
if (!node) return true;
|
|
30088
|
+
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
30089
|
+
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
30090
|
+
};
|
|
30091
|
+
const isFalsyInitialStateBinding = (identifier) => {
|
|
30092
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
30093
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
30094
|
+
if (!binding) return false;
|
|
30095
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
30096
|
+
if (!declarator?.init) return false;
|
|
30097
|
+
const init = stripParenExpression(declarator.init);
|
|
30098
|
+
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
30099
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
30100
|
+
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
30101
|
+
return isFalsyLiteral(init.arguments?.[0]);
|
|
30102
|
+
};
|
|
30103
|
+
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
30104
|
+
//#endregion
|
|
30105
|
+
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
30106
|
+
const isGatedByFalsyInitialState = (node) => {
|
|
30107
|
+
let cursor = node;
|
|
30108
|
+
let parent = node.parent;
|
|
30109
|
+
while (parent) {
|
|
30110
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
30111
|
+
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
30112
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
30113
|
+
cursor = parent;
|
|
30114
|
+
parent = parent.parent ?? null;
|
|
30115
|
+
}
|
|
30116
|
+
return false;
|
|
30117
|
+
};
|
|
30118
|
+
//#endregion
|
|
30119
|
+
//#region src/plugin/utils/references-client-only-flag.ts
|
|
30120
|
+
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
30121
|
+
const referencesClientOnlyFlag = (expression) => {
|
|
30122
|
+
const unwrapped = stripParenExpression(expression);
|
|
30123
|
+
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
30124
|
+
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
30125
|
+
const property = unwrapped.property;
|
|
30126
|
+
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
30127
|
+
}
|
|
30128
|
+
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
30129
|
+
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
30130
|
+
return false;
|
|
30131
|
+
};
|
|
30132
|
+
//#endregion
|
|
30133
|
+
//#region src/plugin/utils/is-inside-client-only-guard.ts
|
|
30134
|
+
const isInsideClientOnlyGuard = (node) => {
|
|
30135
|
+
let cursor = node;
|
|
30136
|
+
let parent = node.parent;
|
|
30137
|
+
while (parent) {
|
|
30138
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
30139
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
30140
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
30141
|
+
cursor = parent;
|
|
30142
|
+
parent = parent.parent ?? null;
|
|
30143
|
+
}
|
|
30144
|
+
return false;
|
|
30145
|
+
};
|
|
30146
|
+
//#endregion
|
|
31149
30147
|
//#region src/plugin/rules/performance/no-locale-format-in-render.ts
|
|
31150
30148
|
const LOCALE_FORMAT_METHOD_NAMES = new Set([
|
|
31151
30149
|
"toLocaleString",
|
|
@@ -31275,12 +30273,75 @@ const matchDateDefaultStringification = (node) => {
|
|
|
31275
30273
|
}
|
|
31276
30274
|
return null;
|
|
31277
30275
|
};
|
|
30276
|
+
const findRenderPhaseComponentOrHook = (node) => {
|
|
30277
|
+
let functionNode = findEnclosingFunction(node);
|
|
30278
|
+
while (functionNode) {
|
|
30279
|
+
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
30280
|
+
if (!executesDuringRender(functionNode)) return null;
|
|
30281
|
+
functionNode = findEnclosingFunction(functionNode);
|
|
30282
|
+
}
|
|
30283
|
+
return null;
|
|
30284
|
+
};
|
|
30285
|
+
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
30286
|
+
if (fileHasUseClientDirective) return true;
|
|
30287
|
+
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
30288
|
+
if (displayName && isReactHookName(displayName)) return true;
|
|
30289
|
+
let callsHook = false;
|
|
30290
|
+
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
30291
|
+
if (callsHook) return false;
|
|
30292
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
30293
|
+
callsHook = true;
|
|
30294
|
+
return false;
|
|
30295
|
+
}
|
|
30296
|
+
});
|
|
30297
|
+
return callsHook;
|
|
30298
|
+
};
|
|
30299
|
+
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
|
|
30300
|
+
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
30301
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
30302
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
30303
|
+
let cursor = node;
|
|
30304
|
+
while (cursor) {
|
|
30305
|
+
ancestors.add(cursor);
|
|
30306
|
+
cursor = cursor.parent ?? null;
|
|
30307
|
+
}
|
|
30308
|
+
for (const statement of body.body ?? []) {
|
|
30309
|
+
if (ancestors.has(statement)) return false;
|
|
30310
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
30311
|
+
if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
|
|
30312
|
+
let returnsEarly = false;
|
|
30313
|
+
walkAst(statement.consequent, (child) => {
|
|
30314
|
+
if (isFunctionLike$1(child)) return false;
|
|
30315
|
+
if (isNodeOfType(child, "ReturnStatement")) {
|
|
30316
|
+
returnsEarly = true;
|
|
30317
|
+
return false;
|
|
30318
|
+
}
|
|
30319
|
+
});
|
|
30320
|
+
if (returnsEarly) return true;
|
|
30321
|
+
}
|
|
30322
|
+
return false;
|
|
30323
|
+
};
|
|
30324
|
+
const findEnclosingJsxOpeningElement = (node) => {
|
|
30325
|
+
let cursor = node.parent;
|
|
30326
|
+
while (cursor) {
|
|
30327
|
+
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
30328
|
+
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
30329
|
+
cursor = cursor.parent ?? null;
|
|
30330
|
+
}
|
|
30331
|
+
return null;
|
|
30332
|
+
};
|
|
31278
30333
|
const noLocaleFormatInRender = defineRule({
|
|
31279
30334
|
id: "no-locale-format-in-render",
|
|
31280
30335
|
title: "Locale/timezone formatting during render",
|
|
31281
30336
|
severity: "warn",
|
|
31282
30337
|
category: "Correctness",
|
|
31283
|
-
|
|
30338
|
+
disabledWhen: [
|
|
30339
|
+
"vite",
|
|
30340
|
+
"cra",
|
|
30341
|
+
"expo",
|
|
30342
|
+
"react-native",
|
|
30343
|
+
"unknown"
|
|
30344
|
+
],
|
|
31284
30345
|
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.",
|
|
31285
30346
|
create: (context) => {
|
|
31286
30347
|
if (isTestlikeFilename(context.filename)) return {};
|
|
@@ -31290,12 +30351,13 @@ const noLocaleFormatInRender = defineRule({
|
|
|
31290
30351
|
const reportedNodes = /* @__PURE__ */ new Set();
|
|
31291
30352
|
const reportIfRenderPhase = (match) => {
|
|
31292
30353
|
if (reportedNodes.has(match.node)) return;
|
|
31293
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node
|
|
30354
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
|
|
31294
30355
|
if (!componentOrHookNode) return;
|
|
31295
30356
|
if (fileIsEmailTemplate) return;
|
|
31296
30357
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
31297
|
-
if (
|
|
31298
|
-
if (
|
|
30358
|
+
if (isInsideClientOnlyGuard(match.node)) return;
|
|
30359
|
+
if (isGatedByFalsyInitialState(match.node)) return;
|
|
30360
|
+
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
|
|
31299
30361
|
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
|
|
31300
30362
|
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
|
|
31301
30363
|
reportedNodes.add(match.node);
|
|
@@ -31322,7 +30384,7 @@ const noLocaleFormatInRender = defineRule({
|
|
|
31322
30384
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
31323
30385
|
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
31324
30386
|
const helperName = expression.callee.name;
|
|
31325
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(node
|
|
30387
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node);
|
|
31326
30388
|
if (!componentOrHookNode) return;
|
|
31327
30389
|
const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
|
|
31328
30390
|
if (!helperNode || !isFunctionLike$1(helperNode)) return;
|
|
@@ -31334,8 +30396,9 @@ const noLocaleFormatInRender = defineRule({
|
|
|
31334
30396
|
if (!match || reportedNodes.has(match.node)) return;
|
|
31335
30397
|
if (fileIsEmailTemplate) return;
|
|
31336
30398
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
31337
|
-
if (
|
|
31338
|
-
if (
|
|
30399
|
+
if (isInsideClientOnlyGuard(node)) return;
|
|
30400
|
+
if (isGatedByFalsyInitialState(node)) return;
|
|
30401
|
+
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
|
|
31339
30402
|
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
|
|
31340
30403
|
reportedNodes.add(match.node);
|
|
31341
30404
|
context.report({
|
|
@@ -31437,6 +30500,9 @@ const noLongTransitionDuration = defineRule({
|
|
|
31437
30500
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
31438
30501
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
31439
30502
|
//#endregion
|
|
30503
|
+
//#region src/plugin/utils/is-event-handler-attribute.ts
|
|
30504
|
+
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
|
|
30505
|
+
//#endregion
|
|
31440
30506
|
//#region src/plugin/rules/architecture/no-many-boolean-props.ts
|
|
31441
30507
|
const IMPERATIVE_CALLBACK_PREFIX_PATTERN = /^(?:show|hide|enable|disable)[A-Z]/;
|
|
31442
30508
|
const collectCallbackUsedNames = (componentBody, propsParam, scopes) => {
|
|
@@ -31568,7 +30634,7 @@ const noMatchMediaInStateInitializer = defineRule({
|
|
|
31568
30634
|
title: "matchMedia in state initializer",
|
|
31569
30635
|
severity: "warn",
|
|
31570
30636
|
category: "Correctness",
|
|
31571
|
-
|
|
30637
|
+
disabledWhen: ["vite", "cra"],
|
|
31572
30638
|
recommendation: "Prefer CSS media queries for layout, or subscribe with `useSyncExternalStore` and provide a stable server snapshot.",
|
|
31573
30639
|
create: (context) => {
|
|
31574
30640
|
if (isTestlikeFilename(context.filename)) return {};
|
|
@@ -32119,41 +31185,6 @@ const noMutableInDeps = defineRule({
|
|
|
32119
31185
|
}
|
|
32120
31186
|
});
|
|
32121
31187
|
//#endregion
|
|
32122
|
-
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
32123
|
-
const isResultDiscardedCall = (callExpression) => {
|
|
32124
|
-
let node = callExpression;
|
|
32125
|
-
let parent = node.parent;
|
|
32126
|
-
while (parent) {
|
|
32127
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
32128
|
-
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
32129
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
32130
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
32131
|
-
node = parent;
|
|
32132
|
-
parent = node.parent;
|
|
32133
|
-
continue;
|
|
32134
|
-
}
|
|
32135
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
32136
|
-
node = parent;
|
|
32137
|
-
parent = node.parent;
|
|
32138
|
-
continue;
|
|
32139
|
-
}
|
|
32140
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
32141
|
-
node = parent;
|
|
32142
|
-
parent = node.parent;
|
|
32143
|
-
continue;
|
|
32144
|
-
}
|
|
32145
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
32146
|
-
const expressions = parent.expressions ?? [];
|
|
32147
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
32148
|
-
node = parent;
|
|
32149
|
-
parent = node.parent;
|
|
32150
|
-
continue;
|
|
32151
|
-
}
|
|
32152
|
-
return false;
|
|
32153
|
-
}
|
|
32154
|
-
return false;
|
|
32155
|
-
};
|
|
32156
|
-
//#endregion
|
|
32157
31188
|
//#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
|
|
32158
31189
|
const LODASH_MUTATOR_NAMES = new Set([
|
|
32159
31190
|
"set",
|
|
@@ -33444,7 +32475,7 @@ const isUseRefIdentifier = (identifier) => {
|
|
|
33444
32475
|
};
|
|
33445
32476
|
const COMMAND_PROP_NAME_PATTERN = /^(fetch|load|refetch|dispatch|register|render)([A-Z_]|$)/;
|
|
33446
32477
|
const SETTER_NAMED_PROP_PATTERN = /^set[A-Z]/;
|
|
33447
|
-
const unwrapChainExpression
|
|
32478
|
+
const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
33448
32479
|
const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
|
|
33449
32480
|
"useCallback",
|
|
33450
32481
|
"useMemo",
|
|
@@ -33479,7 +32510,7 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
33479
32510
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
33480
32511
|
const node = def.node;
|
|
33481
32512
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
33482
|
-
const initializer = unwrapChainExpression
|
|
32513
|
+
const initializer = unwrapChainExpression(node.init);
|
|
33483
32514
|
const wrappedFunction = getWrapperHookWrappedFunction(initializer);
|
|
33484
32515
|
if (wrappedFunction) {
|
|
33485
32516
|
if (wrappedFunction.async) return false;
|
|
@@ -33492,7 +32523,7 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
33492
32523
|
const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
33493
32524
|
const node = def.node;
|
|
33494
32525
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
33495
|
-
return getWrapperHookWrappedFunction(unwrapChainExpression
|
|
32526
|
+
return getWrapperHookWrappedFunction(unwrapChainExpression(node.init)) !== null;
|
|
33496
32527
|
}));
|
|
33497
32528
|
const isHandlerBagArgument = (analysis, argument) => {
|
|
33498
32529
|
if (!isNodeOfType(argument, "ObjectExpression")) return false;
|
|
@@ -33514,7 +32545,7 @@ const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
|
|
|
33514
32545
|
const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
33515
32546
|
const node = def.node;
|
|
33516
32547
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
33517
|
-
const init = unwrapChainExpression
|
|
32548
|
+
const init = unwrapChainExpression(node.init);
|
|
33518
32549
|
if (!isNodeOfType(init, "CallExpression")) return false;
|
|
33519
32550
|
const callee = init.callee;
|
|
33520
32551
|
if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
|
|
@@ -33558,7 +32589,7 @@ const noPassDataToParent = defineRule({
|
|
|
33558
32589
|
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
33559
32590
|
if (isRefCall(analysis, ref)) continue;
|
|
33560
32591
|
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
33561
|
-
const calleeNode = unwrapChainExpression
|
|
32592
|
+
const calleeNode = unwrapChainExpression(callExpr.callee);
|
|
33562
32593
|
const identifier = ref.identifier;
|
|
33563
32594
|
if (calleeNode === identifier) {
|
|
33564
32595
|
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
@@ -34407,11 +33438,7 @@ const noReactChildren = defineRule({
|
|
|
34407
33438
|
//#endregion
|
|
34408
33439
|
//#region src/plugin/rules/architecture/utils/create-deprecated-react-import-rule.ts
|
|
34409
33440
|
const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }) => ({ create: (context) => {
|
|
34410
|
-
const
|
|
34411
|
-
const resolvesToNamespaceImport = (identifier) => {
|
|
34412
|
-
const symbol = resolveConstIdentifierAlias(identifier, context.scopes);
|
|
34413
|
-
return Boolean(symbol && namespaceImportSpecifiers.has(symbol.declarationNode));
|
|
34414
|
-
};
|
|
33441
|
+
const namespaceBindings = /* @__PURE__ */ new Set();
|
|
34415
33442
|
return {
|
|
34416
33443
|
ImportDeclaration(node) {
|
|
34417
33444
|
const sourceValue = node.source?.value;
|
|
@@ -34431,15 +33458,18 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
34431
33458
|
});
|
|
34432
33459
|
continue;
|
|
34433
33460
|
}
|
|
34434
|
-
if (isNodeOfType(specifier, "ImportDefaultSpecifier") || isNodeOfType(specifier, "ImportNamespaceSpecifier"))
|
|
33461
|
+
if (isNodeOfType(specifier, "ImportDefaultSpecifier") || isNodeOfType(specifier, "ImportNamespaceSpecifier")) {
|
|
33462
|
+
const localName = specifier.local?.name;
|
|
33463
|
+
if (localName) namespaceBindings.add(localName);
|
|
33464
|
+
}
|
|
34435
33465
|
}
|
|
34436
33466
|
},
|
|
34437
33467
|
MemberExpression(node) {
|
|
34438
|
-
if (
|
|
33468
|
+
if (namespaceBindings.size === 0) return;
|
|
34439
33469
|
if (node.computed) return;
|
|
34440
33470
|
const receiver = stripParenExpression(node.object);
|
|
34441
33471
|
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
34442
|
-
if (!
|
|
33472
|
+
if (!namespaceBindings.has(receiver.name)) return;
|
|
34443
33473
|
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
34444
33474
|
const message = messages.get(node.property.name);
|
|
34445
33475
|
if (message) context.report({
|
|
@@ -34504,9 +33534,15 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
34504
33534
|
});
|
|
34505
33535
|
//#endregion
|
|
34506
33536
|
//#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
|
|
33537
|
+
const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]]);
|
|
33538
|
+
const isVendoredShadcnUiFilename = (rawFilename) => {
|
|
33539
|
+
if (!rawFilename) return false;
|
|
33540
|
+
const filename = rawFilename.replaceAll("\\", "/");
|
|
33541
|
+
return (filename.startsWith("/") ? filename : `/${filename}`).includes("/components/ui/");
|
|
33542
|
+
};
|
|
34507
33543
|
const deprecatedReactImportRule = createDeprecatedReactImportRule({
|
|
34508
33544
|
source: "react",
|
|
34509
|
-
messages:
|
|
33545
|
+
messages: REACT_19_DEPRECATED_MESSAGES
|
|
34510
33546
|
});
|
|
34511
33547
|
const buildOncePerApiContext = (context) => {
|
|
34512
33548
|
const reportedMessages = /* @__PURE__ */ new Set();
|
|
@@ -34536,8 +33572,11 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
34536
33572
|
requires: ["react:19"],
|
|
34537
33573
|
tags: ["test-noise", "migration-hint"],
|
|
34538
33574
|
severity: "warn",
|
|
34539
|
-
recommendation: "
|
|
34540
|
-
create: (context) =>
|
|
33575
|
+
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
|
|
33576
|
+
create: (context) => {
|
|
33577
|
+
if (isVendoredShadcnUiFilename(context.filename)) return {};
|
|
33578
|
+
return deprecatedReactImportRule.create(buildOncePerApiContext(context));
|
|
33579
|
+
}
|
|
34541
33580
|
});
|
|
34542
33581
|
//#endregion
|
|
34543
33582
|
//#region src/plugin/constants/aria-element-roles.ts
|
|
@@ -36870,140 +35909,6 @@ const noUnescapedEntities = defineRule({
|
|
|
36870
35909
|
} })
|
|
36871
35910
|
});
|
|
36872
35911
|
//#endregion
|
|
36873
|
-
//#region src/plugin/rules/performance/no-unguarded-browser-global-in-render-or-hook-init.ts
|
|
36874
|
-
const BROWSER_GLOBAL_NAMES = new Set([
|
|
36875
|
-
"window",
|
|
36876
|
-
"document",
|
|
36877
|
-
"localStorage",
|
|
36878
|
-
"sessionStorage",
|
|
36879
|
-
"navigator",
|
|
36880
|
-
"matchMedia"
|
|
36881
|
-
]);
|
|
36882
|
-
const getTypeofBrowserGlobalName = (expression, context) => {
|
|
36883
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
36884
|
-
if (!isNodeOfType(unwrappedExpression, "UnaryExpression") || unwrappedExpression.operator !== "typeof") return null;
|
|
36885
|
-
const argument = stripParenExpression(unwrappedExpression.argument);
|
|
36886
|
-
if (isNodeOfType(argument, "Identifier")) return BROWSER_GLOBAL_NAMES.has(argument.name) && context.scopes.isGlobalReference(argument) ? argument.name : null;
|
|
36887
|
-
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;
|
|
36888
|
-
return argument.property.name;
|
|
36889
|
-
};
|
|
36890
|
-
const browserGuardCoversGlobal = (guardName, browserGlobalName) => guardName === browserGlobalName || guardName === "window" || guardName === "document";
|
|
36891
|
-
const mergeAvailability = (leftAvailability, rightAvailability) => {
|
|
36892
|
-
if (leftAvailability === null) return rightAvailability;
|
|
36893
|
-
if (rightAvailability === null) return leftAvailability;
|
|
36894
|
-
return leftAvailability === rightAvailability ? leftAvailability : null;
|
|
36895
|
-
};
|
|
36896
|
-
const readAvailabilityWhenPredicate = (expression, browserGlobalName, context, predicateResult) => {
|
|
36897
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
36898
|
-
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return readAvailabilityWhenPredicate(unwrappedExpression.argument, browserGlobalName, context, !predicateResult);
|
|
36899
|
-
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
|
|
36900
|
-
if (unwrappedExpression.operator === "&&" && predicateResult) return mergeAvailability(readAvailabilityWhenPredicate(unwrappedExpression.left, browserGlobalName, context, true), readAvailabilityWhenPredicate(unwrappedExpression.right, browserGlobalName, context, true));
|
|
36901
|
-
if (unwrappedExpression.operator === "||" && !predicateResult) return mergeAvailability(readAvailabilityWhenPredicate(unwrappedExpression.left, browserGlobalName, context, false), readAvailabilityWhenPredicate(unwrappedExpression.right, browserGlobalName, context, false));
|
|
36902
|
-
return null;
|
|
36903
|
-
}
|
|
36904
|
-
if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
|
|
36905
|
-
const leftTypeofName = getTypeofBrowserGlobalName(unwrappedExpression.left, context);
|
|
36906
|
-
const rightTypeofName = getTypeofBrowserGlobalName(unwrappedExpression.right, context);
|
|
36907
|
-
const leftComparedType = isNodeOfType(unwrappedExpression.left, "Literal") && typeof unwrappedExpression.left.value === "string" ? unwrappedExpression.left.value : null;
|
|
36908
|
-
const rightComparedType = isNodeOfType(unwrappedExpression.right, "Literal") && typeof unwrappedExpression.right.value === "string" ? unwrappedExpression.right.value : null;
|
|
36909
|
-
const guardName = leftTypeofName && rightComparedType ? leftTypeofName : rightTypeofName && leftComparedType ? rightTypeofName : null;
|
|
36910
|
-
const comparedType = leftTypeofName && rightComparedType ? rightComparedType : rightTypeofName && leftComparedType ? leftComparedType : null;
|
|
36911
|
-
if (!guardName || !browserGuardCoversGlobal(guardName, browserGlobalName)) return null;
|
|
36912
|
-
if (!comparedType) return null;
|
|
36913
|
-
const isEquality = unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==";
|
|
36914
|
-
const isInequality = unwrappedExpression.operator === "!==" || unwrappedExpression.operator === "!=";
|
|
36915
|
-
if (!isEquality && !isInequality) return null;
|
|
36916
|
-
const browserType = guardName === "matchMedia" ? "function" : "object";
|
|
36917
|
-
const browserResult = isEquality ? browserType === comparedType : browserType !== comparedType;
|
|
36918
|
-
if (browserResult === (isEquality ? comparedType === "undefined" : comparedType !== "undefined")) return null;
|
|
36919
|
-
return predicateResult === browserResult;
|
|
36920
|
-
};
|
|
36921
|
-
const isInsideAvailabilityGuard = (node, browserGlobalName, context) => {
|
|
36922
|
-
let currentNode = node;
|
|
36923
|
-
let parentNode = currentNode.parent;
|
|
36924
|
-
while (parentNode) {
|
|
36925
|
-
if (isFunctionLike$1(parentNode) && !executesDuringRender(parentNode, context.scopes)) break;
|
|
36926
|
-
if (isNodeOfType(parentNode, "LogicalExpression") && (parentNode.operator === "&&" || parentNode.operator === "||") && parentNode.right === currentNode && readAvailabilityWhenPredicate(parentNode.left, browserGlobalName, context, parentNode.operator === "&&") === true) return true;
|
|
36927
|
-
if (isNodeOfType(parentNode, "ConditionalExpression")) {
|
|
36928
|
-
if (parentNode.consequent === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, true) === true || parentNode.alternate === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, false) === true) return true;
|
|
36929
|
-
}
|
|
36930
|
-
if (isNodeOfType(parentNode, "IfStatement")) {
|
|
36931
|
-
if (parentNode.consequent === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, true) === true || parentNode.alternate === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, false) === true) return true;
|
|
36932
|
-
}
|
|
36933
|
-
currentNode = parentNode;
|
|
36934
|
-
parentNode = currentNode.parent;
|
|
36935
|
-
}
|
|
36936
|
-
return false;
|
|
36937
|
-
};
|
|
36938
|
-
const isAfterAvailabilityEarlyExit = (node, componentOrHookNode, browserGlobalName, context) => {
|
|
36939
|
-
const enclosingFunction = findEnclosingFunction(node);
|
|
36940
|
-
if (!enclosingFunction || enclosingFunction !== componentOrHookNode && !executesDuringRender(enclosingFunction, context.scopes) || !isFunctionLike$1(enclosingFunction) || !isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
|
|
36941
|
-
let currentNode = node;
|
|
36942
|
-
while (currentNode !== enclosingFunction) {
|
|
36943
|
-
const parentNode = currentNode.parent;
|
|
36944
|
-
if (!parentNode) return false;
|
|
36945
|
-
if (isNodeOfType(parentNode, "BlockStatement")) for (const statement of parentNode.body) {
|
|
36946
|
-
if (statement === currentNode) break;
|
|
36947
|
-
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
36948
|
-
if (readAvailabilityWhenPredicate(statement.test, browserGlobalName, context, false) === true && statementAlwaysExits(statement.consequent)) return true;
|
|
36949
|
-
if (readAvailabilityWhenPredicate(statement.test, browserGlobalName, context, true) === true && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
|
|
36950
|
-
}
|
|
36951
|
-
currentNode = parentNode;
|
|
36952
|
-
}
|
|
36953
|
-
return false;
|
|
36954
|
-
};
|
|
36955
|
-
const isTypeofProbe = (node) => {
|
|
36956
|
-
const expressionRoot = findTransparentExpressionRoot(node);
|
|
36957
|
-
const parentNode = expressionRoot.parent;
|
|
36958
|
-
return isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "typeof" && parentNode.argument === expressionRoot;
|
|
36959
|
-
};
|
|
36960
|
-
const noUnguardedBrowserGlobalInRenderOrHookInit = defineRule({
|
|
36961
|
-
id: "no-unguarded-browser-global-in-render-or-hook-init",
|
|
36962
|
-
title: "Browser global read during server render",
|
|
36963
|
-
severity: "error",
|
|
36964
|
-
category: "Correctness",
|
|
36965
|
-
requires: ["ssr"],
|
|
36966
|
-
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.",
|
|
36967
|
-
create: (context) => {
|
|
36968
|
-
if (isTestlikeFilename(context.filename)) return {};
|
|
36969
|
-
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
36970
|
-
let fileIsEmailTemplate = false;
|
|
36971
|
-
const reportedNodes = /* @__PURE__ */ new Set();
|
|
36972
|
-
const reportBrowserRead = (node, browserGlobalName) => {
|
|
36973
|
-
if (reportedNodes.has(node) || isTypeofProbe(node)) return;
|
|
36974
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(node, context.scopes);
|
|
36975
|
-
if (!componentOrHookNode) return;
|
|
36976
|
-
if (fileIsEmailTemplate) return;
|
|
36977
|
-
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(node) ?? node)) return;
|
|
36978
|
-
if (isGatedByFalsyInitialState(node, context.scopes)) return;
|
|
36979
|
-
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode, context.scopes)) return;
|
|
36980
|
-
if (isInsideAvailabilityGuard(node, browserGlobalName, context)) return;
|
|
36981
|
-
if (isAfterAvailabilityEarlyExit(node, componentOrHookNode, browserGlobalName, context)) return;
|
|
36982
|
-
reportedNodes.add(node);
|
|
36983
|
-
context.report({
|
|
36984
|
-
node,
|
|
36985
|
-
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.`
|
|
36986
|
-
});
|
|
36987
|
-
};
|
|
36988
|
-
return {
|
|
36989
|
-
Program(node) {
|
|
36990
|
-
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
36991
|
-
},
|
|
36992
|
-
Identifier(node) {
|
|
36993
|
-
if (!BROWSER_GLOBAL_NAMES.has(node.name)) return;
|
|
36994
|
-
if (!context.scopes.isGlobalReference(node)) return;
|
|
36995
|
-
reportBrowserRead(node, node.name);
|
|
36996
|
-
},
|
|
36997
|
-
MemberExpression(node) {
|
|
36998
|
-
if (node.computed) return;
|
|
36999
|
-
const objectNode = stripParenExpression(node.object);
|
|
37000
|
-
if (!isNodeOfType(objectNode, "Identifier") || objectNode.name !== "globalThis" || !context.scopes.isGlobalReference(objectNode) || !isNodeOfType(node.property, "Identifier") || !BROWSER_GLOBAL_NAMES.has(node.property.name)) return;
|
|
37001
|
-
reportBrowserRead(node, node.property.name);
|
|
37002
|
-
}
|
|
37003
|
-
};
|
|
37004
|
-
}
|
|
37005
|
-
});
|
|
37006
|
-
//#endregion
|
|
37007
35912
|
//#region src/plugin/constants/dom-aria-properties.ts
|
|
37008
35913
|
const ARIA_PROPERTY_NAMES = new Set([
|
|
37009
35914
|
"activedescendant",
|
|
@@ -40846,123 +39751,6 @@ const preferUseEffectEvent = defineRule({
|
|
|
40846
39751
|
}
|
|
40847
39752
|
});
|
|
40848
39753
|
//#endregion
|
|
40849
|
-
//#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
|
|
40850
|
-
const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
40851
|
-
"each",
|
|
40852
|
-
"every",
|
|
40853
|
-
"filter",
|
|
40854
|
-
"find",
|
|
40855
|
-
"findIndex",
|
|
40856
|
-
"findLast",
|
|
40857
|
-
"findLastIndex",
|
|
40858
|
-
"flatMap",
|
|
40859
|
-
"forEach",
|
|
40860
|
-
"map",
|
|
40861
|
-
"reduce",
|
|
40862
|
-
"reduceRight",
|
|
40863
|
-
"some",
|
|
40864
|
-
"sort",
|
|
40865
|
-
"toSorted"
|
|
40866
|
-
]);
|
|
40867
|
-
const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
40868
|
-
"from",
|
|
40869
|
-
"fromAsync",
|
|
40870
|
-
"groupBy"
|
|
40871
|
-
]);
|
|
40872
|
-
const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
40873
|
-
const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
|
|
40874
|
-
const isListenerRemovalViaNullHandler = (callNode) => {
|
|
40875
|
-
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
40876
|
-
const callee = unwrapChainExpression(callNode.callee);
|
|
40877
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
|
|
40878
|
-
};
|
|
40879
|
-
const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
|
|
40880
|
-
"off",
|
|
40881
|
-
"removeEventListener",
|
|
40882
|
-
"removeListener"
|
|
40883
|
-
]);
|
|
40884
|
-
const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
|
|
40885
|
-
if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
|
|
40886
|
-
const handlerArgument = callNode.arguments?.[1];
|
|
40887
|
-
return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
|
|
40888
|
-
};
|
|
40889
|
-
const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
40890
|
-
const callNode = unwrapChainExpression(node);
|
|
40891
|
-
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
40892
|
-
if (isListenerRemovalViaNullHandler(callNode)) return true;
|
|
40893
|
-
const callee = unwrapChainExpression(callNode.callee);
|
|
40894
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
40895
|
-
if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
|
|
40896
|
-
if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
|
|
40897
|
-
if (knownCleanupFunctionNames.has(callee.name)) return true;
|
|
40898
|
-
return false;
|
|
40899
|
-
}
|
|
40900
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
40901
|
-
if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
|
|
40902
|
-
if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
|
|
40903
|
-
return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
|
|
40904
|
-
}
|
|
40905
|
-
return false;
|
|
40906
|
-
};
|
|
40907
|
-
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);
|
|
40908
|
-
const isIteratorCallbackArgument = (node) => {
|
|
40909
|
-
const parentNode = node.parent;
|
|
40910
|
-
if (!isNodeOfType(parentNode, "CallExpression")) return false;
|
|
40911
|
-
if (!parentNode.arguments?.some((argument) => argument === node)) return false;
|
|
40912
|
-
const callee = unwrapChainExpression(parentNode.callee);
|
|
40913
|
-
if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
|
|
40914
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
|
|
40915
|
-
};
|
|
40916
|
-
const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
40917
|
-
let didFindRelease = false;
|
|
40918
|
-
walkAst(node, (child) => {
|
|
40919
|
-
if (didFindRelease) return false;
|
|
40920
|
-
if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
|
|
40921
|
-
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
40922
|
-
didFindRelease = true;
|
|
40923
|
-
return false;
|
|
40924
|
-
}
|
|
40925
|
-
});
|
|
40926
|
-
return didFindRelease;
|
|
40927
|
-
};
|
|
40928
|
-
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
40929
|
-
if (!isFunctionLike$1(node)) return false;
|
|
40930
|
-
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
40931
|
-
};
|
|
40932
|
-
const isProvablyNoOpCleanupFunction = (node) => {
|
|
40933
|
-
if (!isFunctionLike$1(node)) return false;
|
|
40934
|
-
let sawNoOpRemoval = false;
|
|
40935
|
-
let sawOtherCall = false;
|
|
40936
|
-
walkAst(node.body, (child) => {
|
|
40937
|
-
if (sawOtherCall) return false;
|
|
40938
|
-
if (isFunctionLike$1(child)) return false;
|
|
40939
|
-
const callNode = unwrapChainExpression(child);
|
|
40940
|
-
if (!isNodeOfType(callNode, "CallExpression")) return;
|
|
40941
|
-
const callee = unwrapChainExpression(callNode.callee);
|
|
40942
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
|
|
40943
|
-
sawNoOpRemoval = true;
|
|
40944
|
-
return;
|
|
40945
|
-
}
|
|
40946
|
-
sawOtherCall = true;
|
|
40947
|
-
});
|
|
40948
|
-
return sawNoOpRemoval && !sawOtherCall;
|
|
40949
|
-
};
|
|
40950
|
-
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
|
|
40951
|
-
if (!returnedValue) return false;
|
|
40952
|
-
const unwrappedValue = unwrapChainExpression(returnedValue);
|
|
40953
|
-
if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
|
|
40954
|
-
if (isNodeOfType(unwrappedValue, "Identifier")) {
|
|
40955
|
-
if (unwrappedValue.name === "undefined") return false;
|
|
40956
|
-
if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
|
|
40957
|
-
return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
|
|
40958
|
-
}
|
|
40959
|
-
if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
40960
|
-
if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
|
|
40961
|
-
if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
40962
|
-
if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
|
|
40963
|
-
return false;
|
|
40964
|
-
};
|
|
40965
|
-
//#endregion
|
|
40966
39754
|
//#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
|
|
40967
39755
|
const findUseEffectsInComponent = (componentBody) => {
|
|
40968
39756
|
const effectCalls = [];
|
|
@@ -42431,7 +41219,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
42431
41219
|
title: "Time or random value in JSX",
|
|
42432
41220
|
severity: "warn",
|
|
42433
41221
|
category: "Correctness",
|
|
42434
|
-
|
|
41222
|
+
disabledWhen: ["vite", "cra"],
|
|
42435
41223
|
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",
|
|
42436
41224
|
create: (context) => {
|
|
42437
41225
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
@@ -42445,7 +41233,8 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
42445
41233
|
const matched = NONDETERMINISTIC_RENDER_PATTERNS.find((pattern) => pattern.matches(node.expression));
|
|
42446
41234
|
if (matched) {
|
|
42447
41235
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
42448
|
-
if (
|
|
41236
|
+
if (isInsideClientOnlyGuard(node)) return;
|
|
41237
|
+
if (isGatedByFalsyInitialState(node)) return;
|
|
42449
41238
|
if (isInsideMotionTransitionAttribute(node)) return;
|
|
42450
41239
|
context.report({
|
|
42451
41240
|
node,
|
|
@@ -42454,10 +41243,11 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
42454
41243
|
return;
|
|
42455
41244
|
}
|
|
42456
41245
|
walkAst(node.expression, (child) => {
|
|
42457
|
-
if (isFunctionLike$1(child) && !executesDuringRender(child
|
|
41246
|
+
if (isFunctionLike$1(child) && !executesDuringRender(child)) return false;
|
|
42458
41247
|
for (const pattern of NONDETERMINISTIC_RENDER_PATTERNS) if (pattern.matches(child)) {
|
|
42459
41248
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
42460
|
-
if (
|
|
41249
|
+
if (isInsideClientOnlyGuard(child)) return;
|
|
41250
|
+
if (isGatedByFalsyInitialState(child)) return;
|
|
42461
41251
|
if (isInsideMotionTransitionAttribute(child)) return;
|
|
42462
41252
|
if (pattern.display === "new Date()" && isYearOnlyDateRead(child)) return;
|
|
42463
41253
|
context.report({
|
|
@@ -55794,18 +54584,6 @@ const reactDoctorRules = [
|
|
|
55794
54584
|
category: "Accessibility"
|
|
55795
54585
|
}
|
|
55796
54586
|
},
|
|
55797
|
-
{
|
|
55798
|
-
key: "react-doctor/no-hydration-branch-on-browser-global",
|
|
55799
|
-
id: "no-hydration-branch-on-browser-global",
|
|
55800
|
-
source: "react-doctor",
|
|
55801
|
-
originallyExternal: false,
|
|
55802
|
-
rule: {
|
|
55803
|
-
...noHydrationBranchOnBrowserGlobal,
|
|
55804
|
-
framework: "global",
|
|
55805
|
-
category: "Bugs",
|
|
55806
|
-
requires: [...new Set(["react", ...noHydrationBranchOnBrowserGlobal.requires ?? []])]
|
|
55807
|
-
}
|
|
55808
|
-
},
|
|
55809
54587
|
{
|
|
55810
54588
|
key: "react-doctor/no-img-lazy-with-high-fetchpriority",
|
|
55811
54589
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
@@ -56569,18 +55347,6 @@ const reactDoctorRules = [
|
|
|
56569
55347
|
requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
|
|
56570
55348
|
}
|
|
56571
55349
|
},
|
|
56572
|
-
{
|
|
56573
|
-
key: "react-doctor/no-unguarded-browser-global-in-render-or-hook-init",
|
|
56574
|
-
id: "no-unguarded-browser-global-in-render-or-hook-init",
|
|
56575
|
-
source: "react-doctor",
|
|
56576
|
-
originallyExternal: false,
|
|
56577
|
-
rule: {
|
|
56578
|
-
...noUnguardedBrowserGlobalInRenderOrHookInit,
|
|
56579
|
-
framework: "global",
|
|
56580
|
-
category: "Bugs",
|
|
56581
|
-
requires: [...new Set(["react", ...noUnguardedBrowserGlobalInRenderOrHookInit.requires ?? []])]
|
|
56582
|
-
}
|
|
56583
|
-
},
|
|
56584
55350
|
{
|
|
56585
55351
|
key: "react-doctor/no-unknown-property",
|
|
56586
55352
|
id: "no-unknown-property",
|
|
@@ -58913,12 +57679,10 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
58913
57679
|
"nextjs-no-use-search-params-without-suspense",
|
|
58914
57680
|
"no-dynamic-import-path",
|
|
58915
57681
|
"no-full-lodash-import",
|
|
58916
|
-
"no-hydration-branch-on-browser-global",
|
|
58917
57682
|
"no-indeterminate-attribute",
|
|
58918
57683
|
"no-locale-format-in-render",
|
|
58919
57684
|
"no-match-media-in-state-initializer",
|
|
58920
57685
|
"no-mutating-reducer-state",
|
|
58921
|
-
"no-unguarded-browser-global-in-render-or-hook-init",
|
|
58922
57686
|
"prefer-dynamic-import",
|
|
58923
57687
|
"rendering-hydration-mismatch-time",
|
|
58924
57688
|
"rn-no-legacy-shadow-styles",
|
|
@@ -59052,12 +57816,10 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
59052
57816
|
["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
|
|
59053
57817
|
["no-dynamic-import-path", collectNearestManifestDependencies],
|
|
59054
57818
|
["no-full-lodash-import", collectNearestManifestDependencies],
|
|
59055
|
-
["no-hydration-branch-on-browser-global", collectNearestManifestDependencies],
|
|
59056
57819
|
["no-indeterminate-attribute", collectNearestManifestDependencies],
|
|
59057
57820
|
["no-locale-format-in-render", collectNearestManifestDependencies],
|
|
59058
57821
|
["no-match-media-in-state-initializer", collectNearestManifestDependencies],
|
|
59059
57822
|
["no-mutating-reducer-state", collectMutatingReducerDependencies],
|
|
59060
|
-
["no-unguarded-browser-global-in-render-or-hook-init", collectNearestManifestDependencies],
|
|
59061
57823
|
["prefer-dynamic-import", collectNearestManifestDependencies],
|
|
59062
57824
|
["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
|
|
59063
57825
|
["rn-no-legacy-shadow-styles", collectLegacyArchDependencies],
|