uilint-eslint 0.2.103 → 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 +10 -0
- package/dist/index.js +478 -34
- package/dist/index.js.map +1 -1
- package/dist/rules/no-unsafe-type-casts.js +8 -1
- package/dist/rules/no-unsafe-type-casts.js.map +1 -1
- package/dist/rules/prefer-store-selectors.js +437 -0
- package/dist/rules/prefer-store-selectors.js.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +12 -0
- package/src/rule-registry.ts +3 -0
- package/src/rules/no-unsafe-type-casts.ts +9 -4
- package/src/rules/prefer-store-selectors.test.ts +710 -0
- package/src/rules/prefer-store-selectors.ts +621 -0
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(
|
|
7
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|
|
4201
|
-
const m =
|
|
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
|
|
4305
|
+
const meta19 = index.metadataStore.get(id);
|
|
4306
4306
|
const meetsThreshold = score >= threshold ? "\u2713" : "\u2717";
|
|
4307
|
-
log(` ${meetsThreshold} ${(score * 100).toFixed(1)}% - ${id} (${
|
|
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
|
|
4399
|
-
if (!
|
|
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 ${
|
|
4404
|
-
if (nodeLine >=
|
|
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 =
|
|
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
|
-
|
|
4422
|
-
|
|
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: ${
|
|
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:
|
|
4435
|
-
end: { line:
|
|
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:
|
|
4440
|
-
name: name ||
|
|
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:
|
|
4450
|
-
endLine:
|
|
4451
|
-
startColumn:
|
|
4452
|
-
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 ||
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
7722
|
+
var meta18 = {
|
|
7289
7723
|
name: "uilint",
|
|
7290
7724
|
version
|
|
7291
7725
|
};
|
|
7292
7726
|
var plugin = {
|
|
7293
|
-
meta:
|
|
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:
|
|
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
|
-
|
|
8085
|
+
meta18 as meta,
|
|
7642
8086
|
plugin,
|
|
7643
8087
|
ruleRegistry,
|
|
7644
8088
|
rules,
|