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.
Files changed (2) hide show
  1. package/dist/index.js +64 -12
  2. 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
- 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)) {
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: child.callee.property.name,
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 (isSubscribeOrObserveCall(child) && (!doesResourceResultEscape(child, allowReturnedResourceEscape, allowReturnedResourceEscape, context) || options?.requireCallableReturnedResource === true && !isCleanupReturningSubscribeLikeCallExpression(child))) {
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 synced to a prop inside an effect",
32340
+ title: "State adjusted after a prop changes",
32292
32341
  severity: "warn",
32293
32342
  tags: ["test-noise"],
32294
- recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
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
- for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
32308
- if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
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."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.9-dev.842a55f",
3
+ "version": "0.7.9-dev.8835214",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",