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 +139 -1
- package/dist/index.js +643 -55
- package/package.json +1 -1
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/
|
|
30229
|
-
const
|
|
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
|
|
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
|
|
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]));
|