oxlint-plugin-react-doctor 0.8.1-dev.61bf03e → 0.8.1-dev.6fcd9c9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -100,7 +100,7 @@ interface RuleContext extends Omit<BaseRuleContext, "getFilename"> {
100
100
  //#region src/plugin/utils/capability.d.ts
101
101
  declare const FRAMEWORK_TOKENS: readonly ["nextjs", "vite", "cra", "remix", "gatsby", "expo", "react-native", "tanstack-start", "preact", "unknown"];
102
102
  type FrameworkToken = (typeof FRAMEWORK_TOKENS)[number];
103
- type Capability = FrameworkToken | "react" | "remotion" | "pure-preact" | "react-native" | "server-actions" | "ssr" | "client-only" | "nextjs:static-export" | "nextjs:15" | "tailwind" | "tailwind:3.4" | "zod" | "zod:4" | "mobx" | "mobx-react" | "mobx-react-lite" | "mobx-react-binding" | "mobx-state-tree" | "mobx-react-observer" | "typescript" | "react-compiler" | "tanstack-query" | "valtio" | "pre-es2023" | `react:${number}` | `preact:${number}` | `remotion:${number}` | `valtio:${number}` | `mobx:${number}`;
103
+ type Capability = FrameworkToken | "react" | "remotion" | "pure-preact" | "react-native" | "server-actions" | "ssr" | "client-only" | "nextjs:static-export" | "nextjs:15" | "tailwind" | "tailwind:3.4" | "zod" | "zod:4" | "mobx" | "mobx-react" | "mobx-react-lite" | "mobx-react-binding" | "mobx-state-tree" | "mobx-react-observer" | "zustand" | "typescript" | "react-compiler" | "tanstack-query" | "valtio" | "pre-es2023" | `react:${number}` | `preact:${number}` | `remotion:${number}` | `valtio:${number}` | `mobx:${number}` | `zustand:${number}`;
104
104
  type CapabilityQuery = (capability: Capability) => boolean;
105
105
  //#endregion
106
106
  //#region src/plugin/utils/file-scan.d.ts
@@ -9712,6 +9712,75 @@ declare const REACT_DOCTOR_RULES: readonly [{
9712
9712
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
9713
9713
  readonly create: (context: RuleContext) => RuleVisitors;
9714
9714
  };
9715
+ }, {
9716
+ readonly key: "react-doctor/zustand-no-fresh-selector-result";
9717
+ readonly id: "zustand-no-fresh-selector-result";
9718
+ readonly source: "react-doctor";
9719
+ readonly originallyExternal: false;
9720
+ readonly rule: {
9721
+ readonly framework: "global";
9722
+ readonly category: "Performance";
9723
+ readonly requires: readonly Capability[];
9724
+ readonly id: string;
9725
+ readonly title?: string;
9726
+ readonly severity: RuleSeverity;
9727
+ readonly disabledWhen?: ReadonlyArray<Capability>;
9728
+ readonly tags?: ReadonlyArray<string>;
9729
+ readonly matchByOccurrence?: boolean;
9730
+ readonly defaultEnabled?: boolean;
9731
+ readonly lifecycle?: "retired";
9732
+ readonly scan?: FileScan;
9733
+ readonly committedFilesOnly?: boolean;
9734
+ readonly recommendation?: string;
9735
+ readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
9736
+ readonly create: (context: RuleContext) => RuleVisitors;
9737
+ };
9738
+ }, {
9739
+ readonly key: "react-doctor/zustand-no-get-during-initialization";
9740
+ readonly id: "zustand-no-get-during-initialization";
9741
+ readonly source: "react-doctor";
9742
+ readonly originallyExternal: false;
9743
+ readonly rule: {
9744
+ readonly framework: "global";
9745
+ readonly category: "Bugs";
9746
+ readonly requires: readonly Capability[];
9747
+ readonly id: string;
9748
+ readonly title?: string;
9749
+ readonly severity: RuleSeverity;
9750
+ readonly disabledWhen?: ReadonlyArray<Capability>;
9751
+ readonly tags?: ReadonlyArray<string>;
9752
+ readonly matchByOccurrence?: boolean;
9753
+ readonly defaultEnabled?: boolean;
9754
+ readonly lifecycle?: "retired";
9755
+ readonly scan?: FileScan;
9756
+ readonly committedFilesOnly?: boolean;
9757
+ readonly recommendation?: string;
9758
+ readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
9759
+ readonly create: (context: RuleContext) => RuleVisitors;
9760
+ };
9761
+ }, {
9762
+ readonly key: "react-doctor/zustand-no-whole-store-destructure";
9763
+ readonly id: "zustand-no-whole-store-destructure";
9764
+ readonly source: "react-doctor";
9765
+ readonly originallyExternal: false;
9766
+ readonly rule: {
9767
+ readonly framework: "global";
9768
+ readonly category: "Bugs";
9769
+ readonly requires: readonly Capability[];
9770
+ readonly id: string;
9771
+ readonly title?: string;
9772
+ readonly severity: RuleSeverity;
9773
+ readonly disabledWhen?: ReadonlyArray<Capability>;
9774
+ readonly tags?: ReadonlyArray<string>;
9775
+ readonly matchByOccurrence?: boolean;
9776
+ readonly defaultEnabled?: boolean;
9777
+ readonly lifecycle?: "retired";
9778
+ readonly scan?: FileScan;
9779
+ readonly committedFilesOnly?: boolean;
9780
+ readonly recommendation?: string;
9781
+ readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
9782
+ readonly create: (context: RuleContext) => RuleVisitors;
9783
+ };
9715
9784
  }];
9716
9785
  declare const EXTERNAL_RULES: readonly [{
9717
9786
  readonly key: "react-hooks-js/set-state-in-render";
@@ -19323,6 +19392,75 @@ declare const RULES: readonly [{
19323
19392
  readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
19324
19393
  readonly create: (context: RuleContext) => RuleVisitors;
19325
19394
  };
19395
+ }, {
19396
+ readonly key: "react-doctor/zustand-no-fresh-selector-result";
19397
+ readonly id: "zustand-no-fresh-selector-result";
19398
+ readonly source: "react-doctor";
19399
+ readonly originallyExternal: false;
19400
+ readonly rule: {
19401
+ readonly framework: "global";
19402
+ readonly category: "Performance";
19403
+ readonly requires: readonly Capability[];
19404
+ readonly id: string;
19405
+ readonly title?: string;
19406
+ readonly severity: RuleSeverity;
19407
+ readonly disabledWhen?: ReadonlyArray<Capability>;
19408
+ readonly tags?: ReadonlyArray<string>;
19409
+ readonly matchByOccurrence?: boolean;
19410
+ readonly defaultEnabled?: boolean;
19411
+ readonly lifecycle?: "retired";
19412
+ readonly scan?: FileScan;
19413
+ readonly committedFilesOnly?: boolean;
19414
+ readonly recommendation?: string;
19415
+ readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
19416
+ readonly create: (context: RuleContext) => RuleVisitors;
19417
+ };
19418
+ }, {
19419
+ readonly key: "react-doctor/zustand-no-get-during-initialization";
19420
+ readonly id: "zustand-no-get-during-initialization";
19421
+ readonly source: "react-doctor";
19422
+ readonly originallyExternal: false;
19423
+ readonly rule: {
19424
+ readonly framework: "global";
19425
+ readonly category: "Bugs";
19426
+ readonly requires: readonly Capability[];
19427
+ readonly id: string;
19428
+ readonly title?: string;
19429
+ readonly severity: RuleSeverity;
19430
+ readonly disabledWhen?: ReadonlyArray<Capability>;
19431
+ readonly tags?: ReadonlyArray<string>;
19432
+ readonly matchByOccurrence?: boolean;
19433
+ readonly defaultEnabled?: boolean;
19434
+ readonly lifecycle?: "retired";
19435
+ readonly scan?: FileScan;
19436
+ readonly committedFilesOnly?: boolean;
19437
+ readonly recommendation?: string;
19438
+ readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
19439
+ readonly create: (context: RuleContext) => RuleVisitors;
19440
+ };
19441
+ }, {
19442
+ readonly key: "react-doctor/zustand-no-whole-store-destructure";
19443
+ readonly id: "zustand-no-whole-store-destructure";
19444
+ readonly source: "react-doctor";
19445
+ readonly originallyExternal: false;
19446
+ readonly rule: {
19447
+ readonly framework: "global";
19448
+ readonly category: "Bugs";
19449
+ readonly requires: readonly Capability[];
19450
+ readonly id: string;
19451
+ readonly title?: string;
19452
+ readonly severity: RuleSeverity;
19453
+ readonly disabledWhen?: ReadonlyArray<Capability>;
19454
+ readonly tags?: ReadonlyArray<string>;
19455
+ readonly matchByOccurrence?: boolean;
19456
+ readonly defaultEnabled?: boolean;
19457
+ readonly lifecycle?: "retired";
19458
+ readonly scan?: FileScan;
19459
+ readonly committedFilesOnly?: boolean;
19460
+ readonly recommendation?: string;
19461
+ readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
19462
+ readonly create: (context: RuleContext) => RuleVisitors;
19463
+ };
19326
19464
  }, {
19327
19465
  readonly key: "react-hooks-js/set-state-in-render";
19328
19466
  readonly source: "react-compiler";
package/dist/index.js CHANGED
@@ -1953,13 +1953,13 @@ const getImportDeclaration = (node) => {
1953
1953
  }
1954
1954
  return null;
1955
1955
  };
1956
- const getImportSource = (declaration) => typeof declaration.source.value === "string" ? declaration.source.value : null;
1956
+ const getImportSource$1 = (declaration) => typeof declaration.source.value === "string" ? declaration.source.value : null;
1957
1957
  const isNamedImport = (symbol, importedName, moduleSources) => {
1958
1958
  if (symbol.kind !== "import") return false;
1959
1959
  const declaration = symbol.declarationNode;
1960
1960
  if (!isNodeOfType(declaration, "ImportSpecifier")) return false;
1961
1961
  const importDeclaration = getImportDeclaration(declaration);
1962
- const source = importDeclaration ? getImportSource(importDeclaration) : null;
1962
+ const source = importDeclaration ? getImportSource$1(importDeclaration) : null;
1963
1963
  if (!source || !moduleSources.has(source)) return false;
1964
1964
  const imported = declaration.imported;
1965
1965
  return isNodeOfType(imported, "Identifier") && imported.name === importedName || isNodeOfType(imported, "Literal") && imported.value === importedName;
@@ -1968,7 +1968,7 @@ const isSatoriImport = (symbol) => {
1968
1968
  if (symbol.kind !== "import") return false;
1969
1969
  const declaration = symbol.declarationNode;
1970
1970
  const importDeclaration = getImportDeclaration(declaration);
1971
- if (!importDeclaration || getImportSource(importDeclaration) !== "satori") return false;
1971
+ if (!importDeclaration || getImportSource$1(importDeclaration) !== "satori") return false;
1972
1972
  if (isNodeOfType(declaration, "ImportDefaultSpecifier")) return true;
1973
1973
  if (!isNodeOfType(declaration, "ImportSpecifier")) return false;
1974
1974
  const imported = declaration.imported;
@@ -1989,7 +1989,7 @@ const isGeneratedImageRendererCall = (node, scopes) => {
1989
1989
  const declaration = symbol.declarationNode;
1990
1990
  if (!isNodeOfType(declaration, "ImportNamespaceSpecifier")) return false;
1991
1991
  const importDeclaration = getImportDeclaration(declaration);
1992
- const source = importDeclaration ? getImportSource(importDeclaration) : null;
1992
+ const source = importDeclaration ? getImportSource$1(importDeclaration) : null;
1993
1993
  return Boolean(source && IMAGE_RESPONSE_MODULES.has(source));
1994
1994
  };
1995
1995
  //#endregion
@@ -30225,17 +30225,20 @@ const catchClauseRethrowsCaught = (handler) => {
30225
30225
  return didRethrow;
30226
30226
  };
30227
30227
  //#endregion
30228
- //#region src/plugin/utils/is-immediately-invoked-function.ts
30229
- const isImmediatelyInvokedFunction = (functionNode) => {
30228
+ //#region src/plugin/utils/find-immediately-invoked-call-expression.ts
30229
+ const findImmediatelyInvokedCallExpression = (functionNode) => {
30230
30230
  let wrappedCallee = functionNode;
30231
30231
  let enclosing = functionNode.parent;
30232
30232
  while (enclosing && stripParenExpression(enclosing) === functionNode) {
30233
30233
  wrappedCallee = enclosing;
30234
30234
  enclosing = enclosing.parent ?? null;
30235
30235
  }
30236
- return Boolean(enclosing && isNodeOfType(enclosing, "CallExpression") && enclosing.callee === wrappedCallee);
30236
+ return enclosing && isNodeOfType(enclosing, "CallExpression") && enclosing.callee === wrappedCallee ? enclosing : null;
30237
30237
  };
30238
30238
  //#endregion
30239
+ //#region src/plugin/utils/is-immediately-invoked-function.ts
30240
+ const isImmediatelyInvokedFunction = (functionNode) => Boolean(findImmediatelyInvokedCallExpression(functionNode));
30241
+ //#endregion
30239
30242
  //#region src/plugin/utils/find-guarding-try-statement.ts
30240
30243
  const findGuardingTryStatement = (node) => {
30241
30244
  let child = node;
@@ -59108,7 +59111,7 @@ const inlineUseSelectorFunction = (callNode, aliases) => {
59108
59111
  };
59109
59112
  //#endregion
59110
59113
  //#region src/plugin/rules/state-and-effects/redux-useselector-inline-derivation.ts
59111
- const ALLOCATING_ARRAY_METHODS = new Set([
59114
+ const ALLOCATING_ARRAY_METHODS$1 = new Set([
59112
59115
  "filter",
59113
59116
  "map",
59114
59117
  "flatMap",
@@ -59144,7 +59147,7 @@ const getAllocatingCallSiteDescription = (expression) => {
59144
59147
  method: methodName
59145
59148
  };
59146
59149
  }
59147
- if (ALLOCATING_ARRAY_METHODS.has(methodName)) return {
59150
+ if (ALLOCATING_ARRAY_METHODS$1.has(methodName)) return {
59148
59151
  kind: "method",
59149
59152
  method: methodName
59150
59153
  };
@@ -62232,6 +62235,85 @@ const isSetterCalledDuringRender = (root, setterName) => {
62232
62235
  return found;
62233
62236
  };
62234
62237
  //#endregion
62238
+ //#region src/plugin/utils/is-descendant-without-function-boundary.ts
62239
+ const isDescendantWithoutFunctionBoundary = (descendant, ancestor) => {
62240
+ let current = descendant;
62241
+ while (current && current !== ancestor) {
62242
+ if (current !== descendant && isFunctionLike$1(current)) return false;
62243
+ current = current.parent;
62244
+ }
62245
+ return current === ancestor;
62246
+ };
62247
+ //#endregion
62248
+ //#region src/plugin/utils/can-execute-before-async-suspension.ts
62249
+ const collectSuspensionNodes = (functionNode, options) => {
62250
+ const suspensionNodes = new Set(options.suspensionNodes);
62251
+ walkAst(functionNode, (node) => {
62252
+ if (node !== functionNode && isFunctionLike$1(node)) return false;
62253
+ if (!options.suspensionNodes && isNodeOfType(node, "AwaitExpression") || isNodeOfType(node, "ForOfStatement") && node.await) suspensionNodes.add(node);
62254
+ });
62255
+ return suspensionNodes;
62256
+ };
62257
+ const isInsideForAwaitPostSuspensionRegion = (node, functionNode) => {
62258
+ let current = node;
62259
+ while (current && current !== functionNode) {
62260
+ const parent = current.parent;
62261
+ if (parent && isNodeOfType(parent, "ForOfStatement") && parent.await && (parent.left === current || parent.body === current)) return true;
62262
+ current = parent;
62263
+ }
62264
+ return false;
62265
+ };
62266
+ const suspensionBlockNode = (suspensionNode) => isNodeOfType(suspensionNode, "ForOfStatement") ? suspensionNode.right : suspensionNode;
62267
+ const doesSuspensionOccurBeforeNode = (suspensionNode, node) => {
62268
+ if (isNodeOfType(suspensionNode, "AwaitExpression")) {
62269
+ if (isDescendantWithoutFunctionBoundary(node, suspensionNode.argument)) return false;
62270
+ } else if (isNodeOfType(suspensionNode, "ForOfStatement") && isDescendantWithoutFunctionBoundary(node, suspensionNode.right)) return false;
62271
+ if (isDescendantWithoutFunctionBoundary(suspensionNode, node)) return true;
62272
+ const suspensionStart = getRangeStart(suspensionBlockNode(suspensionNode));
62273
+ const nodeStart = getRangeStart(node);
62274
+ return suspensionStart !== null && nodeStart !== null && suspensionStart < nodeStart;
62275
+ };
62276
+ const findExecutionNodeWithinFunction = (node, functionNode, context) => {
62277
+ let executionNode = node;
62278
+ let executionOwner = context.cfg.enclosingFunction(executionNode);
62279
+ while (executionOwner && executionOwner !== functionNode) {
62280
+ if (!isFunctionLike$1(executionOwner) || executionOwner.async || executionOwner.generator) return null;
62281
+ const callExpression = findImmediatelyInvokedCallExpression(executionOwner);
62282
+ if (!callExpression) return null;
62283
+ executionNode = callExpression;
62284
+ executionOwner = context.cfg.enclosingFunction(callExpression);
62285
+ }
62286
+ return executionOwner === functionNode ? executionNode : null;
62287
+ };
62288
+ const canExecuteBeforeAsyncSuspension = (node, functionNode, context, options = {}) => {
62289
+ if (!isFunctionLike$1(functionNode) || !functionNode.async) return isNodeReachableWithinFunction(node, context);
62290
+ const executionNode = findExecutionNodeWithinFunction(node, functionNode, context);
62291
+ if (!executionNode || isInsideForAwaitPostSuspensionRegion(executionNode, functionNode)) return false;
62292
+ const functionCfg = context.cfg.cfgFor(functionNode);
62293
+ const targetBlock = functionCfg?.blockOf(executionNode);
62294
+ if (!functionCfg || !targetBlock) return false;
62295
+ const suspensionsByBlock = /* @__PURE__ */ new Map();
62296
+ for (const suspensionNode of collectSuspensionNodes(functionNode, options)) {
62297
+ const suspensionBlock = functionCfg.blockOf(suspensionBlockNode(suspensionNode));
62298
+ if (!suspensionBlock) continue;
62299
+ const blockSuspensions = suspensionsByBlock.get(suspensionBlock) ?? [];
62300
+ blockSuspensions.push(suspensionNode);
62301
+ suspensionsByBlock.set(suspensionBlock, blockSuspensions);
62302
+ }
62303
+ const visitedBlocks = /* @__PURE__ */ new Set();
62304
+ const pendingBlocks = [functionCfg.entry];
62305
+ while (pendingBlocks.length > 0) {
62306
+ const block = pendingBlocks.pop();
62307
+ if (!block || visitedBlocks.has(block)) continue;
62308
+ visitedBlocks.add(block);
62309
+ const blockSuspensions = suspensionsByBlock.get(block) ?? [];
62310
+ if (block === targetBlock) return !blockSuspensions.some((suspensionNode) => doesSuspensionOccurBeforeNode(suspensionNode, executionNode));
62311
+ if (blockSuspensions.length > 0) continue;
62312
+ for (const edge of block.successors) if (edge.kind !== "throw") pendingBlocks.push(edge.to);
62313
+ }
62314
+ return false;
62315
+ };
62316
+ //#endregion
62235
62317
  //#region src/plugin/rules/state-and-effects/utils/create-external-location-invalidation-checker.ts
62236
62318
  const HISTORY_LOCATION_MUTATION_METHOD_NAMES = new Set(["pushState", "replaceState"]);
62237
62319
  const AGGREGATE_MUTATION_METHOD_NAMES = new Set([...MUTATING_ARRAY_METHODS, ...MUTATING_COLLECTION_METHODS]);
@@ -62424,14 +62506,6 @@ const buildLocationInvalidationIndex = (componentBody, componentFunction, contex
62424
62506
  });
62425
62507
  return index;
62426
62508
  };
62427
- const isDescendantWithoutFunctionBoundary = (descendant, ancestor) => {
62428
- let current = descendant;
62429
- while (current && current !== ancestor) {
62430
- if (current !== descendant && isFunctionLike$1(current)) return false;
62431
- current = current.parent;
62432
- }
62433
- return current === ancestor;
62434
- };
62435
62509
  const areInMutuallyExclusiveConditionalBranches = (firstNode, secondNode) => {
62436
62510
  const firstBranches = /* @__PURE__ */ new Map();
62437
62511
  let current = firstNode;
@@ -62743,39 +62817,7 @@ const canNodeReachNormalFunctionExit = (node, functionNode, index) => {
62743
62817
  }
62744
62818
  return false;
62745
62819
  };
62746
- const canExecuteBeforeAsyncSuspension = (node, functionNode, index) => {
62747
- if (!isFunctionLike$1(functionNode) || !functionNode.async) return isNodeReachableWithinFunction(node, index.context);
62748
- const functionCfg = index.context.cfg.cfgFor(functionNode);
62749
- const targetBlock = functionCfg?.blockOf(node);
62750
- if (!functionCfg || !targetBlock) return false;
62751
- const awaitsByBlock = /* @__PURE__ */ new Map();
62752
- for (const awaitExpression of index.awaitExpressionsByOwner.get(functionNode) ?? []) {
62753
- const awaitBlock = functionCfg.blockOf(awaitExpression);
62754
- if (!awaitBlock) continue;
62755
- const blockAwaits = awaitsByBlock.get(awaitBlock) ?? [];
62756
- blockAwaits.push(awaitExpression);
62757
- awaitsByBlock.set(awaitBlock, blockAwaits);
62758
- }
62759
- const visitedBlocks = /* @__PURE__ */ new Set();
62760
- const pendingBlocks = [functionCfg.entry];
62761
- const targetStart = getRangeStart(node);
62762
- while (pendingBlocks.length > 0) {
62763
- const block = pendingBlocks.pop();
62764
- if (!block || visitedBlocks.has(block)) continue;
62765
- visitedBlocks.add(block);
62766
- const blockAwaits = awaitsByBlock.get(block) ?? [];
62767
- if (block === targetBlock) return !blockAwaits.some((awaitExpression) => {
62768
- if (isNodeOfType(awaitExpression, "AwaitExpression") && isDescendantWithoutFunctionBoundary(node, awaitExpression.argument)) return false;
62769
- if (isDescendantWithoutFunctionBoundary(awaitExpression, node)) return true;
62770
- const awaitStart = getRangeStart(awaitExpression);
62771
- return awaitStart !== null && targetStart !== null && awaitStart < targetStart;
62772
- });
62773
- if (blockAwaits.length > 0) continue;
62774
- for (const edge of block.successors) if (edge.kind !== "throw") pendingBlocks.push(edge.to);
62775
- }
62776
- return false;
62777
- };
62778
- const canReactBatchMutationAfterExecution = (executionNode, mutationNode, owner, index) => (isExclusiveIntrinsicReactEventHandler(owner, index) || index.effectCallbacks.has(owner)) && canNodeReachNode(executionNode, mutationNode, index) && canExecuteBeforeAsyncSuspension(mutationNode, owner, index) && canNodeReachNormalFunctionExit(mutationNode, owner, index);
62820
+ const canReactBatchMutationAfterExecution = (executionNode, mutationNode, owner, index) => (isExclusiveIntrinsicReactEventHandler(owner, index) || index.effectCallbacks.has(owner)) && canNodeReachNode(executionNode, mutationNode, index) && canExecuteBeforeAsyncSuspension(mutationNode, owner, index.context, { suspensionNodes: index.awaitExpressionsByOwner.get(owner) }) && canNodeReachNormalFunctionExit(mutationNode, owner, index);
62779
62821
  const functionMaySynchronouslyMutateLocation = (functionNode, index, visitingFunctions, cycleAffectedFunctions) => {
62780
62822
  const cachedResult = index.synchronousMutationResultByFunction.get(functionNode);
62781
62823
  if (cachedResult !== void 0) return cachedResult;
@@ -62789,7 +62831,7 @@ const functionMaySynchronouslyMutateLocation = (functionNode, index, visitingFun
62789
62831
  return false;
62790
62832
  }
62791
62833
  visitingFunctions.add(functionNode);
62792
- const doesMutateSynchronously = [...collectLocationMutationExecutions(functionNode, index, visitingFunctions, cycleAffectedFunctions)].some((mutationExecution) => canExecuteBeforeAsyncSuspension(mutationExecution, functionNode, index) && canNodeReachNormalFunctionExit(mutationExecution, functionNode, index));
62834
+ const doesMutateSynchronously = [...collectLocationMutationExecutions(functionNode, index, visitingFunctions, cycleAffectedFunctions)].some((mutationExecution) => canExecuteBeforeAsyncSuspension(mutationExecution, functionNode, index.context, { suspensionNodes: index.awaitExpressionsByOwner.get(functionNode) }) && canNodeReachNormalFunctionExit(mutationExecution, functionNode, index));
62793
62835
  visitingFunctions.delete(functionNode);
62794
62836
  if (doesMutateSynchronously || !cycleAffectedFunctions.has(functionNode)) index.synchronousMutationResultByFunction.set(functionNode, doesMutateSynchronously);
62795
62837
  return doesMutateSynchronously;
@@ -62816,20 +62858,20 @@ const functionMustSynchronouslyRemoveLocationListener = (functionNode, registrat
62816
62858
  for (const removal of index.listenerRemovals) {
62817
62859
  if (!isDefinitelyMatchingLocationListenerRemoval(registration, removal)) continue;
62818
62860
  if (index.context.cfg.enclosingFunction(removal.callExpression) !== functionNode) continue;
62819
- if (canExecuteBeforeAsyncSuspension(removal.callExpression, functionNode, index)) removalExecutions.push(removal.callExpression);
62861
+ if (canExecuteBeforeAsyncSuspension(removal.callExpression, functionNode, index.context, { suspensionNodes: index.awaitExpressionsByOwner.get(functionNode) })) removalExecutions.push(removal.callExpression);
62820
62862
  }
62821
62863
  for (const expression of index.expressionsByOwner.get(functionNode) ?? []) {
62822
62864
  const calledFunction = index.calledFunctionByExpression.get(expression);
62823
- if (calledFunction && canExecuteBeforeAsyncSuspension(expression, functionNode, index) && functionMustSynchronouslyRemoveLocationListener(calledFunction, registration, index, nextVisitingFunctions)) removalExecutions.push(expression);
62865
+ if (calledFunction && canExecuteBeforeAsyncSuspension(expression, functionNode, index.context, { suspensionNodes: index.awaitExpressionsByOwner.get(functionNode) }) && functionMustSynchronouslyRemoveLocationListener(calledFunction, registration, index, nextVisitingFunctions)) removalExecutions.push(expression);
62824
62866
  }
62825
62867
  return doNodesCoverEveryPathFromFunctionEntry(functionNode, removalExecutions, index.context);
62826
62868
  };
62827
62869
  const collectSynchronousLocationListenerRemovalExecutions = (functionNode, registration, index) => {
62828
62870
  const removalExecutions = /* @__PURE__ */ new Set();
62829
- for (const removal of index.listenerRemovals) if (isDefinitelyMatchingLocationListenerRemoval(registration, removal) && index.context.cfg.enclosingFunction(removal.callExpression) === functionNode && canExecuteBeforeAsyncSuspension(removal.callExpression, functionNode, index)) removalExecutions.add(removal.callExpression);
62871
+ for (const removal of index.listenerRemovals) if (isDefinitelyMatchingLocationListenerRemoval(registration, removal) && index.context.cfg.enclosingFunction(removal.callExpression) === functionNode && canExecuteBeforeAsyncSuspension(removal.callExpression, functionNode, index.context, { suspensionNodes: index.awaitExpressionsByOwner.get(functionNode) })) removalExecutions.add(removal.callExpression);
62830
62872
  for (const expression of index.expressionsByOwner.get(functionNode) ?? []) {
62831
62873
  const calledFunction = index.calledFunctionByExpression.get(expression);
62832
- if (calledFunction && canExecuteBeforeAsyncSuspension(expression, functionNode, index) && functionMustSynchronouslyRemoveLocationListener(calledFunction, registration, index, /* @__PURE__ */ new Set())) removalExecutions.add(expression);
62874
+ if (calledFunction && canExecuteBeforeAsyncSuspension(expression, functionNode, index.context, { suspensionNodes: index.awaitExpressionsByOwner.get(functionNode) }) && functionMustSynchronouslyRemoveLocationListener(calledFunction, registration, index, /* @__PURE__ */ new Set())) removalExecutions.add(expression);
62833
62875
  }
62834
62876
  return removalExecutions;
62835
62877
  };
@@ -73652,6 +73694,516 @@ const zodV4PreferTopLevelStringFormats = defineRule({
73652
73694
  })
73653
73695
  });
73654
73696
  //#endregion
73697
+ //#region src/plugin/utils/function-returns-collection-at-path.ts
73698
+ const resolveImmutableInitializer = (expression, scopes, visitedSymbolIds) => {
73699
+ const candidate = stripParenExpression(expression);
73700
+ if (!isNodeOfType(candidate, "Identifier")) return candidate;
73701
+ const symbol = scopes.symbolFor(candidate);
73702
+ if (symbol?.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id) || symbol.references.some((reference) => reference.flag !== "read")) return candidate;
73703
+ visitedSymbolIds.add(symbol.id);
73704
+ return resolveImmutableInitializer(symbol.initializer, scopes, visitedSymbolIds);
73705
+ };
73706
+ const expressionAtPropertyPath = (expression, propertyPath, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
73707
+ const candidate = resolveImmutableInitializer(expression, scopes, visitedSymbolIds);
73708
+ const [propertyName, ...remainingPath] = propertyPath;
73709
+ if (!propertyName) return candidate;
73710
+ if (!isNodeOfType(candidate, "ObjectExpression")) return null;
73711
+ for (const property of candidate.properties) {
73712
+ if (!isNodeOfType(property, "Property")) continue;
73713
+ if (getStaticPropertyKeyName(property) !== propertyName) continue;
73714
+ return expressionAtPropertyPath(property.value, remainingPath, scopes, visitedSymbolIds);
73715
+ }
73716
+ return null;
73717
+ };
73718
+ const isGlobalConstructor = (expression, constructorNames, scopes) => {
73719
+ const candidate = stripParenExpression(expression);
73720
+ return isNodeOfType(candidate, "Identifier") && constructorNames.has(candidate.name) && scopes.isGlobalReference(candidate);
73721
+ };
73722
+ const isCollectionExpression = (expression, collectionKind, scopes) => {
73723
+ const candidate = stripParenExpression(expression);
73724
+ if (collectionKind === "array" && isNodeOfType(candidate, "ArrayExpression")) return true;
73725
+ if (!isNodeOfType(candidate, "NewExpression")) return false;
73726
+ return isGlobalConstructor(candidate.callee, collectionKind === "array" ? new Set(["Array"]) : new Set(["Map", "Set"]), scopes);
73727
+ };
73728
+ const functionReturnsCollectionAtPath = ({ collectionKind, functionNode, propertyPath, scopes }) => {
73729
+ if (!isFunctionLike$1(functionNode)) return false;
73730
+ const returnedExpressions = isNodeOfType(functionNode.body, "BlockStatement") ? collectFunctionReturnStatements(functionNode).flatMap((returnStatement) => returnStatement.argument ? [returnStatement.argument] : []) : [functionNode.body];
73731
+ return returnedExpressions.length > 0 && returnedExpressions.every((returnedExpression) => {
73732
+ const collectionExpression = expressionAtPropertyPath(returnedExpression, propertyPath, scopes);
73733
+ return Boolean(collectionExpression && isCollectionExpression(collectionExpression, collectionKind, scopes));
73734
+ });
73735
+ };
73736
+ //#endregion
73737
+ //#region src/plugin/utils/resolve-zustand-api.ts
73738
+ const ZUSTAND_APIS_BY_MODULE = new Map([
73739
+ ["zustand", new Set([
73740
+ "create",
73741
+ "createStore",
73742
+ "useStore"
73743
+ ])],
73744
+ ["zustand/vanilla", new Set(["createStore"])],
73745
+ ["zustand/react", new Set(["useStore"])],
73746
+ ["zustand/traditional", new Set(["createWithEqualityFn", "useStoreWithEqualityFn"])],
73747
+ ["zustand/shallow", new Set(["shallow", "useShallow"])],
73748
+ ["zustand/react/shallow", new Set(["useShallow"])],
73749
+ ["zustand/middleware", new Set([
73750
+ "combine",
73751
+ "createJSONStorage",
73752
+ "devtools",
73753
+ "persist",
73754
+ "redux",
73755
+ "subscribeWithSelector"
73756
+ ])],
73757
+ ["zustand/middleware/immer", new Set(["immer"])]
73758
+ ]);
73759
+ const STATE_CREATOR_MIDDLEWARE_ARGUMENT_INDEX = new Map([
73760
+ ["combine", 1],
73761
+ ["devtools", 0],
73762
+ ["immer", 0],
73763
+ ["persist", 0],
73764
+ ["subscribeWithSelector", 0]
73765
+ ]);
73766
+ const toZustandApiName = (value) => {
73767
+ switch (value) {
73768
+ case "combine":
73769
+ case "create":
73770
+ case "createJSONStorage":
73771
+ case "createStore":
73772
+ case "createWithEqualityFn":
73773
+ case "devtools":
73774
+ case "immer":
73775
+ case "persist":
73776
+ case "redux":
73777
+ case "shallow":
73778
+ case "subscribeWithSelector":
73779
+ case "useShallow":
73780
+ case "useStore":
73781
+ case "useStoreWithEqualityFn": return value;
73782
+ default: return null;
73783
+ }
73784
+ };
73785
+ const getImportSource = (symbol) => {
73786
+ const declaration = getImportDeclarationForSymbol(symbol);
73787
+ if (!declaration || isTypeOnlyImport(declaration) || isNodeOfType(symbol.declarationNode, "ImportSpecifier") && symbol.declarationNode.importKind === "type") return null;
73788
+ return typeof declaration.source.value === "string" ? declaration.source.value : null;
73789
+ };
73790
+ const bindingFromImportSymbol = (symbol) => {
73791
+ const moduleSource = getImportSource(symbol);
73792
+ const supportedApiNames = moduleSource ? ZUSTAND_APIS_BY_MODULE.get(moduleSource) : null;
73793
+ if (!moduleSource || !supportedApiNames) return null;
73794
+ if (moduleSource === "zustand" && isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier")) return {
73795
+ apiName: "create",
73796
+ moduleSource
73797
+ };
73798
+ const importedName = getImportedName(symbol.declarationNode);
73799
+ const apiName = importedName ? toZustandApiName(importedName) : null;
73800
+ return apiName && supportedApiNames.has(apiName) ? {
73801
+ apiName,
73802
+ moduleSource
73803
+ } : null;
73804
+ };
73805
+ const namespaceImportSource = (expression, scopes) => {
73806
+ if (!isNodeOfType(expression, "Identifier")) return null;
73807
+ const symbol = resolveConstIdentifierAlias(expression, scopes);
73808
+ if (!symbol || !isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")) return null;
73809
+ return getImportSource(symbol);
73810
+ };
73811
+ const resolveZustandApiBinding = (expression, scopes) => {
73812
+ const candidate = stripParenExpression(expression);
73813
+ if (isNodeOfType(candidate, "Identifier")) {
73814
+ const symbol = resolveConstIdentifierAlias(candidate, scopes);
73815
+ if (!symbol) return null;
73816
+ if (symbol.kind === "import") return bindingFromImportSymbol(symbol);
73817
+ if (symbol.kind !== "const" || !symbol.initializer) return null;
73818
+ return resolveZustandApiBinding(symbol.initializer, scopes);
73819
+ }
73820
+ if (!isNodeOfType(candidate, "MemberExpression")) return null;
73821
+ const propertyName = getStaticPropertyName(candidate);
73822
+ const moduleSource = namespaceImportSource(stripParenExpression(candidate.object), scopes);
73823
+ const supportedApiNames = moduleSource ? ZUSTAND_APIS_BY_MODULE.get(moduleSource) : null;
73824
+ const apiName = propertyName ? toZustandApiName(propertyName) : null;
73825
+ return apiName && moduleSource && supportedApiNames?.has(apiName) ? {
73826
+ apiName,
73827
+ moduleSource
73828
+ } : null;
73829
+ };
73830
+ const resolveStateCreator = (expression, scopes, middlewareNames, visitedExpressions = /* @__PURE__ */ new Set()) => {
73831
+ const candidate = stripParenExpression(expression);
73832
+ if (visitedExpressions.has(candidate)) return null;
73833
+ visitedExpressions.add(candidate);
73834
+ const creatorFunction = resolveExactLocalFunction(expression, scopes);
73835
+ if (isFunctionLike$1(creatorFunction)) return creatorFunction;
73836
+ if (!isNodeOfType(candidate, "CallExpression")) return null;
73837
+ const middleware = resolveZustandApiBinding(candidate.callee, scopes);
73838
+ const creatorArgumentIndex = middleware ? STATE_CREATOR_MIDDLEWARE_ARGUMENT_INDEX.get(middleware.apiName) : null;
73839
+ if (!middleware || creatorArgumentIndex === void 0 || creatorArgumentIndex === null) return null;
73840
+ const creatorArgument = candidate.arguments[creatorArgumentIndex];
73841
+ if (!creatorArgument || isNodeOfType(creatorArgument, "SpreadElement")) return null;
73842
+ middlewareNames.add(middleware.apiName);
73843
+ return resolveStateCreator(creatorArgument, scopes, middlewareNames, visitedExpressions);
73844
+ };
73845
+ const isStoreFactoryApi = (binding) => binding?.apiName === "create" || binding?.apiName === "createStore" || binding?.apiName === "createWithEqualityFn";
73846
+ const resolveCurriedStoreFactoryBinding = (expression, scopes) => {
73847
+ const candidate = stripParenExpression(expression);
73848
+ if (isNodeOfType(candidate, "Identifier")) {
73849
+ const symbol = resolveConstIdentifierAlias(candidate, scopes);
73850
+ if (symbol?.kind !== "const" || !symbol.initializer) return null;
73851
+ return resolveCurriedStoreFactoryBinding(symbol.initializer, scopes);
73852
+ }
73853
+ if (!isNodeOfType(candidate, "CallExpression") || candidate.arguments.length > 0) return null;
73854
+ const factoryBinding = resolveZustandApiBinding(candidate.callee, scopes);
73855
+ return isStoreFactoryApi(factoryBinding) ? factoryBinding : null;
73856
+ };
73857
+ const resolveZustandStoreCreator = (callExpression, scopes) => {
73858
+ const factoryCall = resolveZustandStoreFactoryCall(callExpression, scopes);
73859
+ if (!factoryCall) return null;
73860
+ const middlewareNames = /* @__PURE__ */ new Set();
73861
+ const creatorFunction = resolveStateCreator(factoryCall.creatorArgument, scopes, middlewareNames);
73862
+ if (!creatorFunction) return null;
73863
+ return {
73864
+ creatorFunction,
73865
+ factoryApiName: factoryCall.factoryApiName,
73866
+ middlewareNames
73867
+ };
73868
+ };
73869
+ const resolveZustandStoreFactoryCall = (callExpression, scopes) => {
73870
+ let factoryBinding = resolveZustandApiBinding(callExpression.callee, scopes);
73871
+ if (!isStoreFactoryApi(factoryBinding)) {
73872
+ factoryBinding = resolveCurriedStoreFactoryBinding(callExpression.callee, scopes);
73873
+ if (!isStoreFactoryApi(factoryBinding)) return null;
73874
+ }
73875
+ const creatorArgument = callExpression.arguments[0];
73876
+ if (!creatorArgument || isNodeOfType(creatorArgument, "SpreadElement")) return null;
73877
+ return {
73878
+ callExpression,
73879
+ creatorArgument,
73880
+ factoryApiName: factoryBinding.apiName
73881
+ };
73882
+ };
73883
+ //#endregion
73884
+ //#region src/plugin/rules/state-and-effects/zustand-no-fresh-selector-result.ts
73885
+ const ALLOCATING_ARRAY_METHODS = new Set([
73886
+ "filter",
73887
+ "flat",
73888
+ "flatMap",
73889
+ "map",
73890
+ "toReversed",
73891
+ "toSorted",
73892
+ "toSpliced",
73893
+ "with"
73894
+ ]);
73895
+ const SAME_REFERENCE_ARRAY_METHODS = new Set(["reverse", "sort"]);
73896
+ const ALLOCATING_NAMESPACE_METHOD_KINDS = new Map([["Array", new Map([["from", "array"], ["of", "array"]])], ["Object", new Map([
73897
+ ["create", "object"],
73898
+ ["entries", "array"],
73899
+ ["fromEntries", "object"],
73900
+ ["keys", "array"],
73901
+ ["values", "array"]
73902
+ ])]]);
73903
+ const isNullishEqualityArgument = (argument, scopes) => {
73904
+ const candidate = stripParenExpression(argument);
73905
+ return isNodeOfType(candidate, "Identifier") && candidate.name === "undefined" && scopes.isGlobalReference(candidate) || isNodeOfType(candidate, "Literal") && candidate.value === null || isNodeOfType(candidate, "UnaryExpression") && candidate.operator === "void";
73906
+ };
73907
+ const hasExplicitEqualityArgument = (argumentsList, equalityArgumentIndex, scopes) => {
73908
+ const equalityArgument = argumentsList[equalityArgumentIndex];
73909
+ if (!equalityArgument) return false;
73910
+ if (isNodeOfType(equalityArgument, "SpreadElement")) return true;
73911
+ return !isNullishEqualityArgument(equalityArgument, scopes);
73912
+ };
73913
+ const resolveZustandStoreCreation = (expression, scopes) => {
73914
+ const candidate = stripParenExpression(expression);
73915
+ if (!isNodeOfType(candidate, "CallExpression")) return null;
73916
+ const factoryCall = resolveZustandStoreFactoryCall(candidate, scopes);
73917
+ if (!factoryCall || factoryCall.factoryApiName !== "create" && factoryCall.factoryApiName !== "createWithEqualityFn") return null;
73918
+ return {
73919
+ creatorFunction: resolveZustandStoreCreator(candidate, scopes)?.creatorFunction ?? null,
73920
+ hasDefaultEquality: factoryCall.factoryApiName === "createWithEqualityFn" && hasExplicitEqualityArgument(candidate.arguments, 1, scopes),
73921
+ supportsEqualityArgument: factoryCall.factoryApiName === "createWithEqualityFn"
73922
+ };
73923
+ };
73924
+ const resolveZustandBoundStore = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
73925
+ const candidate = stripParenExpression(expression);
73926
+ if (!isNodeOfType(candidate, "Identifier")) return null;
73927
+ const symbol = scopes.symbolFor(candidate);
73928
+ if (symbol?.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id) || symbol.references.some((reference) => reference.flag !== "read")) return null;
73929
+ visitedSymbolIds.add(symbol.id);
73930
+ return resolveZustandStoreCreation(symbol.initializer, scopes) ?? resolveZustandBoundStore(symbol.initializer, scopes, visitedSymbolIds);
73931
+ };
73932
+ const getZustandSelectorCall = (callExpression, scopes) => {
73933
+ const apiBinding = resolveZustandApiBinding(callExpression.callee, scopes);
73934
+ if (apiBinding?.apiName === "useStore" || apiBinding?.apiName === "useStoreWithEqualityFn") {
73935
+ const selector = callExpression.arguments[1];
73936
+ if (!selector || isNodeOfType(selector, "SpreadElement")) return null;
73937
+ if (apiBinding.apiName === "useStoreWithEqualityFn" && hasExplicitEqualityArgument(callExpression.arguments, 2, scopes)) return null;
73938
+ return {
73939
+ selector,
73940
+ storeCreatorFunction: null
73941
+ };
73942
+ }
73943
+ const boundStore = resolveZustandBoundStore(callExpression.callee, scopes);
73944
+ const selector = callExpression.arguments[0];
73945
+ if (!boundStore || !selector || isNodeOfType(selector, "SpreadElement")) return null;
73946
+ if (boundStore.hasDefaultEquality || boundStore.supportsEqualityArgument && hasExplicitEqualityArgument(callExpression.arguments, 1, scopes)) return null;
73947
+ return {
73948
+ selector,
73949
+ storeCreatorFunction: boundStore.creatorFunction
73950
+ };
73951
+ };
73952
+ const isUseShallowCall = (callExpression, scopes) => resolveZustandApiBinding(callExpression.callee, scopes)?.apiName === "useShallow";
73953
+ const resolveSelectorFunction = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
73954
+ const candidate = stripParenExpression(expression);
73955
+ if (isFunctionLike$1(candidate)) return candidate;
73956
+ if (isNodeOfType(candidate, "CallExpression")) {
73957
+ if (isUseShallowCall(candidate, scopes)) return null;
73958
+ if (!isReactApiCall(candidate, "useCallback", scopes, {
73959
+ allowGlobalReactNamespace: true,
73960
+ resolveNamedAliases: true
73961
+ })) return null;
73962
+ const callback = candidate.arguments[0];
73963
+ if (!callback || isNodeOfType(callback, "SpreadElement")) return null;
73964
+ return resolveSelectorFunction(callback, scopes, visitedSymbolIds);
73965
+ }
73966
+ if (!isNodeOfType(candidate, "Identifier")) return null;
73967
+ const symbol = scopes.symbolFor(candidate);
73968
+ if (!symbol || visitedSymbolIds.has(symbol.id) || symbol.references.some((reference) => reference.flag !== "read")) return null;
73969
+ if (symbol.kind === "function" && isFunctionLike$1(symbol.declarationNode)) return symbol.declarationNode;
73970
+ if (symbol.kind !== "const" || !symbol.initializer) return null;
73971
+ visitedSymbolIds.add(symbol.id);
73972
+ return resolveSelectorFunction(symbol.initializer, scopes, visitedSymbolIds);
73973
+ };
73974
+ const selectorStatePropertyPath = (expression, analysis, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
73975
+ const candidate = stripParenExpression(expression);
73976
+ if (isNodeOfType(candidate, "Identifier")) {
73977
+ const symbol = analysis.scopes.symbolFor(candidate);
73978
+ const selectorParameter = isFunctionLike$1(analysis.selectorFunction) ? analysis.selectorFunction.params[0] : null;
73979
+ if (selectorParameter && isNodeOfType(selectorParameter, "Identifier") && symbol?.id === analysis.scopes.symbolFor(selectorParameter)?.id) return [];
73980
+ if (symbol?.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id) || symbol.references.some((reference) => reference.flag !== "read")) return null;
73981
+ visitedSymbolIds.add(symbol.id);
73982
+ return selectorStatePropertyPath(symbol.initializer, analysis, visitedSymbolIds);
73983
+ }
73984
+ if (!isNodeOfType(candidate, "MemberExpression")) return null;
73985
+ const propertyName = getStaticPropertyName(candidate);
73986
+ const objectPath = selectorStatePropertyPath(candidate.object, analysis, visitedSymbolIds);
73987
+ return propertyName && objectPath ? [...objectPath, propertyName] : null;
73988
+ };
73989
+ const freshResultFromAllocatingCall = (expression, analysis, visitedSymbolIds) => {
73990
+ const callee = stripParenExpression(expression.callee);
73991
+ if (!isNodeOfType(callee, "MemberExpression")) return null;
73992
+ const methodName = getStaticPropertyName(callee);
73993
+ if (!methodName) return null;
73994
+ const receiver = stripParenExpression(callee.object);
73995
+ if (isNodeOfType(receiver, "Identifier") && analysis.scopes.isGlobalReference(receiver)) {
73996
+ const resultKind = ALLOCATING_NAMESPACE_METHOD_KINDS.get(receiver.name)?.get(methodName);
73997
+ if (resultKind) return {
73998
+ kind: resultKind,
73999
+ node: expression
74000
+ };
74001
+ if (receiver.name === "Object" && methodName === "assign") {
74002
+ const target = expression.arguments[0];
74003
+ if (!target || isNodeOfType(target, "SpreadElement")) return null;
74004
+ const freshTarget = resolveFreshSelectorResult(target, analysis, new Set(visitedSymbolIds));
74005
+ return freshTarget ? {
74006
+ kind: freshTarget.kind,
74007
+ node: expression
74008
+ } : null;
74009
+ }
74010
+ }
74011
+ if (ALLOCATING_ARRAY_METHODS.has(methodName)) {
74012
+ if (resolveFreshSelectorResult(receiver, analysis, new Set(visitedSymbolIds))?.kind === "array") return {
74013
+ kind: "array",
74014
+ node: expression
74015
+ };
74016
+ const receiverPath = selectorStatePropertyPath(receiver, analysis);
74017
+ if (receiverPath && analysis.storeCreatorFunction && functionReturnsCollectionAtPath({
74018
+ collectionKind: "array",
74019
+ functionNode: analysis.storeCreatorFunction,
74020
+ propertyPath: receiverPath,
74021
+ scopes: analysis.scopes
74022
+ })) return {
74023
+ kind: "array",
74024
+ node: expression
74025
+ };
74026
+ return null;
74027
+ }
74028
+ if (!SAME_REFERENCE_ARRAY_METHODS.has(methodName)) return null;
74029
+ return resolveFreshSelectorResult(receiver, analysis, new Set(visitedSymbolIds)) ? {
74030
+ kind: "array",
74031
+ node: expression
74032
+ } : null;
74033
+ };
74034
+ const resolveFreshSelectorResult = (expression, analysis, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
74035
+ const freshRenderValue = resolveFreshRenderValue(expression, analysis.scopes);
74036
+ if (freshRenderValue?.kind === "array" || freshRenderValue?.kind === "function" || freshRenderValue?.kind === "instance" || freshRenderValue?.kind === "object") return {
74037
+ kind: freshRenderValue.kind,
74038
+ node: expression
74039
+ };
74040
+ const candidate = stripParenExpression(expression);
74041
+ if (isNodeOfType(candidate, "CallExpression")) return freshResultFromAllocatingCall(candidate, analysis, visitedSymbolIds);
74042
+ if (isNodeOfType(candidate, "ConditionalExpression")) return resolveFreshSelectorResult(candidate.consequent, analysis, new Set(visitedSymbolIds)) ?? resolveFreshSelectorResult(candidate.alternate, analysis, new Set(visitedSymbolIds));
74043
+ if (isNodeOfType(candidate, "LogicalExpression")) {
74044
+ if (candidate.operator === "&&") return resolveFreshSelectorResult(candidate.right, analysis, visitedSymbolIds);
74045
+ return resolveFreshSelectorResult(candidate.left, analysis, new Set(visitedSymbolIds)) ?? resolveFreshSelectorResult(candidate.right, analysis, new Set(visitedSymbolIds));
74046
+ }
74047
+ if (isNodeOfType(candidate, "SequenceExpression")) {
74048
+ const returnedExpression = candidate.expressions[candidate.expressions.length - 1];
74049
+ return returnedExpression ? resolveFreshSelectorResult(returnedExpression, analysis, visitedSymbolIds) : null;
74050
+ }
74051
+ if (!isNodeOfType(candidate, "Identifier")) return null;
74052
+ const symbol = analysis.scopes.symbolFor(candidate);
74053
+ if (symbol?.kind !== "const" || symbol.scope.kind === "module" || !symbol.initializer || visitedSymbolIds.has(symbol.id) || symbol.references.some((reference) => reference.flag !== "read")) return null;
74054
+ visitedSymbolIds.add(symbol.id);
74055
+ return resolveFreshSelectorResult(symbol.initializer, analysis, visitedSymbolIds);
74056
+ };
74057
+ const findFreshSelectorReturn = (selectorFunction, scopes, storeCreatorFunction) => {
74058
+ if (!isFunctionLike$1(selectorFunction) || !selectorFunction.body) return null;
74059
+ const analysis = {
74060
+ scopes,
74061
+ selectorFunction,
74062
+ storeCreatorFunction
74063
+ };
74064
+ if (!isNodeOfType(selectorFunction.body, "BlockStatement")) return resolveFreshSelectorResult(selectorFunction.body, analysis);
74065
+ let freshResult = null;
74066
+ walkAst(selectorFunction.body, (candidate) => {
74067
+ if (freshResult) return false;
74068
+ if (candidate !== selectorFunction.body && isFunctionLike$1(candidate)) return false;
74069
+ if (!isNodeOfType(candidate, "ReturnStatement") || !candidate.argument) return;
74070
+ freshResult = resolveFreshSelectorResult(candidate.argument, analysis);
74071
+ return freshResult ? false : void 0;
74072
+ });
74073
+ return freshResult;
74074
+ };
74075
+ const zustandNoFreshSelectorResult = defineRule({
74076
+ id: "zustand-no-fresh-selector-result",
74077
+ title: "Zustand selector returns a fresh value",
74078
+ severity: "error",
74079
+ category: "Performance",
74080
+ requires: ["zustand", "zustand:5"],
74081
+ recommendation: "Select a stable store field, split the selector, or wrap a collection selector with `useShallow`.",
74082
+ create: (context) => ({ CallExpression(node) {
74083
+ const selectorCall = getZustandSelectorCall(node, context.scopes);
74084
+ if (!selectorCall) return;
74085
+ const selectorFunction = resolveSelectorFunction(selectorCall.selector, context.scopes);
74086
+ if (!selectorFunction) return;
74087
+ const freshResult = findFreshSelectorReturn(selectorFunction, context.scopes, selectorCall.storeCreatorFunction);
74088
+ if (!freshResult) return;
74089
+ context.report({
74090
+ node: freshResult.node,
74091
+ message: "This Zustand selector creates a new reference whenever the store is read, so Object.is never sees a stable snapshot and Zustand v5 can repeatedly render or hit maximum update depth. Select a stable field or use `useShallow`."
74092
+ });
74093
+ } })
74094
+ });
74095
+ //#endregion
74096
+ //#region src/plugin/rules/state-and-effects/zustand-no-get-during-initialization.ts
74097
+ const findGetParameterIdentifier = (parameter) => {
74098
+ if (!parameter) return null;
74099
+ if (isNodeOfType(parameter, "Identifier")) return parameter;
74100
+ if (isNodeOfType(parameter, "AssignmentPattern") && isNodeOfType(parameter.left, "Identifier")) return parameter.left;
74101
+ return null;
74102
+ };
74103
+ const isGetParameterCall = (callExpression, getParameterSymbol, context) => {
74104
+ const callee = stripParenExpression(callExpression.callee);
74105
+ if (!isNodeOfType(callee, "Identifier")) return false;
74106
+ return resolveConstIdentifierAlias(callee, context.scopes)?.id === getParameterSymbol.id;
74107
+ };
74108
+ const reportEagerGetCalls = (creatorFunction, getParameterSymbol, context) => {
74109
+ const visitedFunctions = /* @__PURE__ */ new Set();
74110
+ const reportFunctionReads = (functionNode) => {
74111
+ if (!isFunctionLike$1(functionNode) || functionNode.generator || visitedFunctions.has(functionNode)) return;
74112
+ visitedFunctions.add(functionNode);
74113
+ walkAst(functionNode, (node) => {
74114
+ if (node !== functionNode && isFunctionLike$1(node)) return false;
74115
+ if (!isNodeOfType(node, "CallExpression")) return;
74116
+ if (isGetParameterCall(node, getParameterSymbol, context)) {
74117
+ if (!canExecuteBeforeAsyncSuspension(node, functionNode, context)) return;
74118
+ context.report({
74119
+ node,
74120
+ message: "`get()` runs before Zustand installs the initial state, so it returns `undefined` here."
74121
+ });
74122
+ return;
74123
+ }
74124
+ if (!canExecuteBeforeAsyncSuspension(node, functionNode, context)) return;
74125
+ const calledFunction = resolveExactLocalFunction(node.callee, context.scopes);
74126
+ if (!calledFunction || calledFunction === functionNode) return;
74127
+ reportFunctionReads(calledFunction);
74128
+ });
74129
+ };
74130
+ reportFunctionReads(creatorFunction);
74131
+ };
74132
+ const zustandNoGetDuringInitialization = defineRule({
74133
+ id: "zustand-no-get-during-initialization",
74134
+ title: "Zustand get() called during store initialization",
74135
+ severity: "error",
74136
+ category: "Correctness",
74137
+ recommendation: "Move the read into a deferred store action or derive the initial value without calling `get()`.",
74138
+ requires: ["zustand", "zustand:1"],
74139
+ create: (context) => {
74140
+ const creatorFunctions = /* @__PURE__ */ new Set();
74141
+ return {
74142
+ CallExpression(node) {
74143
+ const creator = resolveZustandStoreCreator(node, context.scopes);
74144
+ if (creator) creatorFunctions.add(creator.creatorFunction);
74145
+ },
74146
+ "Program:exit"() {
74147
+ for (const creatorFunction of creatorFunctions) {
74148
+ if (creatorFunction.generator) continue;
74149
+ const parameterIdentifier = findGetParameterIdentifier(creatorFunction.params[1]);
74150
+ const getParameterSymbol = parameterIdentifier ? context.scopes.symbolFor(parameterIdentifier) : null;
74151
+ if (!getParameterSymbol) continue;
74152
+ reportEagerGetCalls(creatorFunction, getParameterSymbol, context);
74153
+ }
74154
+ }
74155
+ };
74156
+ }
74157
+ });
74158
+ //#endregion
74159
+ //#region src/plugin/rules/state-and-effects/zustand-no-whole-store-destructure.ts
74160
+ const BOUND_STORE_FACTORY_APIS = new Set(["create", "createWithEqualityFn"]);
74161
+ const VANILLA_STORE_FACTORY_APIS = new Set(["createStore"]);
74162
+ const isStoreFactoryValue = (rawValue, factoryApiNames, scopes) => {
74163
+ const value = stripParenExpression(rawValue);
74164
+ if (!isNodeOfType(value, "CallExpression")) return false;
74165
+ const factoryCall = resolveZustandStoreFactoryCall(value, scopes);
74166
+ return Boolean(factoryCall && factoryApiNames.has(factoryCall.factoryApiName));
74167
+ };
74168
+ const isStoreValue = (rawValue, factoryApiNames, scopes) => {
74169
+ const value = stripParenExpression(rawValue);
74170
+ if (isStoreFactoryValue(value, factoryApiNames, scopes)) return true;
74171
+ if (!isNodeOfType(value, "Identifier")) return false;
74172
+ const symbol = resolveConstIdentifierAlias(value, scopes);
74173
+ return Boolean(symbol?.kind === "const" && symbol.initializer && isStoreFactoryValue(symbol.initializer, factoryApiNames, scopes));
74174
+ };
74175
+ const isBoundStoreCall = (node, scopes) => {
74176
+ const callee = stripParenExpression(node.callee);
74177
+ return isNodeOfType(callee, "Identifier") && isStoreValue(callee, BOUND_STORE_FACTORY_APIS, scopes);
74178
+ };
74179
+ const isVanillaStoreHookCall = (node, scopes) => {
74180
+ if (node.arguments.length !== 1) return false;
74181
+ const hook = resolveZustandApiBinding(node.callee, scopes);
74182
+ if (hook?.apiName !== "useStore" && hook?.apiName !== "useStoreWithEqualityFn") return false;
74183
+ return isStoreValue(node.arguments[0], VANILLA_STORE_FACTORY_APIS, scopes);
74184
+ };
74185
+ const isDirectReactRenderCall = (node, context) => {
74186
+ const renderFunction = findRenderPhaseComponentOrHook(node, context.scopes);
74187
+ if (!renderFunction || findEnclosingFunction$1(node) !== renderFunction) return false;
74188
+ const displayName = componentOrHookDisplayNameForFunction(renderFunction);
74189
+ return Boolean(displayName && (isReactHookName(displayName) || functionHasReactComponentEvidence(renderFunction, context.scopes, context.cfg)));
74190
+ };
74191
+ const zustandNoWholeStoreDestructure = defineRule({
74192
+ id: "zustand-no-whole-store-destructure",
74193
+ title: "Whole Zustand store subscribed during render",
74194
+ severity: "warn",
74195
+ requires: ["zustand:1"],
74196
+ recommendation: "Pass a selector to the Zustand hook so this component rerenders only when the state it reads changes.",
74197
+ create: (context) => ({ CallExpression(node) {
74198
+ if (!isDirectReactRenderCall(node, context)) return;
74199
+ if (!(node.arguments.length === 0 && isBoundStoreCall(node, context.scopes)) && !isVanillaStoreHookCall(node, context.scopes)) return;
74200
+ context.report({
74201
+ node,
74202
+ message: "This hook subscribes to the whole Zustand store, so every store update rerenders this component. Pass a selector for the state it reads."
74203
+ });
74204
+ } })
74205
+ });
74206
+ //#endregion
73655
74207
  //#region src/plugin/rule-registry.ts
73656
74208
  const reactDoctorRules = [
73657
74209
  {
@@ -78494,6 +79046,42 @@ const reactDoctorRules = [
78494
79046
  framework: "global",
78495
79047
  category: "Maintainability"
78496
79048
  }
79049
+ },
79050
+ {
79051
+ key: "react-doctor/zustand-no-fresh-selector-result",
79052
+ id: "zustand-no-fresh-selector-result",
79053
+ source: "react-doctor",
79054
+ originallyExternal: false,
79055
+ rule: {
79056
+ ...zustandNoFreshSelectorResult,
79057
+ framework: "global",
79058
+ category: "Performance",
79059
+ requires: [...new Set(["react", ...zustandNoFreshSelectorResult.requires ?? []])]
79060
+ }
79061
+ },
79062
+ {
79063
+ key: "react-doctor/zustand-no-get-during-initialization",
79064
+ id: "zustand-no-get-during-initialization",
79065
+ source: "react-doctor",
79066
+ originallyExternal: false,
79067
+ rule: {
79068
+ ...zustandNoGetDuringInitialization,
79069
+ framework: "global",
79070
+ category: "Bugs",
79071
+ requires: [...new Set(["react", ...zustandNoGetDuringInitialization.requires ?? []])]
79072
+ }
79073
+ },
79074
+ {
79075
+ key: "react-doctor/zustand-no-whole-store-destructure",
79076
+ id: "zustand-no-whole-store-destructure",
79077
+ source: "react-doctor",
79078
+ originallyExternal: false,
79079
+ rule: {
79080
+ ...zustandNoWholeStoreDestructure,
79081
+ framework: "global",
79082
+ category: "Bugs",
79083
+ requires: [...new Set(["react", ...zustandNoWholeStoreDestructure.requires ?? []])]
79084
+ }
78497
79085
  }
78498
79086
  ];
78499
79087
  const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.8.1-dev.61bf03e",
3
+ "version": "0.8.1-dev.6fcd9c9",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",