uilint-eslint 0.2.102 → 0.2.104

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
@@ -815,6 +815,11 @@ declare const rules: {
815
815
  }], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
816
816
  name: string;
817
817
  };
818
+ "prefer-store-selectors": _typescript_eslint_utils_ts_eslint.RuleModule<"useMemoWithStoreData" | "chainedDerivedState", [{
819
+ storeHookPattern?: string;
820
+ }], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
821
+ name: string;
822
+ };
818
823
  };
819
824
  /**
820
825
  * Plugin metadata
@@ -968,6 +973,11 @@ declare const plugin: {
968
973
  }], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
969
974
  name: string;
970
975
  };
976
+ "prefer-store-selectors": _typescript_eslint_utils_ts_eslint.RuleModule<"useMemoWithStoreData" | "chainedDerivedState", [{
977
+ storeHookPattern?: string;
978
+ }], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
979
+ name: string;
980
+ };
971
981
  };
972
982
  };
973
983
  /**
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import { ESLintUtils } from "@typescript-eslint/utils";
3
3
  var createRule = ESLintUtils.RuleCreator(
4
4
  (name) => `https://github.com/peter-suggate/uilint/blob/main/packages/uilint-eslint/docs/rules/${name}.md`
5
5
  );
6
- function defineRuleMeta(meta18) {
7
- return meta18;
6
+ function defineRuleMeta(meta19) {
7
+ return meta19;
8
8
  }
9
9
 
10
10
  // src/rules/consistent-dark-mode.ts
@@ -3658,7 +3658,7 @@ var no_secrets_in_code_default = createRule({
3658
3658
  }
3659
3659
  }
3660
3660
  }
3661
- function getVariableName(node) {
3661
+ function getVariableName2(node) {
3662
3662
  if (node.parent?.type === "VariableDeclarator") {
3663
3663
  const declarator = node.parent;
3664
3664
  if (declarator.id.type === "Identifier") {
@@ -3677,7 +3677,7 @@ var no_secrets_in_code_default = createRule({
3677
3677
  // Check string literals
3678
3678
  Literal(node) {
3679
3679
  if (typeof node.value === "string") {
3680
- const variableName = getVariableName(node);
3680
+ const variableName = getVariableName2(node);
3681
3681
  checkStringForSecrets(node.value, node, variableName);
3682
3682
  }
3683
3683
  },
@@ -3685,7 +3685,7 @@ var no_secrets_in_code_default = createRule({
3685
3685
  TemplateLiteral(node) {
3686
3686
  if (node.expressions.length === 0 && node.quasis.length === 1) {
3687
3687
  const value = node.quasis[0].value.raw;
3688
- const variableName = getVariableName(node);
3688
+ const variableName = getVariableName2(node);
3689
3689
  checkStringForSecrets(value, node, variableName);
3690
3690
  }
3691
3691
  }
@@ -4197,8 +4197,8 @@ function loadIndex(projectRoot, indexPath) {
4197
4197
  log(`Loaded metadata.json: ${Object.keys(entries).length} entries`);
4198
4198
  const metadataStore = /* @__PURE__ */ new Map();
4199
4199
  const fileToChunks = /* @__PURE__ */ new Map();
4200
- for (const [id, meta18] of Object.entries(entries)) {
4201
- const m = meta18;
4200
+ for (const [id, meta19] of Object.entries(entries)) {
4201
+ const m = meta19;
4202
4202
  metadataStore.set(id, {
4203
4203
  filePath: m.filePath,
4204
4204
  startLine: m.startLine,
@@ -4302,9 +4302,9 @@ function findSimilarChunks(index, chunkId, threshold) {
4302
4302
  const sortedAll = allScores.sort((a, b) => b.score - a.score).slice(0, 10);
4303
4303
  log(` Top 10 similarity scores (threshold=${threshold}):`);
4304
4304
  for (const { id, score } of sortedAll) {
4305
- const meta18 = index.metadataStore.get(id);
4305
+ const meta19 = index.metadataStore.get(id);
4306
4306
  const meetsThreshold = score >= threshold ? "\u2713" : "\u2717";
4307
- log(` ${meetsThreshold} ${(score * 100).toFixed(1)}% - ${id} (${meta18?.name || "anonymous"} in ${meta18?.filePath})`);
4307
+ log(` ${meetsThreshold} ${(score * 100).toFixed(1)}% - ${id} (${meta19?.name || "anonymous"} in ${meta19?.filePath})`);
4308
4308
  }
4309
4309
  log(` Found ${results.length} chunks above threshold`);
4310
4310
  return results.sort((a, b) => b.score - a.score);
@@ -4395,20 +4395,20 @@ var no_semantic_duplicates_default = createRule({
4395
4395
  log(` Chunk ${chunkId} already reported, skipping`);
4396
4396
  continue;
4397
4397
  }
4398
- const meta18 = index.metadataStore.get(chunkId);
4399
- if (!meta18) {
4398
+ const meta19 = index.metadataStore.get(chunkId);
4399
+ if (!meta19) {
4400
4400
  log(` No metadata for chunk ${chunkId}`);
4401
4401
  continue;
4402
4402
  }
4403
- log(` Checking chunk ${chunkId}: lines ${meta18.startLine}-${meta18.endLine} (node at line ${nodeLine})`);
4404
- if (nodeLine >= meta18.startLine && nodeLine <= meta18.endLine) {
4403
+ log(` Checking chunk ${chunkId}: lines ${meta19.startLine}-${meta19.endLine} (node at line ${nodeLine})`);
4404
+ if (nodeLine >= meta19.startLine && nodeLine <= meta19.endLine) {
4405
4405
  log(` Node is within chunk range, searching for similar chunks...`);
4406
4406
  const similar = findSimilarChunks(index, chunkId, threshold);
4407
4407
  if (similar.length > 0) {
4408
4408
  const best = similar[0];
4409
4409
  const bestMeta = index.metadataStore.get(best.id);
4410
4410
  if (bestMeta) {
4411
- const chunkLines = meta18.endLine - meta18.startLine + 1;
4411
+ const chunkLines = meta19.endLine - meta19.startLine + 1;
4412
4412
  if (chunkLines < minLines) {
4413
4413
  log(` Skipping: chunk has ${chunkLines} lines, below minLines=${minLines}`);
4414
4414
  continue;
@@ -4418,8 +4418,8 @@ var no_semantic_duplicates_default = createRule({
4418
4418
  const similarity = Math.round(best.score * 100);
4419
4419
  const sourceCode = extractCodeFromFile(
4420
4420
  filename,
4421
- meta18.startLine,
4422
- meta18.endLine
4421
+ meta19.startLine,
4422
+ meta19.endLine
4423
4423
  );
4424
4424
  const targetAbsolutePath = join6(projectRoot, bestMeta.filePath);
4425
4425
  const targetCode = extractCodeFromFile(
@@ -4427,17 +4427,17 @@ var no_semantic_duplicates_default = createRule({
4427
4427
  bestMeta.startLine,
4428
4428
  bestMeta.endLine
4429
4429
  );
4430
- log(` REPORTING: ${meta18.kind} '${name || meta18.name}' is ${similarity}% similar to '${bestMeta.name}' at ${relPath}:${bestMeta.startLine}`);
4430
+ log(` REPORTING: ${meta19.kind} '${name || meta19.name}' is ${similarity}% similar to '${bestMeta.name}' at ${relPath}:${bestMeta.startLine}`);
4431
4431
  context.report({
4432
4432
  node,
4433
4433
  loc: {
4434
- start: { line: meta18.startLine, column: meta18.startColumn },
4435
- end: { line: meta18.endLine, column: meta18.endColumn }
4434
+ start: { line: meta19.startLine, column: meta19.startColumn },
4435
+ end: { line: meta19.endLine, column: meta19.endColumn }
4436
4436
  },
4437
4437
  messageId: "semanticDuplicate",
4438
4438
  data: {
4439
- kind: meta18.kind,
4440
- name: name || meta18.name || "(anonymous)",
4439
+ kind: meta19.kind,
4440
+ name: name || meta19.name || "(anonymous)",
4441
4441
  similarity: String(similarity),
4442
4442
  otherName: bestMeta.name || "(anonymous)",
4443
4443
  otherLocation: `${relPath}:${bestMeta.startLine}`,
@@ -4446,10 +4446,10 @@ var no_semantic_duplicates_default = createRule({
4446
4446
  targetCode: targetCode || "",
4447
4447
  sourceLocation: JSON.stringify({
4448
4448
  filePath: relativeFilename,
4449
- startLine: meta18.startLine,
4450
- endLine: meta18.endLine,
4451
- startColumn: meta18.startColumn,
4452
- endColumn: meta18.endColumn
4449
+ startLine: meta19.startLine,
4450
+ endLine: meta19.endLine,
4451
+ startColumn: meta19.startColumn,
4452
+ endColumn: meta19.endColumn
4453
4453
  }),
4454
4454
  targetLocation: JSON.stringify({
4455
4455
  filePath: bestMeta.filePath,
@@ -4459,7 +4459,7 @@ var no_semantic_duplicates_default = createRule({
4459
4459
  startColumn: bestMeta.startColumn,
4460
4460
  endColumn: bestMeta.endColumn
4461
4461
  }),
4462
- sourceName: name || meta18.name || "(anonymous)",
4462
+ sourceName: name || meta19.name || "(anonymous)",
4463
4463
  targetName: bestMeta.name || "(anonymous)",
4464
4464
  similarityScore: String(best.score)
4465
4465
  }
@@ -4469,7 +4469,7 @@ var no_semantic_duplicates_default = createRule({
4469
4469
  log(` No similar chunks found above threshold`);
4470
4470
  }
4471
4471
  } else {
4472
- log(` Node line ${nodeLine} not in chunk range ${meta18.startLine}-${meta18.endLine}`);
4472
+ log(` Node line ${nodeLine} not in chunk range ${meta19.startLine}-${meta19.endLine}`);
4473
4473
  }
4474
4474
  }
4475
4475
  }
@@ -7038,7 +7038,14 @@ function getTypeName(typeAnnotation) {
7038
7038
  }
7039
7039
  }
7040
7040
  function getQualifiedName(node) {
7041
- const left = node.left.type === "Identifier" ? node.left.name : getQualifiedName(node.left);
7041
+ let left;
7042
+ if (node.left.type === "Identifier") {
7043
+ left = node.left.name;
7044
+ } else if (node.left.type === "TSQualifiedName") {
7045
+ left = getQualifiedName(node.left);
7046
+ } else {
7047
+ left = "this";
7048
+ }
7042
7049
  return `${left}.${node.right.name}`;
7043
7050
  }
7044
7051
  function isAllowedType(typeAnnotation, allowedTypes) {
@@ -7203,6 +7210,430 @@ var no_unsafe_type_casts_default = createRule({
7203
7210
  }
7204
7211
  });
7205
7212
 
7213
+ // src/rules/prefer-store-selectors.ts
7214
+ var meta17 = defineRuleMeta({
7215
+ id: "prefer-store-selectors",
7216
+ version: "1.0.0",
7217
+ name: "Prefer Store Selectors",
7218
+ description: "Derived state from store should use selectors, not useMemo",
7219
+ defaultSeverity: "warn",
7220
+ category: "static",
7221
+ icon: "\u{1F3EA}",
7222
+ hint: "Move derived state to store selectors",
7223
+ defaultEnabled: true,
7224
+ defaultOptions: [{ storeHookPattern: "^use.*Store$" }],
7225
+ optionSchema: {
7226
+ fields: [
7227
+ {
7228
+ key: "storeHookPattern",
7229
+ label: "Store hook pattern",
7230
+ type: "text",
7231
+ defaultValue: "^use.*Store$",
7232
+ description: "Regex pattern for identifying Zustand store hooks"
7233
+ }
7234
+ ]
7235
+ },
7236
+ docs: `
7237
+ ## What it does
7238
+
7239
+ Detects when derived state computed from Zustand store data using \`useMemo\`
7240
+ should instead be moved to a store selector for better performance and cleaner code.
7241
+
7242
+ ## Why it's useful
7243
+
7244
+ - **Performance**: Selectors are memoized at the store level, avoiding recomputation
7245
+ - **Reusability**: Selectors can be shared across components
7246
+ - **Testability**: Selectors are pure functions that are easy to unit test
7247
+ - **Separation of concerns**: Keeps data transformation logic out of components
7248
+
7249
+ ## Examples
7250
+
7251
+ ### \u274C Incorrect
7252
+
7253
+ \`\`\`tsx
7254
+ function ProductList() {
7255
+ const products = useStore((s) => s.products);
7256
+
7257
+ // Derived state computed in component - should be a selector
7258
+ const activeProducts = useMemo(
7259
+ () => products.filter((p) => p.isActive),
7260
+ [products]
7261
+ );
7262
+
7263
+ return <List items={activeProducts} />;
7264
+ }
7265
+
7266
+ // Multiple chained useMemo calls
7267
+ function Dashboard() {
7268
+ const data = useDataStore((s) => s.data);
7269
+
7270
+ const filtered = useMemo(() => data.filter(isValid), [data]);
7271
+ const sorted = useMemo(() => filtered.sort(byDate), [filtered]);
7272
+ const mapped = useMemo(() => sorted.map(format), [sorted]);
7273
+
7274
+ return <Table rows={mapped} />;
7275
+ }
7276
+ \`\`\`
7277
+
7278
+ ### \u2705 Correct
7279
+
7280
+ \`\`\`tsx
7281
+ // Define selectors in the store file
7282
+ const selectActiveProducts = (state) =>
7283
+ state.products.filter((p) => p.isActive);
7284
+
7285
+ function ProductList() {
7286
+ // Use selector for derived state
7287
+ const activeProducts = useStore(selectActiveProducts);
7288
+
7289
+ return <List items={activeProducts} />;
7290
+ }
7291
+
7292
+ // Combined selector for dashboard
7293
+ const selectFormattedData = (state) =>
7294
+ state.data
7295
+ .filter(isValid)
7296
+ .sort(byDate)
7297
+ .map(format);
7298
+
7299
+ function Dashboard() {
7300
+ const formattedData = useDataStore(selectFormattedData);
7301
+ return <Table rows={formattedData} />;
7302
+ }
7303
+ \`\`\`
7304
+
7305
+ ## Configuration
7306
+
7307
+ \`\`\`js
7308
+ // eslint.config.js
7309
+ "uilint/prefer-store-selectors": ["warn", {
7310
+ storeHookPattern: "^use.*Store$" // Match useXxxStore pattern
7311
+ }]
7312
+ \`\`\`
7313
+ `
7314
+ });
7315
+ var TRANSFORMATION_METHODS = /* @__PURE__ */ new Set([
7316
+ "filter",
7317
+ "map",
7318
+ "reduce",
7319
+ "sort",
7320
+ "flat",
7321
+ "flatMap",
7322
+ "slice",
7323
+ "concat",
7324
+ "find",
7325
+ "findIndex",
7326
+ "some",
7327
+ "every",
7328
+ "includes",
7329
+ "reverse",
7330
+ "join"
7331
+ ]);
7332
+ var ARRAY_TRANSFORMATION_FUNCTIONS = /* @__PURE__ */ new Set([
7333
+ "Array.from",
7334
+ "Object.keys",
7335
+ "Object.values",
7336
+ "Object.entries"
7337
+ ]);
7338
+ function isZustandStoreCall2(node, storePattern) {
7339
+ if (node.callee.type === "Identifier") {
7340
+ return storePattern.test(node.callee.name);
7341
+ }
7342
+ return false;
7343
+ }
7344
+ function isUseMemoCall(node) {
7345
+ return node.callee.type === "Identifier" && node.callee.name === "useMemo";
7346
+ }
7347
+ function getVariableName(node) {
7348
+ if (node.id.type === "Identifier") {
7349
+ return node.id.name;
7350
+ }
7351
+ return null;
7352
+ }
7353
+ function isTransformationCall(node, trackedVars) {
7354
+ if (node.callee.type === "MemberExpression") {
7355
+ const { object, property } = node.callee;
7356
+ if (object.type === "Identifier" && property.type === "Identifier" && trackedVars.has(object.name) && TRANSFORMATION_METHODS.has(property.name)) {
7357
+ return { isTransform: true, varName: object.name };
7358
+ }
7359
+ if (object.type === "CallExpression") {
7360
+ const nested = isTransformationCall(object, trackedVars);
7361
+ if (nested.isTransform) {
7362
+ return nested;
7363
+ }
7364
+ }
7365
+ }
7366
+ if (node.callee.type === "MemberExpression") {
7367
+ const { object, property } = node.callee;
7368
+ if (object.type === "Identifier" && property.type === "Identifier") {
7369
+ const funcName = `${object.name}.${property.name}`;
7370
+ if (ARRAY_TRANSFORMATION_FUNCTIONS.has(funcName)) {
7371
+ if (node.arguments.length > 0 && node.arguments[0].type === "Identifier" && trackedVars.has(node.arguments[0].name)) {
7372
+ return { isTransform: true, varName: node.arguments[0].name };
7373
+ }
7374
+ }
7375
+ }
7376
+ }
7377
+ return { isTransform: false, varName: null };
7378
+ }
7379
+ function referencesTrackedVar(node, trackedVars) {
7380
+ if (node.type === "Identifier" && trackedVars.has(node.name)) {
7381
+ return node.name;
7382
+ }
7383
+ if (node.type === "MemberExpression") {
7384
+ return referencesTrackedVar(node.object, trackedVars);
7385
+ }
7386
+ if (node.type === "CallExpression") {
7387
+ const calleeRef = referencesTrackedVar(node.callee, trackedVars);
7388
+ if (calleeRef) return calleeRef;
7389
+ for (const arg of node.arguments) {
7390
+ const argRef = referencesTrackedVar(arg, trackedVars);
7391
+ if (argRef) return argRef;
7392
+ }
7393
+ }
7394
+ if (node.type === "BinaryExpression" || node.type === "LogicalExpression") {
7395
+ const leftRef = referencesTrackedVar(node.left, trackedVars);
7396
+ if (leftRef) return leftRef;
7397
+ return referencesTrackedVar(node.right, trackedVars);
7398
+ }
7399
+ if (node.type === "ConditionalExpression") {
7400
+ const testRef = referencesTrackedVar(node.test, trackedVars);
7401
+ if (testRef) return testRef;
7402
+ const consequentRef = referencesTrackedVar(node.consequent, trackedVars);
7403
+ if (consequentRef) return consequentRef;
7404
+ return referencesTrackedVar(node.alternate, trackedVars);
7405
+ }
7406
+ if (node.type === "ArrayExpression") {
7407
+ for (const element of node.elements) {
7408
+ if (element) {
7409
+ const elemRef = referencesTrackedVar(element, trackedVars);
7410
+ if (elemRef) return elemRef;
7411
+ }
7412
+ }
7413
+ }
7414
+ if (node.type === "SpreadElement") {
7415
+ return referencesTrackedVar(node.argument, trackedVars);
7416
+ }
7417
+ return null;
7418
+ }
7419
+ function analyzeUseMemoBody(body, trackedVars) {
7420
+ if (body.type === "CallExpression") {
7421
+ const result = isTransformationCall(body, trackedVars);
7422
+ if (result.isTransform) {
7423
+ return { hasTransformation: true, varName: result.varName };
7424
+ }
7425
+ }
7426
+ if (body.type === "BlockStatement") {
7427
+ for (const statement of body.body) {
7428
+ if (statement.type === "ReturnStatement" && statement.argument) {
7429
+ if (statement.argument.type === "CallExpression") {
7430
+ const result = isTransformationCall(statement.argument, trackedVars);
7431
+ if (result.isTransform) {
7432
+ return { hasTransformation: true, varName: result.varName };
7433
+ }
7434
+ }
7435
+ const varRef = referencesTrackedVar(statement.argument, trackedVars);
7436
+ if (varRef && statement.argument.type === "CallExpression") {
7437
+ return { hasTransformation: true, varName: varRef };
7438
+ }
7439
+ }
7440
+ if (statement.type === "VariableDeclaration") {
7441
+ for (const decl of statement.declarations) {
7442
+ if (decl.init && decl.init.type === "CallExpression") {
7443
+ const result = isTransformationCall(decl.init, trackedVars);
7444
+ if (result.isTransform) {
7445
+ return { hasTransformation: true, varName: result.varName };
7446
+ }
7447
+ }
7448
+ }
7449
+ }
7450
+ }
7451
+ }
7452
+ return { hasTransformation: false, varName: null };
7453
+ }
7454
+ function getUseMemoCallback(node) {
7455
+ if (node.arguments.length === 0) return null;
7456
+ const callback = node.arguments[0];
7457
+ if (callback.type === "ArrowFunctionExpression" || callback.type === "FunctionExpression") {
7458
+ return callback;
7459
+ }
7460
+ return null;
7461
+ }
7462
+ var prefer_store_selectors_default = createRule({
7463
+ name: "prefer-store-selectors",
7464
+ meta: {
7465
+ type: "suggestion",
7466
+ docs: {
7467
+ description: "Derived state from store should use selectors, not useMemo"
7468
+ },
7469
+ messages: {
7470
+ useMemoWithStoreData: "useMemo derives state from '{{varName}}' which comes from store. Move this computation to a Zustand selector.",
7471
+ chainedDerivedState: "Multiple chained useMemo calls derive from store data. Consolidate into store selectors."
7472
+ },
7473
+ schema: [
7474
+ {
7475
+ type: "object",
7476
+ properties: {
7477
+ storeHookPattern: {
7478
+ type: "string",
7479
+ description: "Regex pattern for store hook names"
7480
+ }
7481
+ },
7482
+ additionalProperties: false
7483
+ }
7484
+ ]
7485
+ },
7486
+ defaultOptions: [
7487
+ {
7488
+ storeHookPattern: "^use.*Store$"
7489
+ }
7490
+ ],
7491
+ create(context) {
7492
+ const options = context.options[0] || {};
7493
+ const storeHookPatternStr = options.storeHookPattern ?? "^use.*Store$";
7494
+ let storePattern;
7495
+ try {
7496
+ storePattern = new RegExp(storeHookPatternStr);
7497
+ } catch {
7498
+ storePattern = /^use.*Store$/;
7499
+ }
7500
+ const scopeStack = [];
7501
+ function currentScope() {
7502
+ return scopeStack[scopeStack.length - 1];
7503
+ }
7504
+ function pushScope() {
7505
+ scopeStack.push({
7506
+ storeVars: /* @__PURE__ */ new Set(),
7507
+ derivedMemoVars: /* @__PURE__ */ new Set(),
7508
+ useMemoNodes: []
7509
+ });
7510
+ }
7511
+ function popScope() {
7512
+ return scopeStack.pop();
7513
+ }
7514
+ function reportScope(scope) {
7515
+ const { useMemoNodes, derivedMemoVars } = scope;
7516
+ if (useMemoNodes.length === 1) {
7517
+ const { node, sourceVar } = useMemoNodes[0];
7518
+ context.report({
7519
+ node,
7520
+ messageId: "useMemoWithStoreData",
7521
+ data: { varName: sourceVar }
7522
+ });
7523
+ } else if (useMemoNodes.length > 1) {
7524
+ const hasChain = useMemoNodes.some(
7525
+ ({ sourceVar }) => derivedMemoVars.has(sourceVar)
7526
+ );
7527
+ if (hasChain) {
7528
+ context.report({
7529
+ node: useMemoNodes[0].node,
7530
+ messageId: "chainedDerivedState"
7531
+ });
7532
+ } else {
7533
+ for (const { node, sourceVar } of useMemoNodes) {
7534
+ context.report({
7535
+ node,
7536
+ messageId: "useMemoWithStoreData",
7537
+ data: { varName: sourceVar }
7538
+ });
7539
+ }
7540
+ }
7541
+ }
7542
+ }
7543
+ function isComponentOrHook(node) {
7544
+ if (node.type === "FunctionDeclaration") {
7545
+ return true;
7546
+ }
7547
+ if (node.parent?.type === "VariableDeclarator") {
7548
+ const declarator = node.parent;
7549
+ if (declarator.id.type === "Identifier") {
7550
+ const name = declarator.id.name;
7551
+ return /^[A-Z]/.test(name) || /^use[A-Z]/.test(name);
7552
+ }
7553
+ }
7554
+ if (node.type === "FunctionExpression" && node.id) {
7555
+ const name = node.id.name;
7556
+ return /^[A-Z]/.test(name) || /^use[A-Z]/.test(name);
7557
+ }
7558
+ return false;
7559
+ }
7560
+ return {
7561
+ // Push scope for component/hook functions
7562
+ "FunctionDeclaration, FunctionExpression, ArrowFunctionExpression"(node) {
7563
+ if (isComponentOrHook(node)) {
7564
+ pushScope();
7565
+ }
7566
+ },
7567
+ // Pop scope and report when exiting component/hook functions
7568
+ "FunctionDeclaration:exit"(node) {
7569
+ if (isComponentOrHook(node)) {
7570
+ const scope = popScope();
7571
+ if (scope) {
7572
+ reportScope(scope);
7573
+ }
7574
+ }
7575
+ },
7576
+ "FunctionExpression:exit"(node) {
7577
+ if (isComponentOrHook(node)) {
7578
+ const scope = popScope();
7579
+ if (scope) {
7580
+ reportScope(scope);
7581
+ }
7582
+ }
7583
+ },
7584
+ "ArrowFunctionExpression:exit"(node) {
7585
+ if (isComponentOrHook(node)) {
7586
+ const scope = popScope();
7587
+ if (scope) {
7588
+ reportScope(scope);
7589
+ }
7590
+ }
7591
+ },
7592
+ // Also handle program-level code
7593
+ Program() {
7594
+ pushScope();
7595
+ },
7596
+ "Program:exit"() {
7597
+ const scope = popScope();
7598
+ if (scope) {
7599
+ reportScope(scope);
7600
+ }
7601
+ },
7602
+ // Track variable declarations from store hooks
7603
+ VariableDeclarator(node) {
7604
+ const scope = currentScope();
7605
+ if (!scope) return;
7606
+ if (!node.init || node.init.type !== "CallExpression") {
7607
+ return;
7608
+ }
7609
+ const varName = getVariableName(node);
7610
+ if (!varName) return;
7611
+ if (isZustandStoreCall2(node.init, storePattern)) {
7612
+ scope.storeVars.add(varName);
7613
+ return;
7614
+ }
7615
+ if (isUseMemoCall(node.init)) {
7616
+ const callback = getUseMemoCallback(node.init);
7617
+ if (!callback) return;
7618
+ const body = callback.body;
7619
+ const allTracked = /* @__PURE__ */ new Set();
7620
+ scope.storeVars.forEach((v) => allTracked.add(v));
7621
+ scope.derivedMemoVars.forEach((v) => allTracked.add(v));
7622
+ const analysis = analyzeUseMemoBody(body, allTracked);
7623
+ if (analysis.hasTransformation && analysis.varName) {
7624
+ scope.derivedMemoVars.add(varName);
7625
+ scope.useMemoNodes.push({
7626
+ node: node.init,
7627
+ varName,
7628
+ sourceVar: analysis.varName
7629
+ });
7630
+ }
7631
+ }
7632
+ }
7633
+ };
7634
+ }
7635
+ });
7636
+
7206
7637
  // src/category-registry.ts
7207
7638
  var categoryRegistry = [
7208
7639
  {
@@ -7248,7 +7679,9 @@ var ruleRegistry = [
7248
7679
  // Style preferences
7249
7680
  meta15,
7250
7681
  // Type safety
7251
- meta16
7682
+ meta16,
7683
+ // Zustand best practices
7684
+ meta17
7252
7685
  ];
7253
7686
  function getRuleMetadata(id) {
7254
7687
  return ruleRegistry.find((rule) => rule.id === id);
@@ -7282,15 +7715,16 @@ var rules = {
7282
7715
  "no-semantic-duplicates": no_semantic_duplicates_default,
7283
7716
  "require-test-coverage": require_test_coverage_default,
7284
7717
  "prefer-tailwind": prefer_tailwind_default,
7285
- "no-unsafe-type-casts": no_unsafe_type_casts_default
7718
+ "no-unsafe-type-casts": no_unsafe_type_casts_default,
7719
+ "prefer-store-selectors": prefer_store_selectors_default
7286
7720
  };
7287
7721
  var version = "0.1.0";
7288
- var meta17 = {
7722
+ var meta18 = {
7289
7723
  name: "uilint",
7290
7724
  version
7291
7725
  };
7292
7726
  var plugin = {
7293
- meta: meta17,
7727
+ meta: meta18,
7294
7728
  rules
7295
7729
  };
7296
7730
  var jsxLanguageOptions = {
@@ -7435,6 +7869,11 @@ var recommendedConfig = {
7435
7869
  "allowInCatchBlocks": true,
7436
7870
  "allowedTypes": []
7437
7871
  }
7872
+ ]],
7873
+ "uilint/prefer-store-selectors": ["warn", ...[
7874
+ {
7875
+ "storeHookPattern": "^use.*Store$"
7876
+ }
7438
7877
  ]]
7439
7878
  }
7440
7879
  };
@@ -7592,6 +8031,11 @@ var strictConfig = {
7592
8031
  "allowInCatchBlocks": true,
7593
8032
  "allowedTypes": []
7594
8033
  }
8034
+ ]],
8035
+ "uilint/prefer-store-selectors": ["warn", ...[
8036
+ {
8037
+ "storeHookPattern": "^use.*Store$"
8038
+ }
7595
8039
  ]]
7596
8040
  }
7597
8041
  };
@@ -7600,7 +8044,7 @@ var configs = {
7600
8044
  strict: strictConfig
7601
8045
  };
7602
8046
  var uilintEslint = {
7603
- meta: meta17,
8047
+ meta: meta18,
7604
8048
  plugin,
7605
8049
  rules,
7606
8050
  configs
@@ -7638,7 +8082,7 @@ export {
7638
8082
  isEventHandlerAttribute,
7639
8083
  loadCache,
7640
8084
  loadStyleguide,
7641
- meta17 as meta,
8085
+ meta18 as meta,
7642
8086
  plugin,
7643
8087
  ruleRegistry,
7644
8088
  rules,