@violetflux/eslint-plugin-kerros 0.2.4 → 0.3.1
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.cjs +56 -34
- package/dist/index.mjs +56 -34
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -80,6 +80,7 @@ function getTypeServices(context) {
|
|
|
80
80
|
//#region src/internal/kerros-types.ts
|
|
81
81
|
const markerNames = {
|
|
82
82
|
externalStoreProvider: "externalStoreProviderMarker",
|
|
83
|
+
storeGetter: "storeGetterMarker",
|
|
83
84
|
storeHook: "storeHookMarker",
|
|
84
85
|
storeInstanceHook: "storeInstanceHookMarker"
|
|
85
86
|
};
|
|
@@ -177,11 +178,11 @@ function createKerrosProgramTools(program) {
|
|
|
177
178
|
const returnType = checker.getReturnTypeOfSignature(signature);
|
|
178
179
|
const hookType = getTupleMemberType(returnType, 0, node);
|
|
179
180
|
if (!hookType || !hasMarker(hookType, "storeHook")) return void 0;
|
|
180
|
-
const
|
|
181
|
-
if (name === "createStore"
|
|
182
|
-
if (name !== "bindStore" || !
|
|
181
|
+
const thirdType = getTupleMemberType(returnType, 2, node);
|
|
182
|
+
if (name === "createStore") return !thirdType || hasMarker(thirdType, "storeGetter") ? "createStore" : void 0;
|
|
183
|
+
if (name !== "bindStore" || !thirdType) return void 0;
|
|
183
184
|
const providerType = getTupleMemberType(returnType, 1, node);
|
|
184
|
-
return providerType && hasMarker(providerType, "externalStoreProvider") && hasMarker(
|
|
185
|
+
return providerType && hasMarker(providerType, "externalStoreProvider") && hasMarker(thirdType, "storeInstanceHook") ? "bindStore" : void 0;
|
|
185
186
|
};
|
|
186
187
|
/** Test whether a call invokes a nominal Kerros Store Hook. */
|
|
187
188
|
const isStoreHookCall = (node) => {
|
|
@@ -301,10 +302,11 @@ const bindingNaming = createRule({
|
|
|
301
302
|
name: "binding-naming",
|
|
302
303
|
meta: {
|
|
303
304
|
type: "problem",
|
|
304
|
-
docs: { description: "Keep Kerros Hook and
|
|
305
|
+
docs: { description: "Keep Kerros Hook, Provider, and getter binding names aligned." },
|
|
305
306
|
schema: [],
|
|
306
307
|
messages: {
|
|
307
308
|
destructureBinding: "Kerros factory results must be destructured.",
|
|
309
|
+
getterName: "The Store getter must be named getXxx.",
|
|
308
310
|
hookName: "The Store Hook must be named useXxx.",
|
|
309
311
|
providerName: "The Provider name must match the Store Hook.",
|
|
310
312
|
instanceName: "The instance Hook name must match the Store Hook."
|
|
@@ -334,12 +336,12 @@ const bindingNaming = createRule({
|
|
|
334
336
|
}
|
|
335
337
|
const hook = getElement(declarator.id, 0);
|
|
336
338
|
const provider = getElement(declarator.id, 1);
|
|
337
|
-
const
|
|
339
|
+
const third = getElement(declarator.id, 2);
|
|
338
340
|
const explicitName = kind === "createStore" ? getCreateStoreName(node) : getBindStoreName(node);
|
|
339
341
|
const hookMatch = hook && /^use(?<name>[A-Z][A-Za-z0-9]*)$/u.exec(hook.name);
|
|
340
342
|
const providerMatch = provider && /^(?<name>[A-Z][A-Za-z0-9]*)Provider$/u.exec(provider.name);
|
|
341
|
-
const
|
|
342
|
-
const inferredName = hookMatch?.groups?.name ?? providerMatch?.groups?.name ??
|
|
343
|
+
const thirdMatch = third && (kind === "createStore" ? /^get(?<name>[A-Z][A-Za-z0-9]*)$/u.exec(third.name) : /^use(?<name>[A-Z][A-Za-z0-9]*)Instance$/u.exec(third.name));
|
|
344
|
+
const inferredName = hookMatch?.groups?.name ?? providerMatch?.groups?.name ?? thirdMatch?.groups?.name;
|
|
343
345
|
const name = explicitName ?? inferredName;
|
|
344
346
|
if (hook && (!hookMatch || name && hookMatch.groups?.name !== name)) context.report({
|
|
345
347
|
node: hook,
|
|
@@ -349,9 +351,9 @@ const bindingNaming = createRule({
|
|
|
349
351
|
node: provider,
|
|
350
352
|
messageId: "providerName"
|
|
351
353
|
});
|
|
352
|
-
if (
|
|
353
|
-
node:
|
|
354
|
-
messageId: "instanceName"
|
|
354
|
+
if (third && (!thirdMatch || name && thirdMatch.groups?.name !== name)) context.report({
|
|
355
|
+
node: third,
|
|
356
|
+
messageId: kind === "createStore" ? "getterName" : "instanceName"
|
|
355
357
|
});
|
|
356
358
|
} };
|
|
357
359
|
}
|
|
@@ -470,32 +472,51 @@ const setMutationMethods = /* @__PURE__ */ new Set([
|
|
|
470
472
|
"clear",
|
|
471
473
|
"delete"
|
|
472
474
|
]);
|
|
473
|
-
/** Build
|
|
474
|
-
function
|
|
475
|
+
/** Build a reusable index that streams dynamic call-site contexts without retaining every path. */
|
|
476
|
+
function createFunctionCallSiteContexts(edges) {
|
|
475
477
|
const incoming = /* @__PURE__ */ new Map();
|
|
476
478
|
for (const edge of edges) {
|
|
477
479
|
const existing = incoming.get(edge.callee) ?? [];
|
|
478
480
|
existing.push(edge);
|
|
479
481
|
incoming.set(edge.callee, existing);
|
|
480
482
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
483
|
+
/** Visit paths independently while retaining only the current DFS path. */
|
|
484
|
+
const some = (target, predicate) => {
|
|
485
|
+
const calls = /* @__PURE__ */ new Map();
|
|
486
|
+
const active = /* @__PURE__ */ new Set([target]);
|
|
487
|
+
const frames = [{
|
|
488
|
+
advanced: false,
|
|
489
|
+
edges: incoming.get(target) ?? [],
|
|
490
|
+
fn: target,
|
|
491
|
+
index: 0
|
|
492
|
+
}];
|
|
493
|
+
while (frames.length > 0) {
|
|
494
|
+
const frame = frames.at(-1);
|
|
495
|
+
if (!frame) break;
|
|
496
|
+
const edge = frame.edges[frame.index];
|
|
497
|
+
if (edge) {
|
|
498
|
+
frame.index += 1;
|
|
499
|
+
if (active.has(edge.caller)) continue;
|
|
500
|
+
frame.advanced = true;
|
|
501
|
+
calls.set(frame.fn, edge.site);
|
|
502
|
+
active.add(edge.caller);
|
|
503
|
+
frames.push({
|
|
504
|
+
advanced: false,
|
|
505
|
+
edges: incoming.get(edge.caller) ?? [],
|
|
506
|
+
fn: edge.caller,
|
|
507
|
+
index: 0,
|
|
508
|
+
parent: frame.fn
|
|
509
|
+
});
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (!frame.advanced && predicate(calls)) return true;
|
|
513
|
+
frames.pop();
|
|
514
|
+
active.delete(frame.fn);
|
|
515
|
+
if (frame.parent) calls.delete(frame.parent);
|
|
494
516
|
}
|
|
495
|
-
|
|
517
|
+
return false;
|
|
496
518
|
};
|
|
497
|
-
|
|
498
|
-
return contexts;
|
|
519
|
+
return { some };
|
|
499
520
|
}
|
|
500
521
|
/** Track assignment sources and resolve definitions that reach a concrete reference point. */
|
|
501
522
|
function createReferenceOriginTracker(program) {
|
|
@@ -1479,7 +1500,7 @@ const noRenderInstanceSnapshot = createRule({
|
|
|
1479
1500
|
changed = true;
|
|
1480
1501
|
}
|
|
1481
1502
|
}
|
|
1482
|
-
const
|
|
1503
|
+
const callContexts = createFunctionCallSiteContexts(localEdges.filter((edge) => rendered.has(edge.caller)));
|
|
1483
1504
|
/** Test whether an expression is a local alias of a Store instance Hook result. */
|
|
1484
1505
|
const isInstanceDerived = (input, calls, seen = /* @__PURE__ */ new Set()) => {
|
|
1485
1506
|
const node = unwrapExpression(input);
|
|
@@ -1512,7 +1533,7 @@ const noRenderInstanceSnapshot = createRule({
|
|
|
1512
1533
|
};
|
|
1513
1534
|
for (const snapshot of snapshots) {
|
|
1514
1535
|
if (!snapshot.owner || !rendered.has(snapshot.owner)) continue;
|
|
1515
|
-
if (
|
|
1536
|
+
if (callContexts.some(snapshot.owner, (calls) => {
|
|
1516
1537
|
return snapshot.kind === "instance" ? isInstanceDerived(snapshot.source, calls) : isSnapshotReaderDerived(snapshot.source, calls);
|
|
1517
1538
|
})) context.report({
|
|
1518
1539
|
node: snapshot.node,
|
|
@@ -1680,9 +1701,10 @@ const noStoreMutation = createRule({
|
|
|
1680
1701
|
site: node
|
|
1681
1702
|
});
|
|
1682
1703
|
}
|
|
1683
|
-
|
|
1704
|
+
const callContexts = createFunctionCallSiteContexts(localEdges);
|
|
1705
|
+
for (const candidate of candidates) if (candidate.owner ? callContexts.some(candidate.owner, (calls) => {
|
|
1684
1706
|
return isSnapshotDerived(candidate.expression, maxAliasDepth, /* @__PURE__ */ new Set(), calls);
|
|
1685
|
-
})) context.report({
|
|
1707
|
+
}) : isSnapshotDerived(candidate.expression, maxAliasDepth, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map())) context.report({
|
|
1686
1708
|
node: candidate.node,
|
|
1687
1709
|
messageId: "mutation"
|
|
1688
1710
|
});
|
|
@@ -2449,7 +2471,7 @@ const rules = {
|
|
|
2449
2471
|
const plugin = {
|
|
2450
2472
|
meta: {
|
|
2451
2473
|
name: "@violetflux/eslint-plugin-kerros",
|
|
2452
|
-
version: "0.
|
|
2474
|
+
version: "0.3.1"
|
|
2453
2475
|
},
|
|
2454
2476
|
rules
|
|
2455
2477
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -52,6 +52,7 @@ function getTypeServices(context) {
|
|
|
52
52
|
//#region src/internal/kerros-types.ts
|
|
53
53
|
const markerNames = {
|
|
54
54
|
externalStoreProvider: "externalStoreProviderMarker",
|
|
55
|
+
storeGetter: "storeGetterMarker",
|
|
55
56
|
storeHook: "storeHookMarker",
|
|
56
57
|
storeInstanceHook: "storeInstanceHookMarker"
|
|
57
58
|
};
|
|
@@ -149,11 +150,11 @@ function createKerrosProgramTools(program) {
|
|
|
149
150
|
const returnType = checker.getReturnTypeOfSignature(signature);
|
|
150
151
|
const hookType = getTupleMemberType(returnType, 0, node);
|
|
151
152
|
if (!hookType || !hasMarker(hookType, "storeHook")) return void 0;
|
|
152
|
-
const
|
|
153
|
-
if (name === "createStore"
|
|
154
|
-
if (name !== "bindStore" || !
|
|
153
|
+
const thirdType = getTupleMemberType(returnType, 2, node);
|
|
154
|
+
if (name === "createStore") return !thirdType || hasMarker(thirdType, "storeGetter") ? "createStore" : void 0;
|
|
155
|
+
if (name !== "bindStore" || !thirdType) return void 0;
|
|
155
156
|
const providerType = getTupleMemberType(returnType, 1, node);
|
|
156
|
-
return providerType && hasMarker(providerType, "externalStoreProvider") && hasMarker(
|
|
157
|
+
return providerType && hasMarker(providerType, "externalStoreProvider") && hasMarker(thirdType, "storeInstanceHook") ? "bindStore" : void 0;
|
|
157
158
|
};
|
|
158
159
|
/** Test whether a call invokes a nominal Kerros Store Hook. */
|
|
159
160
|
const isStoreHookCall = (node) => {
|
|
@@ -273,10 +274,11 @@ const bindingNaming = createRule({
|
|
|
273
274
|
name: "binding-naming",
|
|
274
275
|
meta: {
|
|
275
276
|
type: "problem",
|
|
276
|
-
docs: { description: "Keep Kerros Hook and
|
|
277
|
+
docs: { description: "Keep Kerros Hook, Provider, and getter binding names aligned." },
|
|
277
278
|
schema: [],
|
|
278
279
|
messages: {
|
|
279
280
|
destructureBinding: "Kerros factory results must be destructured.",
|
|
281
|
+
getterName: "The Store getter must be named getXxx.",
|
|
280
282
|
hookName: "The Store Hook must be named useXxx.",
|
|
281
283
|
providerName: "The Provider name must match the Store Hook.",
|
|
282
284
|
instanceName: "The instance Hook name must match the Store Hook."
|
|
@@ -306,12 +308,12 @@ const bindingNaming = createRule({
|
|
|
306
308
|
}
|
|
307
309
|
const hook = getElement(declarator.id, 0);
|
|
308
310
|
const provider = getElement(declarator.id, 1);
|
|
309
|
-
const
|
|
311
|
+
const third = getElement(declarator.id, 2);
|
|
310
312
|
const explicitName = kind === "createStore" ? getCreateStoreName(node) : getBindStoreName(node);
|
|
311
313
|
const hookMatch = hook && /^use(?<name>[A-Z][A-Za-z0-9]*)$/u.exec(hook.name);
|
|
312
314
|
const providerMatch = provider && /^(?<name>[A-Z][A-Za-z0-9]*)Provider$/u.exec(provider.name);
|
|
313
|
-
const
|
|
314
|
-
const inferredName = hookMatch?.groups?.name ?? providerMatch?.groups?.name ??
|
|
315
|
+
const thirdMatch = third && (kind === "createStore" ? /^get(?<name>[A-Z][A-Za-z0-9]*)$/u.exec(third.name) : /^use(?<name>[A-Z][A-Za-z0-9]*)Instance$/u.exec(third.name));
|
|
316
|
+
const inferredName = hookMatch?.groups?.name ?? providerMatch?.groups?.name ?? thirdMatch?.groups?.name;
|
|
315
317
|
const name = explicitName ?? inferredName;
|
|
316
318
|
if (hook && (!hookMatch || name && hookMatch.groups?.name !== name)) context.report({
|
|
317
319
|
node: hook,
|
|
@@ -321,9 +323,9 @@ const bindingNaming = createRule({
|
|
|
321
323
|
node: provider,
|
|
322
324
|
messageId: "providerName"
|
|
323
325
|
});
|
|
324
|
-
if (
|
|
325
|
-
node:
|
|
326
|
-
messageId: "instanceName"
|
|
326
|
+
if (third && (!thirdMatch || name && thirdMatch.groups?.name !== name)) context.report({
|
|
327
|
+
node: third,
|
|
328
|
+
messageId: kind === "createStore" ? "getterName" : "instanceName"
|
|
327
329
|
});
|
|
328
330
|
} };
|
|
329
331
|
}
|
|
@@ -442,32 +444,51 @@ const setMutationMethods = /* @__PURE__ */ new Set([
|
|
|
442
444
|
"clear",
|
|
443
445
|
"delete"
|
|
444
446
|
]);
|
|
445
|
-
/** Build
|
|
446
|
-
function
|
|
447
|
+
/** Build a reusable index that streams dynamic call-site contexts without retaining every path. */
|
|
448
|
+
function createFunctionCallSiteContexts(edges) {
|
|
447
449
|
const incoming = /* @__PURE__ */ new Map();
|
|
448
450
|
for (const edge of edges) {
|
|
449
451
|
const existing = incoming.get(edge.callee) ?? [];
|
|
450
452
|
existing.push(edge);
|
|
451
453
|
incoming.set(edge.callee, existing);
|
|
452
454
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
455
|
+
/** Visit paths independently while retaining only the current DFS path. */
|
|
456
|
+
const some = (target, predicate) => {
|
|
457
|
+
const calls = /* @__PURE__ */ new Map();
|
|
458
|
+
const active = /* @__PURE__ */ new Set([target]);
|
|
459
|
+
const frames = [{
|
|
460
|
+
advanced: false,
|
|
461
|
+
edges: incoming.get(target) ?? [],
|
|
462
|
+
fn: target,
|
|
463
|
+
index: 0
|
|
464
|
+
}];
|
|
465
|
+
while (frames.length > 0) {
|
|
466
|
+
const frame = frames.at(-1);
|
|
467
|
+
if (!frame) break;
|
|
468
|
+
const edge = frame.edges[frame.index];
|
|
469
|
+
if (edge) {
|
|
470
|
+
frame.index += 1;
|
|
471
|
+
if (active.has(edge.caller)) continue;
|
|
472
|
+
frame.advanced = true;
|
|
473
|
+
calls.set(frame.fn, edge.site);
|
|
474
|
+
active.add(edge.caller);
|
|
475
|
+
frames.push({
|
|
476
|
+
advanced: false,
|
|
477
|
+
edges: incoming.get(edge.caller) ?? [],
|
|
478
|
+
fn: edge.caller,
|
|
479
|
+
index: 0,
|
|
480
|
+
parent: frame.fn
|
|
481
|
+
});
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (!frame.advanced && predicate(calls)) return true;
|
|
485
|
+
frames.pop();
|
|
486
|
+
active.delete(frame.fn);
|
|
487
|
+
if (frame.parent) calls.delete(frame.parent);
|
|
466
488
|
}
|
|
467
|
-
|
|
489
|
+
return false;
|
|
468
490
|
};
|
|
469
|
-
|
|
470
|
-
return contexts;
|
|
491
|
+
return { some };
|
|
471
492
|
}
|
|
472
493
|
/** Track assignment sources and resolve definitions that reach a concrete reference point. */
|
|
473
494
|
function createReferenceOriginTracker(program) {
|
|
@@ -1451,7 +1472,7 @@ const noRenderInstanceSnapshot = createRule({
|
|
|
1451
1472
|
changed = true;
|
|
1452
1473
|
}
|
|
1453
1474
|
}
|
|
1454
|
-
const
|
|
1475
|
+
const callContexts = createFunctionCallSiteContexts(localEdges.filter((edge) => rendered.has(edge.caller)));
|
|
1455
1476
|
/** Test whether an expression is a local alias of a Store instance Hook result. */
|
|
1456
1477
|
const isInstanceDerived = (input, calls, seen = /* @__PURE__ */ new Set()) => {
|
|
1457
1478
|
const node = unwrapExpression(input);
|
|
@@ -1484,7 +1505,7 @@ const noRenderInstanceSnapshot = createRule({
|
|
|
1484
1505
|
};
|
|
1485
1506
|
for (const snapshot of snapshots) {
|
|
1486
1507
|
if (!snapshot.owner || !rendered.has(snapshot.owner)) continue;
|
|
1487
|
-
if (
|
|
1508
|
+
if (callContexts.some(snapshot.owner, (calls) => {
|
|
1488
1509
|
return snapshot.kind === "instance" ? isInstanceDerived(snapshot.source, calls) : isSnapshotReaderDerived(snapshot.source, calls);
|
|
1489
1510
|
})) context.report({
|
|
1490
1511
|
node: snapshot.node,
|
|
@@ -1652,9 +1673,10 @@ const noStoreMutation = createRule({
|
|
|
1652
1673
|
site: node
|
|
1653
1674
|
});
|
|
1654
1675
|
}
|
|
1655
|
-
|
|
1676
|
+
const callContexts = createFunctionCallSiteContexts(localEdges);
|
|
1677
|
+
for (const candidate of candidates) if (candidate.owner ? callContexts.some(candidate.owner, (calls) => {
|
|
1656
1678
|
return isSnapshotDerived(candidate.expression, maxAliasDepth, /* @__PURE__ */ new Set(), calls);
|
|
1657
|
-
})) context.report({
|
|
1679
|
+
}) : isSnapshotDerived(candidate.expression, maxAliasDepth, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Map())) context.report({
|
|
1658
1680
|
node: candidate.node,
|
|
1659
1681
|
messageId: "mutation"
|
|
1660
1682
|
});
|
|
@@ -2421,7 +2443,7 @@ const rules = {
|
|
|
2421
2443
|
const plugin = {
|
|
2422
2444
|
meta: {
|
|
2423
2445
|
name: "@violetflux/eslint-plugin-kerros",
|
|
2424
|
-
version: "0.
|
|
2446
|
+
version: "0.3.1"
|
|
2425
2447
|
},
|
|
2426
2448
|
rules
|
|
2427
2449
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@violetflux/eslint-plugin-kerros",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Type-aware ESLint rules for Kerros stores.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@typescript-eslint/parser": "^8.0.0",
|
|
38
|
-
"@violetflux/kerros": "^0.
|
|
38
|
+
"@violetflux/kerros": "^0.3.0",
|
|
39
39
|
"eslint": "^9.0.0 || ^10.0.0",
|
|
40
40
|
"typescript": "^5.0.0 || >=6.0.0 <6.1.0"
|
|
41
41
|
},
|