oxlint-plugin-react-doctor 0.7.4-dev.a31f5e8 → 0.7.4-dev.b47d053
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.js +486 -907
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8406,54 +8406,6 @@ const effectListenerCleanupMismatch = defineRule({
|
|
|
8406
8406
|
} })
|
|
8407
8407
|
});
|
|
8408
8408
|
//#endregion
|
|
8409
|
-
//#region src/plugin/utils/collect-effect-invoked-functions.ts
|
|
8410
|
-
const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
|
|
8411
|
-
"then",
|
|
8412
|
-
"catch",
|
|
8413
|
-
"finally"
|
|
8414
|
-
]);
|
|
8415
|
-
const collectEffectInvokedFunctions = (effectCallback) => {
|
|
8416
|
-
const invokedFunctions = new Set([effectCallback]);
|
|
8417
|
-
const localFunctionBindings = /* @__PURE__ */ new Map();
|
|
8418
|
-
const calledBindingNames = /* @__PURE__ */ new Set();
|
|
8419
|
-
const pendingFunctions = [effectCallback];
|
|
8420
|
-
const enqueue = (candidate) => {
|
|
8421
|
-
const strippedCandidate = candidate ? stripParenExpression(candidate) : candidate;
|
|
8422
|
-
if (!isFunctionLike$1(strippedCandidate) || invokedFunctions.has(strippedCandidate)) return;
|
|
8423
|
-
invokedFunctions.add(strippedCandidate);
|
|
8424
|
-
pendingFunctions.push(strippedCandidate);
|
|
8425
|
-
};
|
|
8426
|
-
const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
|
|
8427
|
-
while (pendingFunctions.length > 0) {
|
|
8428
|
-
const currentFunction = pendingFunctions.pop();
|
|
8429
|
-
if (!currentFunction) break;
|
|
8430
|
-
walkAst(currentFunction, (child) => {
|
|
8431
|
-
if (child !== currentFunction && isFunctionLike$1(child)) {
|
|
8432
|
-
if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) localFunctionBindings.set(child.id.name, child);
|
|
8433
|
-
return false;
|
|
8434
|
-
}
|
|
8435
|
-
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
8436
|
-
const initializer = child.init ? stripParenExpression(child.init) : null;
|
|
8437
|
-
if (isFunctionLike$1(initializer)) localFunctionBindings.set(child.id.name, initializer);
|
|
8438
|
-
return;
|
|
8439
|
-
}
|
|
8440
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8441
|
-
const callee = stripParenExpression(child.callee);
|
|
8442
|
-
if (isFunctionLike$1(callee)) {
|
|
8443
|
-
enqueue(callee);
|
|
8444
|
-
return;
|
|
8445
|
-
}
|
|
8446
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
8447
|
-
calledBindingNames.add(callee.name);
|
|
8448
|
-
return;
|
|
8449
|
-
}
|
|
8450
|
-
if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
8451
|
-
});
|
|
8452
|
-
for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
|
|
8453
|
-
}
|
|
8454
|
-
return invokedFunctions;
|
|
8455
|
-
};
|
|
8456
|
-
//#endregion
|
|
8457
8409
|
//#region src/plugin/utils/is-react-hook-name.ts
|
|
8458
8410
|
const isReactHookName = (name) => {
|
|
8459
8411
|
if (!name.startsWith("use")) return false;
|
|
@@ -8509,90 +8461,46 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
8509
8461
|
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
8510
8462
|
};
|
|
8511
8463
|
//#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
8464
|
//#region src/plugin/utils/get-range-start.ts
|
|
8589
8465
|
const getRangeStart = (node) => {
|
|
8590
8466
|
const rangeStart = node.range?.[0];
|
|
8591
8467
|
return typeof rangeStart === "number" ? rangeStart : null;
|
|
8592
8468
|
};
|
|
8593
8469
|
//#endregion
|
|
8594
|
-
//#region src/plugin/utils/is-
|
|
8595
|
-
const
|
|
8470
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
8471
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
8472
|
+
let node = callExpression;
|
|
8473
|
+
let parent = node.parent;
|
|
8474
|
+
while (parent) {
|
|
8475
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
8476
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
8477
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
8478
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
8479
|
+
node = parent;
|
|
8480
|
+
parent = node.parent;
|
|
8481
|
+
continue;
|
|
8482
|
+
}
|
|
8483
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
8484
|
+
node = parent;
|
|
8485
|
+
parent = node.parent;
|
|
8486
|
+
continue;
|
|
8487
|
+
}
|
|
8488
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
8489
|
+
node = parent;
|
|
8490
|
+
parent = node.parent;
|
|
8491
|
+
continue;
|
|
8492
|
+
}
|
|
8493
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
8494
|
+
const expressions = parent.expressions ?? [];
|
|
8495
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
8496
|
+
node = parent;
|
|
8497
|
+
parent = node.parent;
|
|
8498
|
+
continue;
|
|
8499
|
+
}
|
|
8500
|
+
return false;
|
|
8501
|
+
}
|
|
8502
|
+
return false;
|
|
8503
|
+
};
|
|
8596
8504
|
//#endregion
|
|
8597
8505
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
8598
8506
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
@@ -8627,79 +8535,136 @@ const isCleanupReturningSubscribeLikeCallExpression = (node) => {
|
|
|
8627
8535
|
return true;
|
|
8628
8536
|
};
|
|
8629
8537
|
//#endregion
|
|
8630
|
-
//#region src/plugin/rules/state-and-effects/
|
|
8631
|
-
const
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8538
|
+
//#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
|
|
8539
|
+
const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
|
|
8540
|
+
"each",
|
|
8541
|
+
"every",
|
|
8542
|
+
"filter",
|
|
8543
|
+
"find",
|
|
8544
|
+
"findIndex",
|
|
8545
|
+
"findLast",
|
|
8546
|
+
"findLastIndex",
|
|
8547
|
+
"flatMap",
|
|
8548
|
+
"forEach",
|
|
8549
|
+
"map",
|
|
8550
|
+
"reduce",
|
|
8551
|
+
"reduceRight",
|
|
8552
|
+
"some",
|
|
8553
|
+
"sort",
|
|
8554
|
+
"toSorted"
|
|
8555
|
+
]);
|
|
8556
|
+
const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
8557
|
+
"from",
|
|
8558
|
+
"fromAsync",
|
|
8559
|
+
"groupBy"
|
|
8560
|
+
]);
|
|
8561
|
+
const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8562
|
+
const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
|
|
8563
|
+
const isListenerRemovalViaNullHandler = (callNode) => {
|
|
8564
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
8565
|
+
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8566
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
|
|
8637
8567
|
};
|
|
8638
|
-
const
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8568
|
+
const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
|
|
8569
|
+
"off",
|
|
8570
|
+
"removeEventListener",
|
|
8571
|
+
"removeListener"
|
|
8572
|
+
]);
|
|
8573
|
+
const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
|
|
8574
|
+
if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
|
|
8575
|
+
const handlerArgument = callNode.arguments?.[1];
|
|
8576
|
+
return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
|
|
8642
8577
|
};
|
|
8643
|
-
const
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
if (
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
if (
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
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;
|
|
8578
|
+
const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8579
|
+
const callNode = unwrapChainExpression$3(node);
|
|
8580
|
+
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
8581
|
+
if (isListenerRemovalViaNullHandler(callNode)) return true;
|
|
8582
|
+
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8583
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
8584
|
+
if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
|
|
8585
|
+
if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
|
|
8586
|
+
if (knownCleanupFunctionNames.has(callee.name)) return true;
|
|
8587
|
+
return false;
|
|
8667
8588
|
}
|
|
8668
|
-
if (isNodeOfType(
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
return rangeStart === null ? null : `function:${rangeStart}`;
|
|
8589
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
8590
|
+
if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
|
|
8591
|
+
if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
|
|
8592
|
+
return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
|
|
8673
8593
|
}
|
|
8674
|
-
return
|
|
8594
|
+
return false;
|
|
8675
8595
|
};
|
|
8676
|
-
const
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8596
|
+
const isStaticIteratorCallbackCallee = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && isNodeOfType(callee.property, "Identifier") && (callee.object.name === "Array" || callee.object.name === "Map" || callee.object.name === "Object") && STATIC_ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
|
|
8597
|
+
const isIteratorCallbackArgument = (node) => {
|
|
8598
|
+
const parentNode = node.parent;
|
|
8599
|
+
if (!isNodeOfType(parentNode, "CallExpression")) return false;
|
|
8600
|
+
if (!parentNode.arguments?.some((argument) => argument === node)) return false;
|
|
8601
|
+
const callee = unwrapChainExpression$3(parentNode.callee);
|
|
8602
|
+
if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
|
|
8603
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(callee.property.name);
|
|
8604
|
+
};
|
|
8605
|
+
const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8606
|
+
let didFindRelease = false;
|
|
8607
|
+
walkAst(node, (child) => {
|
|
8608
|
+
if (didFindRelease) return false;
|
|
8609
|
+
if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
|
|
8610
|
+
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
8611
|
+
didFindRelease = true;
|
|
8612
|
+
return false;
|
|
8613
|
+
}
|
|
8614
|
+
});
|
|
8615
|
+
return didFindRelease;
|
|
8616
|
+
};
|
|
8617
|
+
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
8618
|
+
if (!isFunctionLike$1(node)) return false;
|
|
8619
|
+
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
8620
|
+
};
|
|
8621
|
+
const isProvablyNoOpCleanupFunction = (node) => {
|
|
8622
|
+
if (!isFunctionLike$1(node)) return false;
|
|
8623
|
+
let sawNoOpRemoval = false;
|
|
8624
|
+
let sawOtherCall = false;
|
|
8625
|
+
walkAst(node.body, (child) => {
|
|
8626
|
+
if (sawOtherCall) return false;
|
|
8627
|
+
if (isFunctionLike$1(child)) return false;
|
|
8628
|
+
const callNode = unwrapChainExpression$3(child);
|
|
8629
|
+
if (!isNodeOfType(callNode, "CallExpression")) return;
|
|
8630
|
+
const callee = unwrapChainExpression$3(callNode.callee);
|
|
8631
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
|
|
8632
|
+
sawNoOpRemoval = true;
|
|
8633
|
+
return;
|
|
8634
|
+
}
|
|
8635
|
+
sawOtherCall = true;
|
|
8636
|
+
});
|
|
8637
|
+
return sawNoOpRemoval && !sawOtherCall;
|
|
8638
|
+
};
|
|
8639
|
+
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
|
|
8640
|
+
if (!returnedValue) return false;
|
|
8641
|
+
const unwrappedValue = unwrapChainExpression$3(returnedValue);
|
|
8642
|
+
if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
|
|
8643
|
+
if (isNodeOfType(unwrappedValue, "Identifier")) {
|
|
8644
|
+
if (unwrappedValue.name === "undefined") return false;
|
|
8645
|
+
if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
|
|
8646
|
+
return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
|
|
8682
8647
|
}
|
|
8683
|
-
if (
|
|
8684
|
-
if (
|
|
8685
|
-
return
|
|
8648
|
+
if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
8649
|
+
if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
|
|
8650
|
+
if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
|
|
8651
|
+
if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
|
|
8652
|
+
return false;
|
|
8653
|
+
};
|
|
8654
|
+
//#endregion
|
|
8655
|
+
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
8656
|
+
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
8657
|
+
const RESOURCE_NOUN_BY_KIND = {
|
|
8658
|
+
subscribe: "subscription",
|
|
8659
|
+
timer: "timer",
|
|
8660
|
+
socket: "connection"
|
|
8686
8661
|
};
|
|
8687
|
-
const
|
|
8688
|
-
|
|
8689
|
-
if (
|
|
8690
|
-
|
|
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
|
-
};
|
|
8662
|
+
const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
|
|
8663
|
+
const isSubscribeOrObserveCall = (node) => {
|
|
8664
|
+
if (isSubscribeLikeCallExpression(node)) return true;
|
|
8665
|
+
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
8701
8666
|
};
|
|
8702
|
-
const findSubscribeLikeUsages = (callback
|
|
8667
|
+
const findSubscribeLikeUsages = (callback) => {
|
|
8703
8668
|
const usages = [];
|
|
8704
8669
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8705
8670
|
let cleanupArgument = null;
|
|
@@ -8708,22 +8673,13 @@ const findSubscribeLikeUsages = (callback, context) => {
|
|
|
8708
8673
|
const lastCallbackStatement = callbackStatements[callbackStatements.length - 1];
|
|
8709
8674
|
if (isNodeOfType(lastCallbackStatement, "ReturnStatement") && lastCallbackStatement.argument) cleanupArgument = lastCallbackStatement.argument;
|
|
8710
8675
|
}
|
|
8711
|
-
const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
|
|
8712
8676
|
walkAst(callback, (child) => {
|
|
8713
|
-
if (child
|
|
8714
|
-
if (child === cleanupArgument) return false;
|
|
8715
|
-
if (!effectInvokedFunctions.has(child) && !isSynchronousIteratorCallback(child)) return false;
|
|
8716
|
-
}
|
|
8677
|
+
if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
|
|
8717
8678
|
if (isSocketConstruction(child)) {
|
|
8718
8679
|
usages.push({
|
|
8719
8680
|
kind: "socket",
|
|
8720
8681
|
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
|
|
8682
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8727
8683
|
});
|
|
8728
8684
|
return;
|
|
8729
8685
|
}
|
|
@@ -8732,348 +8688,118 @@ const findSubscribeLikeUsages = (callback, context) => {
|
|
|
8732
8688
|
usages.push({
|
|
8733
8689
|
kind: "timer",
|
|
8734
8690
|
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
|
|
8691
|
+
resourceName: child.callee.name
|
|
8741
8692
|
});
|
|
8742
8693
|
return;
|
|
8743
8694
|
}
|
|
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
|
-
|
|
8695
|
+
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) usages.push({
|
|
8696
|
+
kind: "subscribe",
|
|
8697
|
+
node: child,
|
|
8698
|
+
resourceName: child.callee.property.name
|
|
8699
|
+
});
|
|
8700
|
+
});
|
|
8701
|
+
return usages;
|
|
8702
|
+
};
|
|
8703
|
+
const collectCleanupBindings = (effectCallback) => {
|
|
8704
|
+
const bindings = {
|
|
8705
|
+
cleanupFunctionNames: /* @__PURE__ */ new Set(),
|
|
8706
|
+
subscriptionNames: /* @__PURE__ */ new Set(),
|
|
8707
|
+
effectScopeVariableNames: /* @__PURE__ */ new Set()
|
|
8708
|
+
};
|
|
8709
|
+
if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
|
|
8710
|
+
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
|
|
8711
|
+
walkAst(effectCallback.body, (child) => {
|
|
8712
|
+
if (!isSubscribeOrObserveCall(child)) return;
|
|
8713
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8714
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
8715
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
8716
|
+
bindings.subscriptionNames.add(child.callee.object.name);
|
|
8717
|
+
});
|
|
8718
|
+
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
8719
|
+
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
8720
|
+
for (const declarator of child.declarations ?? []) {
|
|
8721
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
8722
|
+
const bindingName = declarator.id.name;
|
|
8723
|
+
bindings.effectScopeVariableNames.add(bindingName);
|
|
8724
|
+
const init = declarator.init;
|
|
8725
|
+
if (!init) continue;
|
|
8726
|
+
if (isSocketConstruction(init)) {
|
|
8727
|
+
bindings.subscriptionNames.add(bindingName);
|
|
8728
|
+
continue;
|
|
8729
|
+
}
|
|
8730
|
+
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
8731
|
+
if (isSubscribeLikeCallExpression(init)) {
|
|
8732
|
+
bindings.subscriptionNames.add(bindingName);
|
|
8733
|
+
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
8734
|
+
}
|
|
8753
8735
|
}
|
|
8754
8736
|
});
|
|
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) => {
|
|
8737
|
+
walkAst(effectCallback.body, (child) => {
|
|
8738
|
+
if (child !== effectCallback.body && (isNodeOfType(child, "ArrowFunctionExpression") || isNodeOfType(child, "FunctionExpression"))) return false;
|
|
8739
|
+
if (isNodeOfType(child, "FunctionDeclaration") && child.id && isCleanupFunctionLike(child, bindings.cleanupFunctionNames, bindings.subscriptionNames)) {
|
|
8740
|
+
bindings.cleanupFunctionNames.add(child.id.name);
|
|
8741
|
+
return false;
|
|
8742
|
+
}
|
|
8743
|
+
});
|
|
8744
|
+
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
8745
|
+
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
8746
|
+
for (const declarator of child.declarations ?? []) {
|
|
8747
|
+
if (!isNodeOfType(declarator.id, "Identifier") || !declarator.init) continue;
|
|
8748
|
+
if (isCleanupFunctionLike(declarator.init, bindings.cleanupFunctionNames, bindings.subscriptionNames)) bindings.cleanupFunctionNames.add(declarator.id.name);
|
|
8749
|
+
}
|
|
8750
|
+
});
|
|
8751
|
+
walkAst(effectCallback.body, (child) => {
|
|
8752
|
+
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);
|
|
8753
|
+
});
|
|
8754
|
+
return bindings;
|
|
8755
|
+
};
|
|
8756
|
+
const removeSynchronouslyReleasedUsages = (callback, usages) => {
|
|
8820
8757
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8821
8758
|
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
8822
|
-
const
|
|
8759
|
+
const releaseStarts = [];
|
|
8823
8760
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8824
|
-
if (!
|
|
8825
|
-
|
|
8761
|
+
if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
|
|
8762
|
+
const releaseStart = getRangeStart(child);
|
|
8763
|
+
if (releaseStart !== null) releaseStarts.push(releaseStart);
|
|
8826
8764
|
});
|
|
8827
|
-
if (
|
|
8765
|
+
if (releaseStarts.length === 0) return usages;
|
|
8828
8766
|
return usages.filter((usage) => {
|
|
8829
8767
|
const usageStart = getRangeStart(usage.node);
|
|
8830
8768
|
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);
|
|
8836
|
-
});
|
|
8837
|
-
};
|
|
8838
|
-
const resolveIteratorCollectionKey = (expression, context) => {
|
|
8839
|
-
if (!expression || !isNodeOfType(expression, "Identifier")) return null;
|
|
8840
|
-
const symbol = context.scopes.symbolFor(expression);
|
|
8841
|
-
if (!symbol || symbol.kind !== "parameter") return null;
|
|
8842
|
-
let callbackNode = symbol.bindingIdentifier.parent;
|
|
8843
|
-
while (callbackNode && !isFunctionLike$1(callbackNode)) callbackNode = callbackNode.parent;
|
|
8844
|
-
if (!callbackNode || !isFunctionLike$1(callbackNode)) return null;
|
|
8845
|
-
const callNode = callbackNode.parent;
|
|
8846
|
-
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8847
|
-
const callee = stripParenExpression(callNode.callee);
|
|
8848
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
|
|
8849
|
-
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from" && callNode.arguments?.[1] === callbackNode) return resolveExpressionKey$1(callNode.arguments[0], context);
|
|
8850
|
-
if (callNode.arguments?.[0] === callbackNode) return resolveExpressionKey$1(callee.object, context);
|
|
8851
|
-
}
|
|
8852
|
-
return null;
|
|
8853
|
-
};
|
|
8854
|
-
const findCollectionMappingCall = (callbackNode) => {
|
|
8855
|
-
if (!isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression") || callbackNode.async || callbackNode.generator) return null;
|
|
8856
|
-
const callNode = callbackNode.parent;
|
|
8857
|
-
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8858
|
-
const callee = stripParenExpression(callNode.callee);
|
|
8859
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return null;
|
|
8860
|
-
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from" && callNode.arguments?.[1] === callbackNode) return callNode;
|
|
8861
|
-
return callee.property.name === "map" && callNode.arguments?.[0] === callbackNode ? callNode : null;
|
|
8862
|
-
};
|
|
8863
|
-
const findMappedResourceCollectionKey = (resourceNode, context) => {
|
|
8864
|
-
const callbackNode = findEnclosingFunction(resourceNode);
|
|
8865
|
-
if (!callbackNode || !isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression")) return null;
|
|
8866
|
-
const mappingCall = findCollectionMappingCall(callbackNode);
|
|
8867
|
-
if (!mappingCall) return null;
|
|
8868
|
-
if (isNodeOfType(callbackNode.body, "BlockStatement")) {
|
|
8869
|
-
const resourceRoot = findTransparentExpressionRoot(resourceNode);
|
|
8870
|
-
const resourceDeclarator = resourceRoot.parent;
|
|
8871
|
-
const resourceDeclaration = resourceDeclarator?.parent;
|
|
8872
|
-
if (!isNodeOfType(resourceDeclarator, "VariableDeclarator") || resourceDeclarator.init !== resourceRoot || !isNodeOfType(resourceDeclarator.id, "Identifier") || !isNodeOfType(resourceDeclaration, "VariableDeclaration") || resourceDeclaration.kind !== "const" || resourceDeclaration.parent !== callbackNode.body) return null;
|
|
8873
|
-
const returnStatements = [];
|
|
8874
|
-
walkAst(callbackNode.body, (child) => {
|
|
8875
|
-
if (child !== callbackNode.body && isFunctionLike$1(child)) return false;
|
|
8876
|
-
if (isNodeOfType(child, "ReturnStatement")) returnStatements.push(child);
|
|
8877
|
-
});
|
|
8878
|
-
const returnStatement = returnStatements[0];
|
|
8879
|
-
const callbackStatements = callbackNode.body.body ?? [];
|
|
8880
|
-
const returnedIdentifier = isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument ? stripParenExpression(returnStatement.argument) : null;
|
|
8881
|
-
const resourceSymbol = context.scopes.symbolFor(resourceDeclarator.id);
|
|
8882
|
-
if (returnStatements.length !== 1 || callbackStatements[callbackStatements.length - 1] !== returnStatement || !isNodeOfType(returnedIdentifier, "Identifier") || !resourceSymbol || context.scopes.symbolFor(returnedIdentifier)?.id !== resourceSymbol.id || !doMatchingNodesCoverEveryPathAfterUsage(resourceNode, [returnStatement], context)) return null;
|
|
8883
|
-
} else if (findTransparentExpressionRoot(resourceNode) !== callbackNode.body) return null;
|
|
8884
|
-
const mappingRoot = findTransparentExpressionRoot(mappingCall);
|
|
8885
|
-
const collectionDeclarator = mappingRoot.parent;
|
|
8886
|
-
return isNodeOfType(collectionDeclarator, "VariableDeclarator") && collectionDeclarator.init === mappingRoot ? resolveExpressionKey$1(collectionDeclarator.id, context) : null;
|
|
8887
|
-
};
|
|
8888
|
-
const findContainingCollectionKey = (resourceNode, context) => {
|
|
8889
|
-
const mappedCollectionKey = findMappedResourceCollectionKey(resourceNode, context);
|
|
8890
|
-
if (mappedCollectionKey !== null) return mappedCollectionKey;
|
|
8891
|
-
let currentNode = resourceNode;
|
|
8892
|
-
let parentNode = currentNode.parent;
|
|
8893
|
-
while (parentNode) {
|
|
8894
|
-
if (isFunctionLike$1(parentNode)) return null;
|
|
8895
|
-
if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode) return resolveExpressionKey$1(parentNode.id, context);
|
|
8896
|
-
currentNode = parentNode;
|
|
8897
|
-
parentNode = currentNode.parent;
|
|
8898
|
-
}
|
|
8899
|
-
return null;
|
|
8900
|
-
};
|
|
8901
|
-
const isWithinAssignmentTarget = (identifier) => {
|
|
8902
|
-
let currentNode = identifier;
|
|
8903
|
-
let parentNode = currentNode.parent;
|
|
8904
|
-
while (parentNode) {
|
|
8905
|
-
if (isNodeOfType(parentNode, "AssignmentExpression")) return parentNode.left === currentNode;
|
|
8906
|
-
if (isNodeOfType(parentNode, "UpdateExpression") || isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "delete") return parentNode.argument === currentNode;
|
|
8907
|
-
if (isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return parentNode.left === currentNode;
|
|
8908
|
-
currentNode = parentNode;
|
|
8909
|
-
parentNode = currentNode.parent;
|
|
8910
|
-
}
|
|
8911
|
-
return false;
|
|
8912
|
-
};
|
|
8913
|
-
const resolveStableValue = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
8914
|
-
if (!expression) return null;
|
|
8915
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
8916
|
-
if (!isNodeOfType(unwrappedExpression, "Identifier")) return unwrappedExpression;
|
|
8917
|
-
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
8918
|
-
const isUnreassignedMutableBinding = (symbol?.kind === "let" || symbol?.kind === "var") && isNodeOfType(symbol.declarationNode, "VariableDeclarator") && symbol.declarationNode.id === symbol.bindingIdentifier && symbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier)) && symbol.scope.symbols.filter((candidate) => candidate.name === symbol.name).length === 1;
|
|
8919
|
-
const recursiveFunctionSymbol = symbol?.kind === "function" && isFunctionLike$1(symbol.declarationNode) ? context.scopes.ownScopeFor(symbol.declarationNode)?.symbols.find((candidate) => candidate.name === symbol.name && candidate.declarationNode === symbol.declarationNode) : null;
|
|
8920
|
-
const isUnreassignedFunctionBinding = symbol?.kind === "function" && symbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier)) && (!recursiveFunctionSymbol || recursiveFunctionSymbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier))) && symbol.scope.symbols.filter((candidate) => candidate.name === symbol.name).length === 1;
|
|
8921
|
-
if (!symbol || symbol.kind !== "const" && !isUnreassignedMutableBinding && !isUnreassignedFunctionBinding || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return unwrappedExpression;
|
|
8922
|
-
visitedSymbolIds.add(symbol.id);
|
|
8923
|
-
return resolveStableValue(symbol.initializer, context, visitedSymbolIds);
|
|
8924
|
-
};
|
|
8925
|
-
const resolveObjectExpression = (expression, context) => {
|
|
8926
|
-
const resolvedExpression = resolveStableValue(expression, context);
|
|
8927
|
-
return isNodeOfType(resolvedExpression, "ObjectExpression") ? resolvedExpression : null;
|
|
8928
|
-
};
|
|
8929
|
-
const getListenerAbortControllerKey = (usage, context) => {
|
|
8930
|
-
if (usage.registrationVerbName !== "addEventListener" || !isNodeOfType(usage.node, "CallExpression")) return null;
|
|
8931
|
-
const optionsArgument = usage.node.arguments?.[2];
|
|
8932
|
-
const optionsObject = resolveObjectExpression(optionsArgument, context);
|
|
8933
|
-
if (!optionsObject) return null;
|
|
8934
|
-
for (const property of optionsObject.properties ?? []) {
|
|
8935
|
-
if (!isNodeOfType(property, "Property") || getStaticPropertyKeyName(property) !== "signal") continue;
|
|
8936
|
-
const signalKey = resolveExpressionKey$1(property.value, context);
|
|
8937
|
-
return signalKey?.endsWith(".signal") ? signalKey.slice(0, -7) : null;
|
|
8938
|
-
}
|
|
8939
|
-
return null;
|
|
8940
|
-
};
|
|
8941
|
-
const SYNCHRONOUS_ITERATOR_METHOD_NAMES$1 = new Set([
|
|
8942
|
-
"every",
|
|
8943
|
-
"filter",
|
|
8944
|
-
"flatMap",
|
|
8945
|
-
"forEach",
|
|
8946
|
-
"map",
|
|
8947
|
-
"reduce",
|
|
8948
|
-
"reduceRight",
|
|
8949
|
-
"some"
|
|
8950
|
-
]);
|
|
8951
|
-
const isSynchronousIteratorCallback = (functionNode) => {
|
|
8952
|
-
const callNode = functionNode.parent;
|
|
8953
|
-
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
8954
|
-
const callee = stripParenExpression(callNode.callee);
|
|
8955
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
8956
|
-
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from") return callNode.arguments?.[1] === functionNode;
|
|
8957
|
-
return SYNCHRONOUS_ITERATOR_METHOD_NAMES$1.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
|
|
8958
|
-
};
|
|
8959
|
-
const findDirectCallForReference = (identifier) => {
|
|
8960
|
-
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
8961
|
-
const callNode = expressionRoot.parent;
|
|
8962
|
-
return isNodeOfType(callNode, "CallExpression") && callNode.callee === expressionRoot ? callNode : null;
|
|
8963
|
-
};
|
|
8964
|
-
const findSingleDirectInvocation = (functionNode, caller, context) => {
|
|
8965
|
-
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
8966
|
-
if (!bindingIdentifier || resolveStableValue(bindingIdentifier, context) !== functionNode) return null;
|
|
8967
|
-
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
8968
|
-
if (!symbol) return null;
|
|
8969
|
-
const invocationCalls = symbol.references.flatMap((reference) => {
|
|
8970
|
-
const callNode = findDirectCallForReference(reference.identifier);
|
|
8971
|
-
return callNode ? [callNode] : [];
|
|
8769
|
+
return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
|
|
8972
8770
|
});
|
|
8973
|
-
if (invocationCalls.length !== 1) return null;
|
|
8974
|
-
const invocationCall = invocationCalls[0];
|
|
8975
|
-
return findEnclosingFunction(invocationCall) === caller && isNodeReachableWithinFunction(invocationCall, context) ? invocationCall : null;
|
|
8976
8771
|
};
|
|
8977
|
-
const
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
const
|
|
8983
|
-
|
|
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
|
-
}
|
|
8772
|
+
const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
|
|
8773
|
+
if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
|
|
8774
|
+
const returnStart = getRangeStart(returnStatement);
|
|
8775
|
+
if (returnStart === null) return true;
|
|
8776
|
+
return usages.some((usage) => {
|
|
8777
|
+
const usageStart = getRangeStart(usage.node);
|
|
8778
|
+
return usageStart === null || usageStart < returnStart;
|
|
9016
8779
|
});
|
|
9017
|
-
return didCleanupFunctionMatch;
|
|
9018
8780
|
};
|
|
9019
|
-
const
|
|
8781
|
+
const effectHasCleanupReturn = (callback, usages) => {
|
|
9020
8782
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
9021
|
-
if (
|
|
9022
|
-
|
|
9023
|
-
|
|
8783
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return isCleanupReturningSubscribeLikeCallExpression(callback.body);
|
|
8784
|
+
const cleanupBindings = collectCleanupBindings(callback);
|
|
8785
|
+
let didFindCleanupReturn = false;
|
|
9024
8786
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8787
|
+
if (didFindCleanupReturn) return;
|
|
9025
8788
|
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);
|
|
8789
|
+
if (!cleanupReturnRunsAfterUsage(child, usages)) return;
|
|
8790
|
+
if (isCleanupReturn(child.argument, cleanupBindings.cleanupFunctionNames, cleanupBindings.subscriptionNames, { allowOpaqueReturn: true })) didFindCleanupReturn = true;
|
|
9048
8791
|
});
|
|
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));
|
|
8792
|
+
return didFindCleanupReturn;
|
|
9068
8793
|
};
|
|
9069
|
-
const
|
|
8794
|
+
const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
|
|
8795
|
+
const isSelfReleasingListenerOptionProperty = (property) => {
|
|
9070
8796
|
if (!isNodeOfType(property, "Property")) return false;
|
|
9071
8797
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
|
|
9072
|
-
if (keyName === "signal") return
|
|
8798
|
+
if (keyName === "signal") return true;
|
|
9073
8799
|
if (keyName !== "once") return false;
|
|
9074
8800
|
return isNodeOfType(property.value, "Literal") && property.value.value === true;
|
|
9075
8801
|
};
|
|
9076
|
-
const hasSelfReleasingListenerOptions = (node
|
|
8802
|
+
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
|
|
9077
8803
|
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
9078
8804
|
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
9079
8805
|
["addListener", new Set([
|
|
@@ -9099,185 +8825,85 @@ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
|
9099
8825
|
"teardown"
|
|
9100
8826
|
]);
|
|
9101
8827
|
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8828
|
+
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
9102
8829
|
const getReleaseVerbName = (node) => {
|
|
8830
|
+
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
9103
8831
|
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
9104
8832
|
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
9105
8833
|
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
|
-
}
|
|
8834
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8835
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
9111
8836
|
return null;
|
|
9112
8837
|
};
|
|
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
8838
|
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;
|
|
8839
|
+
const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
|
|
8840
|
+
let didFindPairedRelease = false;
|
|
8841
|
+
walkAst(body, (child) => {
|
|
8842
|
+
if (didFindPairedRelease) return false;
|
|
8843
|
+
if (child !== body && isFunctionLike$1(child)) return false;
|
|
8844
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8845
|
+
if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
|
|
8846
|
+
didFindPairedRelease = true;
|
|
9189
8847
|
return false;
|
|
9190
8848
|
}
|
|
9191
8849
|
});
|
|
9192
|
-
return
|
|
8850
|
+
return didFindPairedRelease;
|
|
9193
8851
|
};
|
|
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);
|
|
8852
|
+
const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
|
|
8853
|
+
const collectFileReleaseVerbNames = (anyNode) => {
|
|
8854
|
+
let programNode = anyNode;
|
|
8855
|
+
while (programNode.parent) programNode = programNode.parent;
|
|
8856
|
+
const cached = fileReleaseVerbNamesCache.get(programNode);
|
|
8857
|
+
if (cached) return cached;
|
|
8858
|
+
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
8859
|
+
walkAst(programNode, (child) => {
|
|
8860
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8861
|
+
if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
|
|
8862
|
+
});
|
|
8863
|
+
fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
|
|
8864
|
+
return releaseVerbNames;
|
|
9211
8865
|
};
|
|
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
|
-
}
|
|
8866
|
+
const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
|
|
8867
|
+
const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
|
|
8868
|
+
const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
|
|
8869
|
+
if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
|
|
8870
|
+
for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
|
|
9225
8871
|
return false;
|
|
9226
8872
|
};
|
|
9227
|
-
const findRetainedFunctionLeak = (retainedFunction
|
|
8873
|
+
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
9228
8874
|
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
9229
8875
|
const body = retainedFunction.body;
|
|
9230
8876
|
if (!body) return null;
|
|
8877
|
+
const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
|
|
9231
8878
|
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
8879
|
walkAst(body, (child) => {
|
|
9236
8880
|
if (leak !== null) return false;
|
|
9237
8881
|
if (isFunctionLike$1(child)) return false;
|
|
9238
|
-
if (
|
|
9239
|
-
|
|
8882
|
+
if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
|
|
8883
|
+
if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
|
|
8884
|
+
leak = {
|
|
9240
8885
|
kind: "socket",
|
|
9241
8886
|
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
|
|
8887
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
9248
8888
|
};
|
|
9249
|
-
|
|
9250
|
-
leak = socketUsage;
|
|
9251
|
-
return false;
|
|
9252
|
-
}
|
|
8889
|
+
return false;
|
|
9253
8890
|
}
|
|
9254
8891
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
9255
|
-
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && !
|
|
9256
|
-
|
|
8892
|
+
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
|
|
8893
|
+
leak = {
|
|
9257
8894
|
kind: "timer",
|
|
9258
8895
|
node: child,
|
|
9259
|
-
resourceName: "setInterval"
|
|
9260
|
-
handleKey: findAssignedResourceKey(child, context),
|
|
9261
|
-
receiverKey: null,
|
|
9262
|
-
registrationVerbName: "setInterval",
|
|
9263
|
-
eventKey: null,
|
|
9264
|
-
handlerKey: null
|
|
8896
|
+
resourceName: "setInterval"
|
|
9265
8897
|
};
|
|
9266
|
-
|
|
9267
|
-
leak = timerUsage;
|
|
9268
|
-
return false;
|
|
9269
|
-
}
|
|
8898
|
+
return false;
|
|
9270
8899
|
}
|
|
9271
|
-
if (isSubscribeOrObserveCall(child) &&
|
|
9272
|
-
const
|
|
9273
|
-
|
|
8900
|
+
if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
|
|
8901
|
+
const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
|
|
8902
|
+
if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
|
|
9274
8903
|
kind: "subscribe",
|
|
9275
8904
|
node: child,
|
|
9276
|
-
resourceName:
|
|
9277
|
-
handleKey: findAssignedResourceKey(child, context),
|
|
9278
|
-
...registrationDetails
|
|
8905
|
+
resourceName: registrationVerbName
|
|
9279
8906
|
};
|
|
9280
|
-
if (!hasSelfReleasingListenerOptions(child, context) && !hasReleaseForUsage(subscriptionUsage)) leak = subscriptionUsage;
|
|
9281
8907
|
return false;
|
|
9282
8908
|
}
|
|
9283
8909
|
});
|
|
@@ -9289,26 +8915,6 @@ const isRetainedComponentScopeFunction = (functionNode) => {
|
|
|
9289
8915
|
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
9290
8916
|
return enclosingComponentOrHookName(functionNode) !== null;
|
|
9291
8917
|
};
|
|
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
8918
|
const effectNeedsCleanup = defineRule({
|
|
9313
8919
|
id: "effect-needs-cleanup",
|
|
9314
8920
|
title: "Effect subscription or timer never cleaned up",
|
|
@@ -9317,8 +8923,7 @@ const effectNeedsCleanup = defineRule({
|
|
|
9317
8923
|
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
8924
|
create: (context) => {
|
|
9319
8925
|
const reportRetainedLeak = (retainedFunction) => {
|
|
9320
|
-
|
|
9321
|
-
const leak = findRetainedFunctionLeak(retainedFunction, context);
|
|
8926
|
+
const leak = findRetainedFunctionLeak(retainedFunction);
|
|
9322
8927
|
if (!leak) return;
|
|
9323
8928
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
9324
8929
|
context.report({
|
|
@@ -9330,31 +8935,30 @@ const effectNeedsCleanup = defineRule({
|
|
|
9330
8935
|
CallExpression(node) {
|
|
9331
8936
|
if (isHookCall$2(node, "useCallback")) {
|
|
9332
8937
|
const retainedCallback = getEffectCallback(node);
|
|
9333
|
-
if (retainedCallback
|
|
8938
|
+
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
9334
8939
|
return;
|
|
9335
8940
|
}
|
|
9336
|
-
if (!isHookCall$2(node,
|
|
8941
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
9337
8942
|
const callback = getEffectCallback(node);
|
|
9338
8943
|
if (!callback) return;
|
|
9339
|
-
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback
|
|
8944
|
+
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
|
|
9340
8945
|
if (usages.length === 0) return;
|
|
9341
|
-
|
|
9342
|
-
|
|
8946
|
+
if (effectHasCleanupReturn(callback, usages)) return;
|
|
8947
|
+
const firstUsage = usages[0];
|
|
9343
8948
|
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
9344
|
-
const hookName = getCalleeName$2(node) ?? "effect";
|
|
9345
8949
|
context.report({
|
|
9346
8950
|
node,
|
|
9347
|
-
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in
|
|
8951
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
9348
8952
|
});
|
|
9349
8953
|
},
|
|
9350
8954
|
FunctionDeclaration(node) {
|
|
9351
8955
|
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9352
8956
|
},
|
|
9353
8957
|
ArrowFunctionExpression(node) {
|
|
9354
|
-
if (isRetainedComponentScopeFunction(node)
|
|
8958
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9355
8959
|
},
|
|
9356
8960
|
FunctionExpression(node) {
|
|
9357
|
-
if (isRetainedComponentScopeFunction(node)
|
|
8961
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
9358
8962
|
}
|
|
9359
8963
|
};
|
|
9360
8964
|
}
|
|
@@ -12738,6 +12342,20 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
12738
12342
|
return names;
|
|
12739
12343
|
};
|
|
12740
12344
|
//#endregion
|
|
12345
|
+
//#region src/plugin/utils/get-function-binding-name.ts
|
|
12346
|
+
const getFunctionBindingIdentifier = (functionNode) => {
|
|
12347
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
12348
|
+
const parent = functionNode.parent;
|
|
12349
|
+
if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
|
|
12350
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionNode && isNodeOfType(parent.left, "Identifier")) return parent.left;
|
|
12351
|
+
if (isNodeOfType(parent, "CallExpression")) {
|
|
12352
|
+
const callParent = parent.parent;
|
|
12353
|
+
if (isNodeOfType(callParent, "VariableDeclarator") && isNodeOfType(callParent.id, "Identifier")) return callParent.id;
|
|
12354
|
+
}
|
|
12355
|
+
return null;
|
|
12356
|
+
};
|
|
12357
|
+
const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(functionNode)?.name ?? null;
|
|
12358
|
+
//#endregion
|
|
12741
12359
|
//#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
|
|
12742
12360
|
const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
|
|
12743
12361
|
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
@@ -13882,7 +13500,7 @@ const jsHoistIntl = defineRule({
|
|
|
13882
13500
|
});
|
|
13883
13501
|
//#endregion
|
|
13884
13502
|
//#region src/plugin/utils/create-loop-aware-visitors.ts
|
|
13885
|
-
const ITERATOR_CALLBACK_METHOD_NAMES
|
|
13503
|
+
const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
13886
13504
|
"map",
|
|
13887
13505
|
"flatMap",
|
|
13888
13506
|
"forEach",
|
|
@@ -13901,7 +13519,7 @@ const isIteratorCallback = (node) => {
|
|
|
13901
13519
|
const parent = node.parent;
|
|
13902
13520
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
13903
13521
|
if (!parent.arguments.includes(node)) return false;
|
|
13904
|
-
return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES
|
|
13522
|
+
return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(parent.callee.property.name);
|
|
13905
13523
|
};
|
|
13906
13524
|
const createLoopAwareVisitors = (innerVisitors, options = {}) => {
|
|
13907
13525
|
let loopDepth = 0;
|
|
@@ -14130,7 +13748,7 @@ const findIndexedArrayObject = (callbackBody, indexParameterName) => {
|
|
|
14130
13748
|
});
|
|
14131
13749
|
return indexedArrayObject;
|
|
14132
13750
|
};
|
|
14133
|
-
const unwrapChainExpression$
|
|
13751
|
+
const unwrapChainExpression$2 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
14134
13752
|
const LENGTH_EQUALITY_OPERATORS = new Set(["===", "=="]);
|
|
14135
13753
|
const LENGTH_MISMATCH_OPERATORS = new Set([
|
|
14136
13754
|
"!==",
|
|
@@ -14142,17 +13760,17 @@ const LENGTH_MISMATCH_OPERATORS = new Set([
|
|
|
14142
13760
|
]);
|
|
14143
13761
|
const LENGTH_ANY_COMPARISON_OPERATORS = new Set([...LENGTH_EQUALITY_OPERATORS, ...LENGTH_MISMATCH_OPERATORS]);
|
|
14144
13762
|
const isLengthComparison = (candidate, receiverArray, indexedArray, operators) => {
|
|
14145
|
-
const binaryGuard = unwrapChainExpression$
|
|
13763
|
+
const binaryGuard = unwrapChainExpression$2(candidate);
|
|
14146
13764
|
if (!isNodeOfType(binaryGuard, "BinaryExpression")) return false;
|
|
14147
13765
|
if (!operators.has(binaryGuard.operator)) return false;
|
|
14148
|
-
const leftSide = unwrapChainExpression$
|
|
14149
|
-
const rightSide = unwrapChainExpression$
|
|
13766
|
+
const leftSide = unwrapChainExpression$2(binaryGuard.left);
|
|
13767
|
+
const rightSide = unwrapChainExpression$2(binaryGuard.right);
|
|
14150
13768
|
if (!isMemberProperty(leftSide, "length")) return false;
|
|
14151
13769
|
if (!isMemberProperty(rightSide, "length")) return false;
|
|
14152
|
-
const leftLengthObject = unwrapChainExpression$
|
|
14153
|
-
const rightLengthObject = unwrapChainExpression$
|
|
14154
|
-
const normalizedReceiver = unwrapChainExpression$
|
|
14155
|
-
const normalizedIndexed = unwrapChainExpression$
|
|
13770
|
+
const leftLengthObject = unwrapChainExpression$2(leftSide.object);
|
|
13771
|
+
const rightLengthObject = unwrapChainExpression$2(rightSide.object);
|
|
13772
|
+
const normalizedReceiver = unwrapChainExpression$2(receiverArray);
|
|
13773
|
+
const normalizedIndexed = unwrapChainExpression$2(indexedArray);
|
|
14156
13774
|
const matchesReceiverThenIndexed = areExpressionsStructurallyEqual(leftLengthObject, normalizedReceiver) && areExpressionsStructurallyEqual(rightLengthObject, normalizedIndexed);
|
|
14157
13775
|
const matchesIndexedThenReceiver = areExpressionsStructurallyEqual(leftLengthObject, normalizedIndexed) && areExpressionsStructurallyEqual(rightLengthObject, normalizedReceiver);
|
|
14158
13776
|
return matchesReceiverThenIndexed || matchesIndexedThenReceiver;
|
|
@@ -14239,18 +13857,18 @@ const LENGTH_PRESERVING_METHOD_NAMES = new Set([
|
|
|
14239
13857
|
"toReversed"
|
|
14240
13858
|
]);
|
|
14241
13859
|
const peelLengthPreservingDerivation = (expression) => {
|
|
14242
|
-
let current = unwrapChainExpression$
|
|
13860
|
+
let current = unwrapChainExpression$2(expression);
|
|
14243
13861
|
for (;;) {
|
|
14244
13862
|
if (isNodeOfType(current, "ArrayExpression") && current.elements?.length === 1 && isNodeOfType(current.elements[0], "SpreadElement")) {
|
|
14245
|
-
current = unwrapChainExpression$
|
|
13863
|
+
current = unwrapChainExpression$2(current.elements[0].argument);
|
|
14246
13864
|
continue;
|
|
14247
13865
|
}
|
|
14248
13866
|
if (isNodeOfType(current, "CallExpression")) {
|
|
14249
|
-
const callee = unwrapChainExpression$
|
|
13867
|
+
const callee = unwrapChainExpression$2(current.callee);
|
|
14250
13868
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
14251
|
-
const calleeObject = unwrapChainExpression$
|
|
13869
|
+
const calleeObject = unwrapChainExpression$2(callee.object);
|
|
14252
13870
|
if (isNodeOfType(calleeObject, "Identifier") && calleeObject.name === "Array" && callee.property.name === "from" && current.arguments?.length === 1) {
|
|
14253
|
-
current = unwrapChainExpression$
|
|
13871
|
+
current = unwrapChainExpression$2(current.arguments[0]);
|
|
14254
13872
|
continue;
|
|
14255
13873
|
}
|
|
14256
13874
|
if (LENGTH_PRESERVING_METHOD_NAMES.has(callee.property.name)) {
|
|
@@ -14295,7 +13913,7 @@ const resolveComparedArraySource = (expression, scopeNode) => {
|
|
|
14295
13913
|
const initializer = findConstInitializer(current.name, scopeNode);
|
|
14296
13914
|
if (!initializer) return current;
|
|
14297
13915
|
const peeledInitializer = peelLengthPreservingDerivation(initializer);
|
|
14298
|
-
if (peeledInitializer === unwrapChainExpression$
|
|
13916
|
+
if (peeledInitializer === unwrapChainExpression$2(initializer)) return current;
|
|
14299
13917
|
current = peeledInitializer;
|
|
14300
13918
|
}
|
|
14301
13919
|
return current;
|
|
@@ -19989,41 +19607,89 @@ const nextjsMissingMetadata = defineRule({
|
|
|
19989
19607
|
node: programNode,
|
|
19990
19608
|
message: "This page has no metadata, so search engines and social previews get no title or description."
|
|
19991
19609
|
});
|
|
19992
|
-
} })
|
|
19993
|
-
});
|
|
19994
|
-
//#endregion
|
|
19995
|
-
//#region src/plugin/rules/nextjs/nextjs-no-a-element.ts
|
|
19996
|
-
const getJsxAttributeLiteralValue = (attributeValue) => {
|
|
19997
|
-
if (!attributeValue) return void 0;
|
|
19998
|
-
if (isNodeOfType(attributeValue, "Literal")) return attributeValue.value;
|
|
19999
|
-
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && isNodeOfType(attributeValue.expression, "Literal")) return attributeValue.expression.value;
|
|
19610
|
+
} })
|
|
19611
|
+
});
|
|
19612
|
+
//#endregion
|
|
19613
|
+
//#region src/plugin/rules/nextjs/nextjs-no-a-element.ts
|
|
19614
|
+
const getJsxAttributeLiteralValue = (attributeValue) => {
|
|
19615
|
+
if (!attributeValue) return void 0;
|
|
19616
|
+
if (isNodeOfType(attributeValue, "Literal")) return attributeValue.value;
|
|
19617
|
+
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && isNodeOfType(attributeValue.expression, "Literal")) return attributeValue.expression.value;
|
|
19618
|
+
};
|
|
19619
|
+
const nextjsNoAElement = defineRule({
|
|
19620
|
+
id: "nextjs-no-a-element",
|
|
19621
|
+
title: "Plain anchor reloads internal Next.js links",
|
|
19622
|
+
tags: ["test-noise"],
|
|
19623
|
+
requires: ["nextjs"],
|
|
19624
|
+
severity: "warn",
|
|
19625
|
+
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
19626
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
19627
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
19628
|
+
const attributes = node.attributes ?? [];
|
|
19629
|
+
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
19630
|
+
if (downloadAttribute) {
|
|
19631
|
+
if (!downloadAttribute.value) return;
|
|
19632
|
+
const downloadValue = getJsxAttributeLiteralValue(downloadAttribute.value);
|
|
19633
|
+
if (downloadValue !== false && downloadValue !== null) return;
|
|
19634
|
+
}
|
|
19635
|
+
if (getJsxAttributeLiteralValue(findJsxAttribute(attributes, "target")?.value) === "_blank") return;
|
|
19636
|
+
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
19637
|
+
if (!hrefAttribute?.value) return;
|
|
19638
|
+
const hrefValue = getJsxAttributeLiteralValue(hrefAttribute.value);
|
|
19639
|
+
if (typeof hrefValue === "string" && hrefValue.startsWith("/") && !hrefValue.startsWith("//")) context.report({
|
|
19640
|
+
node,
|
|
19641
|
+
message: "Plain <a> reloads the whole page for internal links, so Next.js loses client-side navigation and prefetching."
|
|
19642
|
+
});
|
|
19643
|
+
} })
|
|
19644
|
+
});
|
|
19645
|
+
//#endregion
|
|
19646
|
+
//#region src/plugin/utils/collect-effect-invoked-functions.ts
|
|
19647
|
+
const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
|
|
19648
|
+
"then",
|
|
19649
|
+
"catch",
|
|
19650
|
+
"finally"
|
|
19651
|
+
]);
|
|
19652
|
+
const collectEffectInvokedFunctions = (effectCallback) => {
|
|
19653
|
+
const invokedFunctions = new Set([effectCallback]);
|
|
19654
|
+
const localFunctionBindings = /* @__PURE__ */ new Map();
|
|
19655
|
+
const calledBindingNames = /* @__PURE__ */ new Set();
|
|
19656
|
+
const pendingFunctions = [effectCallback];
|
|
19657
|
+
const enqueue = (candidate) => {
|
|
19658
|
+
const strippedCandidate = candidate ? stripParenExpression(candidate) : candidate;
|
|
19659
|
+
if (!isFunctionLike$1(strippedCandidate) || invokedFunctions.has(strippedCandidate)) return;
|
|
19660
|
+
invokedFunctions.add(strippedCandidate);
|
|
19661
|
+
pendingFunctions.push(strippedCandidate);
|
|
19662
|
+
};
|
|
19663
|
+
const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
|
|
19664
|
+
while (pendingFunctions.length > 0) {
|
|
19665
|
+
const currentFunction = pendingFunctions.pop();
|
|
19666
|
+
if (!currentFunction) break;
|
|
19667
|
+
walkAst(currentFunction, (child) => {
|
|
19668
|
+
if (child !== currentFunction && isFunctionLike$1(child)) {
|
|
19669
|
+
if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) localFunctionBindings.set(child.id.name, child);
|
|
19670
|
+
return false;
|
|
19671
|
+
}
|
|
19672
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
19673
|
+
const initializer = child.init ? stripParenExpression(child.init) : null;
|
|
19674
|
+
if (isFunctionLike$1(initializer)) localFunctionBindings.set(child.id.name, initializer);
|
|
19675
|
+
return;
|
|
19676
|
+
}
|
|
19677
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
19678
|
+
const callee = stripParenExpression(child.callee);
|
|
19679
|
+
if (isFunctionLike$1(callee)) {
|
|
19680
|
+
enqueue(callee);
|
|
19681
|
+
return;
|
|
19682
|
+
}
|
|
19683
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
19684
|
+
calledBindingNames.add(callee.name);
|
|
19685
|
+
return;
|
|
19686
|
+
}
|
|
19687
|
+
if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
19688
|
+
});
|
|
19689
|
+
for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
|
|
19690
|
+
}
|
|
19691
|
+
return invokedFunctions;
|
|
20000
19692
|
};
|
|
20001
|
-
const nextjsNoAElement = defineRule({
|
|
20002
|
-
id: "nextjs-no-a-element",
|
|
20003
|
-
title: "Plain anchor reloads internal Next.js links",
|
|
20004
|
-
tags: ["test-noise"],
|
|
20005
|
-
requires: ["nextjs"],
|
|
20006
|
-
severity: "warn",
|
|
20007
|
-
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20008
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
20009
|
-
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
20010
|
-
const attributes = node.attributes ?? [];
|
|
20011
|
-
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20012
|
-
if (downloadAttribute) {
|
|
20013
|
-
if (!downloadAttribute.value) return;
|
|
20014
|
-
const downloadValue = getJsxAttributeLiteralValue(downloadAttribute.value);
|
|
20015
|
-
if (downloadValue !== false && downloadValue !== null) return;
|
|
20016
|
-
}
|
|
20017
|
-
if (getJsxAttributeLiteralValue(findJsxAttribute(attributes, "target")?.value) === "_blank") return;
|
|
20018
|
-
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
20019
|
-
if (!hrefAttribute?.value) return;
|
|
20020
|
-
const hrefValue = getJsxAttributeLiteralValue(hrefAttribute.value);
|
|
20021
|
-
if (typeof hrefValue === "string" && hrefValue.startsWith("/") && !hrefValue.startsWith("//")) context.report({
|
|
20022
|
-
node,
|
|
20023
|
-
message: "Plain <a> reloads the whole page for internal links, so Next.js loses client-side navigation and prefetching."
|
|
20024
|
-
});
|
|
20025
|
-
} })
|
|
20026
|
-
});
|
|
20027
19693
|
//#endregion
|
|
20028
19694
|
//#region src/plugin/utils/contains-fetch-call.ts
|
|
20029
19695
|
const isFetchCall$1 = (node) => {
|
|
@@ -25151,7 +24817,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
25151
24817
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
25152
24818
|
return false;
|
|
25153
24819
|
};
|
|
25154
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
24820
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
|
|
25155
24821
|
"forEach",
|
|
25156
24822
|
"map",
|
|
25157
24823
|
"filter",
|
|
@@ -25172,7 +24838,7 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
25172
24838
|
if (parent.callee === functionNode) return true;
|
|
25173
24839
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
25174
24840
|
const callee = parent.callee;
|
|
25175
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
24841
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
|
|
25176
24842
|
};
|
|
25177
24843
|
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
25178
24844
|
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
@@ -27805,14 +27471,14 @@ const hasDocumentClassListMutation = (node) => {
|
|
|
27805
27471
|
//#endregion
|
|
27806
27472
|
//#region src/plugin/rules/state-and-effects/no-effect-event-handler.ts
|
|
27807
27473
|
const hasEventLikeNode = (node) => findTriggeredSideEffectCalleeName(node) !== null || hasDocumentClassListMutation(node);
|
|
27808
|
-
const unwrapChainExpression$
|
|
27474
|
+
const unwrapChainExpression$1 = (node) => {
|
|
27809
27475
|
if (!node) return null;
|
|
27810
27476
|
if (isNodeOfType(node, "ChainExpression")) return node.expression;
|
|
27811
27477
|
return node;
|
|
27812
27478
|
};
|
|
27813
27479
|
const collectGuardExpressions = (node, into) => {
|
|
27814
27480
|
if (!node) return;
|
|
27815
|
-
const unwrappedNode = unwrapChainExpression$
|
|
27481
|
+
const unwrappedNode = unwrapChainExpression$1(node);
|
|
27816
27482
|
if (!unwrappedNode) return;
|
|
27817
27483
|
const rootIdentifierName = getRootIdentifierName(unwrappedNode);
|
|
27818
27484
|
if (rootIdentifierName) {
|
|
@@ -27843,7 +27509,7 @@ const isReturnOnlyStatement = (node) => {
|
|
|
27843
27509
|
};
|
|
27844
27510
|
const hasEventLikeRemainingStatements = (statements) => statements.some((statement) => !isNodeOfType(statement, "ReturnStatement") && hasEventLikeNode(statement));
|
|
27845
27511
|
const doesGuardMatchDependency = (guardExpression, dependencyExpression) => {
|
|
27846
|
-
const unwrappedDependencyExpression = unwrapChainExpression$
|
|
27512
|
+
const unwrappedDependencyExpression = unwrapChainExpression$1(dependencyExpression);
|
|
27847
27513
|
if (!unwrappedDependencyExpression) return false;
|
|
27848
27514
|
if (areExpressionsStructurallyEqual(guardExpression.expression, unwrappedDependencyExpression)) return true;
|
|
27849
27515
|
return isNodeOfType(unwrappedDependencyExpression, "Identifier") && unwrappedDependencyExpression.name === guardExpression.rootIdentifierName;
|
|
@@ -29902,6 +29568,57 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
29902
29568
|
} })
|
|
29903
29569
|
});
|
|
29904
29570
|
//#endregion
|
|
29571
|
+
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
29572
|
+
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
29573
|
+
let ancestor = bindingIdentifier.parent;
|
|
29574
|
+
while (ancestor) {
|
|
29575
|
+
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
29576
|
+
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
29577
|
+
ancestor = ancestor.parent ?? null;
|
|
29578
|
+
}
|
|
29579
|
+
return null;
|
|
29580
|
+
};
|
|
29581
|
+
//#endregion
|
|
29582
|
+
//#region src/plugin/utils/executes-during-render.ts
|
|
29583
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
29584
|
+
"map",
|
|
29585
|
+
"filter",
|
|
29586
|
+
"forEach",
|
|
29587
|
+
"flatMap",
|
|
29588
|
+
"reduce",
|
|
29589
|
+
"reduceRight",
|
|
29590
|
+
"some",
|
|
29591
|
+
"every",
|
|
29592
|
+
"find",
|
|
29593
|
+
"findIndex",
|
|
29594
|
+
"findLast",
|
|
29595
|
+
"findLastIndex",
|
|
29596
|
+
"sort",
|
|
29597
|
+
"toSorted"
|
|
29598
|
+
]);
|
|
29599
|
+
const REACT_RENDER_PHASE_HOOK_NAMES = new Set(["useMemo", "useState"]);
|
|
29600
|
+
const isGlobalArrayFromMember = (node, scopes) => {
|
|
29601
|
+
const unwrappedNode = stripParenExpression(node);
|
|
29602
|
+
if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed || !isNodeOfType(unwrappedNode.object, "Identifier") || unwrappedNode.object.name !== "Array" || !scopes.isGlobalReference(unwrappedNode.object) || !isNodeOfType(unwrappedNode.property, "Identifier")) return false;
|
|
29603
|
+
return unwrappedNode.property.name === "from";
|
|
29604
|
+
};
|
|
29605
|
+
const isConstAliasOfGlobalArrayFrom = (node, scopes) => {
|
|
29606
|
+
const unwrappedNode = stripParenExpression(node);
|
|
29607
|
+
if (!isNodeOfType(unwrappedNode, "Identifier")) return false;
|
|
29608
|
+
const binding = findVariableInitializer(unwrappedNode, unwrappedNode.name);
|
|
29609
|
+
if (!binding?.initializer) return false;
|
|
29610
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
29611
|
+
return Boolean(declarator && scopes.symbolFor(unwrappedNode)?.declarationNode === declarator && declarator.init && declarator.parent && isNodeOfType(declarator.parent, "VariableDeclaration") && declarator.parent.kind === "const" && isGlobalArrayFromMember(declarator.init, scopes));
|
|
29612
|
+
};
|
|
29613
|
+
const executesDuringRender = (functionNode, scopes) => {
|
|
29614
|
+
const parent = functionNode.parent;
|
|
29615
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
29616
|
+
if (parent.callee === functionNode) return true;
|
|
29617
|
+
if ((scopes ? isReactApiCall(parent, REACT_RENDER_PHASE_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }) : isHookCall$2(parent, REACT_RENDER_PHASE_HOOK_NAMES)) && parent.arguments?.[0] === functionNode) return true;
|
|
29618
|
+
if (scopes && parent.arguments?.[1] === functionNode && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
|
|
29619
|
+
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
|
|
29620
|
+
};
|
|
29621
|
+
//#endregion
|
|
29905
29622
|
//#region src/plugin/utils/find-enclosing-jsx-opening-element.ts
|
|
29906
29623
|
const findEnclosingJsxOpeningElement = (node) => {
|
|
29907
29624
|
let cursor = node.parent;
|
|
@@ -29913,6 +29630,17 @@ const findEnclosingJsxOpeningElement = (node) => {
|
|
|
29913
29630
|
return null;
|
|
29914
29631
|
};
|
|
29915
29632
|
//#endregion
|
|
29633
|
+
//#region src/plugin/utils/find-render-phase-component-or-hook.ts
|
|
29634
|
+
const findRenderPhaseComponentOrHook = (node, scopes) => {
|
|
29635
|
+
let functionNode = findEnclosingFunction(node);
|
|
29636
|
+
while (functionNode) {
|
|
29637
|
+
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
29638
|
+
if (!executesDuringRender(functionNode, scopes)) return null;
|
|
29639
|
+
functionNode = findEnclosingFunction(functionNode);
|
|
29640
|
+
}
|
|
29641
|
+
return null;
|
|
29642
|
+
};
|
|
29643
|
+
//#endregion
|
|
29916
29644
|
//#region src/plugin/utils/has-client-render-evidence.ts
|
|
29917
29645
|
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
29918
29646
|
if (fileHasUseClientDirective) return true;
|
|
@@ -30021,6 +29749,9 @@ const isGatedByFalsyInitialState = (node, scopes) => {
|
|
|
30021
29749
|
return false;
|
|
30022
29750
|
};
|
|
30023
29751
|
//#endregion
|
|
29752
|
+
//#region src/plugin/utils/is-event-handler-attribute.ts
|
|
29753
|
+
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
|
|
29754
|
+
//#endregion
|
|
30024
29755
|
//#region src/plugin/rules/performance/no-hydration-branch-on-browser-global.ts
|
|
30025
29756
|
const evaluateEquality = (operator, left, right) => {
|
|
30026
29757
|
if (operator === "===" || operator === "==") return left === right;
|
|
@@ -32119,41 +31850,6 @@ const noMutableInDeps = defineRule({
|
|
|
32119
31850
|
}
|
|
32120
31851
|
});
|
|
32121
31852
|
//#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
31853
|
//#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
|
|
32158
31854
|
const LODASH_MUTATOR_NAMES = new Set([
|
|
32159
31855
|
"set",
|
|
@@ -33444,7 +33140,7 @@ const isUseRefIdentifier = (identifier) => {
|
|
|
33444
33140
|
};
|
|
33445
33141
|
const COMMAND_PROP_NAME_PATTERN = /^(fetch|load|refetch|dispatch|register|render)([A-Z_]|$)/;
|
|
33446
33142
|
const SETTER_NAMED_PROP_PATTERN = /^set[A-Z]/;
|
|
33447
|
-
const unwrapChainExpression
|
|
33143
|
+
const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
33448
33144
|
const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
|
|
33449
33145
|
"useCallback",
|
|
33450
33146
|
"useMemo",
|
|
@@ -33479,7 +33175,7 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
33479
33175
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
33480
33176
|
const node = def.node;
|
|
33481
33177
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
33482
|
-
const initializer = unwrapChainExpression
|
|
33178
|
+
const initializer = unwrapChainExpression(node.init);
|
|
33483
33179
|
const wrappedFunction = getWrapperHookWrappedFunction(initializer);
|
|
33484
33180
|
if (wrappedFunction) {
|
|
33485
33181
|
if (wrappedFunction.async) return false;
|
|
@@ -33492,7 +33188,7 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
33492
33188
|
const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
33493
33189
|
const node = def.node;
|
|
33494
33190
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
33495
|
-
return getWrapperHookWrappedFunction(unwrapChainExpression
|
|
33191
|
+
return getWrapperHookWrappedFunction(unwrapChainExpression(node.init)) !== null;
|
|
33496
33192
|
}));
|
|
33497
33193
|
const isHandlerBagArgument = (analysis, argument) => {
|
|
33498
33194
|
if (!isNodeOfType(argument, "ObjectExpression")) return false;
|
|
@@ -33514,7 +33210,7 @@ const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
|
|
|
33514
33210
|
const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
33515
33211
|
const node = def.node;
|
|
33516
33212
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
33517
|
-
const init = unwrapChainExpression
|
|
33213
|
+
const init = unwrapChainExpression(node.init);
|
|
33518
33214
|
if (!isNodeOfType(init, "CallExpression")) return false;
|
|
33519
33215
|
const callee = init.callee;
|
|
33520
33216
|
if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
|
|
@@ -33558,7 +33254,7 @@ const noPassDataToParent = defineRule({
|
|
|
33558
33254
|
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
33559
33255
|
if (isRefCall(analysis, ref)) continue;
|
|
33560
33256
|
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
33561
|
-
const calleeNode = unwrapChainExpression
|
|
33257
|
+
const calleeNode = unwrapChainExpression(callExpr.callee);
|
|
33562
33258
|
const identifier = ref.identifier;
|
|
33563
33259
|
if (calleeNode === identifier) {
|
|
33564
33260
|
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
@@ -40846,123 +40542,6 @@ const preferUseEffectEvent = defineRule({
|
|
|
40846
40542
|
}
|
|
40847
40543
|
});
|
|
40848
40544
|
//#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
40545
|
//#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
|
|
40967
40546
|
const findUseEffectsInComponent = (componentBody) => {
|
|
40968
40547
|
const effectCalls = [];
|