oxlint-plugin-react-doctor 0.7.9-dev.842a55f → 0.7.9-dev.8835214
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 +64 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13883,6 +13883,7 @@ const walkInsideStatementBlocks = (node, visitor) => {
|
|
|
13883
13883
|
};
|
|
13884
13884
|
//#endregion
|
|
13885
13885
|
//#region src/plugin/rules/state-and-effects/utils/is-subscribe-like-call-expression.ts
|
|
13886
|
+
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
13886
13887
|
const getSubscribeLikeMethodName = (node) => {
|
|
13887
13888
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
13888
13889
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
@@ -13893,6 +13894,11 @@ const isSubscribeLikeCallExpression = (node) => {
|
|
|
13893
13894
|
const methodName = getSubscribeLikeMethodName(node);
|
|
13894
13895
|
return methodName !== null && SUBSCRIPTION_METHOD_NAMES.has(methodName);
|
|
13895
13896
|
};
|
|
13897
|
+
const getSubscribeOrObserveMethodName = (node) => {
|
|
13898
|
+
const methodName = getSubscribeLikeMethodName(node);
|
|
13899
|
+
return methodName !== null && (SUBSCRIPTION_METHOD_NAMES.has(methodName) || methodName === "observe") ? methodName : null;
|
|
13900
|
+
};
|
|
13901
|
+
const isSubscribeOrObserveCallExpression = (node) => getSubscribeOrObserveMethodName(node) !== null;
|
|
13896
13902
|
const isCleanupReturningSubscribeLikeCallExpression = (node) => {
|
|
13897
13903
|
const methodName = getSubscribeLikeMethodName(node);
|
|
13898
13904
|
if (methodName === null || !CLEANUP_RETURNING_SUBSCRIPTION_METHOD_NAMES.has(methodName)) return false;
|
|
@@ -13945,7 +13951,6 @@ const isNodeReachableWithinFunction = (node, context) => {
|
|
|
13945
13951
|
};
|
|
13946
13952
|
//#endregion
|
|
13947
13953
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
13948
|
-
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
13949
13954
|
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
13950
13955
|
const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
|
|
13951
13956
|
const REACT_REF_EFFECT_ANALYSIS_CACHE = /* @__PURE__ */ new WeakMap();
|
|
@@ -13955,10 +13960,6 @@ const RESOURCE_NOUN_BY_KIND = {
|
|
|
13955
13960
|
socket: "connection"
|
|
13956
13961
|
};
|
|
13957
13962
|
const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
|
|
13958
|
-
const isSubscribeOrObserveCall = (node) => {
|
|
13959
|
-
if (isSubscribeLikeCallExpression(node)) return true;
|
|
13960
|
-
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
13961
|
-
};
|
|
13962
13963
|
const resolveExpressionKey = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
13963
13964
|
if (!expression) return null;
|
|
13964
13965
|
const unwrappedExpression = stripParenExpression(expression);
|
|
@@ -14060,12 +14061,13 @@ const findSubscribeLikeUsages = (callback, context) => {
|
|
|
14060
14061
|
});
|
|
14061
14062
|
return;
|
|
14062
14063
|
}
|
|
14063
|
-
|
|
14064
|
+
const subscribeOrObserveMethodName = getSubscribeOrObserveMethodName(child);
|
|
14065
|
+
if (subscribeOrObserveMethodName !== null) {
|
|
14064
14066
|
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
14065
14067
|
usages.push({
|
|
14066
14068
|
kind: "subscribe",
|
|
14067
14069
|
node: child,
|
|
14068
|
-
resourceName:
|
|
14070
|
+
resourceName: subscribeOrObserveMethodName,
|
|
14069
14071
|
handleKey: findAssignedResourceKey(child, context),
|
|
14070
14072
|
...registrationDetails
|
|
14071
14073
|
});
|
|
@@ -15683,7 +15685,7 @@ const findRetainedFunctionLeak = (retainedFunction, context, options) => {
|
|
|
15683
15685
|
return false;
|
|
15684
15686
|
}
|
|
15685
15687
|
}
|
|
15686
|
-
if (
|
|
15688
|
+
if (isSubscribeOrObserveCallExpression(child) && (!doesResourceResultEscape(child, allowReturnedResourceEscape, allowReturnedResourceEscape, context) || options?.requireCallableReturnedResource === true && !isCleanupReturningSubscribeLikeCallExpression(child))) {
|
|
15687
15689
|
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
15688
15690
|
const subscriptionUsage = {
|
|
15689
15691
|
kind: "subscribe",
|
|
@@ -32270,6 +32272,7 @@ const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFile
|
|
|
32270
32272
|
sourceReferences,
|
|
32271
32273
|
isDeferred: frame.isDeferred,
|
|
32272
32274
|
isRenderKnownCopy,
|
|
32275
|
+
isSynchronousRenderValue: !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue,
|
|
32273
32276
|
matchesStateInitializer: doesMatchStateInitializer,
|
|
32274
32277
|
resetsSourceState: false
|
|
32275
32278
|
});
|
|
@@ -32285,13 +32288,59 @@ const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFile
|
|
|
32285
32288
|
});
|
|
32286
32289
|
};
|
|
32287
32290
|
//#endregion
|
|
32291
|
+
//#region src/plugin/rules/state-and-effects/utils/has-deferred-or-external-effect-work.ts
|
|
32292
|
+
const DEFERRED_MEMBER_NAMES = new Set([
|
|
32293
|
+
"catch",
|
|
32294
|
+
"finally",
|
|
32295
|
+
"then"
|
|
32296
|
+
]);
|
|
32297
|
+
const hasDeferredOrExternalEffectWork = (analysis, effectNode, scopes) => {
|
|
32298
|
+
const effectFunction = getEffectFn(analysis, effectNode);
|
|
32299
|
+
if (!effectFunction) return false;
|
|
32300
|
+
if (containsFetchCall(effectFunction, { stopAtFunctionBoundary: true })) return true;
|
|
32301
|
+
const effectInvokedFunctions = collectEffectInvokedFunctions(effectFunction);
|
|
32302
|
+
let didFindDeferredOrExternalWork = false;
|
|
32303
|
+
walkAst(effectFunction, (child) => {
|
|
32304
|
+
if (didFindDeferredOrExternalWork) return false;
|
|
32305
|
+
if (child !== effectFunction && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
32306
|
+
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
32307
|
+
const assignmentTarget = child.left;
|
|
32308
|
+
if ((isNodeOfType(assignmentTarget, "MemberExpression") ? getStaticPropertyName(assignmentTarget) : null)?.startsWith("on") && isFunctionLike$1(child.right)) {
|
|
32309
|
+
didFindDeferredOrExternalWork = true;
|
|
32310
|
+
return false;
|
|
32311
|
+
}
|
|
32312
|
+
}
|
|
32313
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32314
|
+
if (isSubscribeOrObserveCallExpression(child)) {
|
|
32315
|
+
didFindDeferredOrExternalWork = true;
|
|
32316
|
+
return false;
|
|
32317
|
+
}
|
|
32318
|
+
const localFunction = resolveExactLocalFunction(child.callee, scopes);
|
|
32319
|
+
if (isFunctionLike$1(localFunction) && localFunction.async) {
|
|
32320
|
+
didFindDeferredOrExternalWork = true;
|
|
32321
|
+
return false;
|
|
32322
|
+
}
|
|
32323
|
+
const callee = child.callee;
|
|
32324
|
+
if (isNodeOfType(callee, "Identifier") && TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES.has(callee.name)) {
|
|
32325
|
+
didFindDeferredOrExternalWork = true;
|
|
32326
|
+
return false;
|
|
32327
|
+
}
|
|
32328
|
+
const memberName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : null;
|
|
32329
|
+
if (memberName && DEFERRED_MEMBER_NAMES.has(memberName)) {
|
|
32330
|
+
didFindDeferredOrExternalWork = true;
|
|
32331
|
+
return false;
|
|
32332
|
+
}
|
|
32333
|
+
});
|
|
32334
|
+
return didFindDeferredOrExternalWork;
|
|
32335
|
+
};
|
|
32336
|
+
//#endregion
|
|
32288
32337
|
//#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
|
|
32289
32338
|
const noAdjustStateOnPropChange = defineRule({
|
|
32290
32339
|
id: "no-adjust-state-on-prop-change",
|
|
32291
|
-
title: "State
|
|
32340
|
+
title: "State adjusted after a prop changes",
|
|
32292
32341
|
severity: "warn",
|
|
32293
32342
|
tags: ["test-noise"],
|
|
32294
|
-
recommendation: "
|
|
32343
|
+
recommendation: "Remove the adjustment effect by deriving values during render, resetting the component with a key, or updating related state in the event that changes the prop. Avoid tracking the previous prop in more state, which preserves the duplication. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
|
|
32295
32344
|
create: (context) => ({ CallExpression(node) {
|
|
32296
32345
|
if (!isReactApiCall(node, "useEffect", context.scopes, {
|
|
32297
32346
|
allowGlobalReactNamespace: true,
|
|
@@ -32304,8 +32353,11 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
32304
32353
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
32305
32354
|
if (!dependencyReferences) return;
|
|
32306
32355
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
32307
|
-
|
|
32308
|
-
|
|
32356
|
+
const facts = collectEffectStateWriteFacts(analysis, context, node, context.filename);
|
|
32357
|
+
if (hasCleanup(analysis, node) || hasDeferredOrExternalEffectWork(analysis, node, context.scopes) || facts.some((fact) => fact.isDeferred)) return;
|
|
32358
|
+
for (const fact of facts) {
|
|
32359
|
+
if (!fact.isSynchronousRenderValue || fact.resetsSourceState) continue;
|
|
32360
|
+
if (fact.sourceReferences.flatMap((reference) => getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) continue;
|
|
32309
32361
|
context.report({
|
|
32310
32362
|
node: fact.callExpression,
|
|
32311
32363
|
message: "This effect adjusts state after a prop changes, so users briefly see the stale value."
|