@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 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 instanceType = getTupleMemberType(returnType, 2, node);
181
- if (name === "createStore" && !instanceType) return "createStore";
182
- if (name !== "bindStore" || !instanceType) return void 0;
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(instanceType, "storeInstanceHook") ? "bindStore" : void 0;
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 Provider binding names aligned." },
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 instance = getElement(declarator.id, 2);
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 instanceMatch = instance && /^use(?<name>[A-Z][A-Za-z0-9]*)Instance$/u.exec(instance.name);
342
- const inferredName = hookMatch?.groups?.name ?? providerMatch?.groups?.name ?? instanceMatch?.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 (instance && (!instanceMatch || name && instanceMatch.groups?.name !== name)) context.report({
353
- node: instance,
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 separate dynamic call-site contexts for one local function. */
474
- function getFunctionCallSiteContexts(target, edges) {
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
- const contexts = [];
482
- /** Trace callers independently so states from different invocation paths never merge globally. */
483
- const trace = (fn, calls, stack) => {
484
- const edgesForFunction = incoming.get(fn) ?? [];
485
- let advanced = false;
486
- for (const edge of edgesForFunction) {
487
- if (stack.has(edge.caller)) continue;
488
- advanced = true;
489
- const nextCalls = new Map(calls);
490
- nextCalls.set(fn, edge.site);
491
- const nextStack = new Set(stack);
492
- nextStack.add(edge.caller);
493
- trace(edge.caller, nextCalls, nextStack);
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
- if (!advanced) contexts.push(calls);
517
+ return false;
496
518
  };
497
- trace(target, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set([target]));
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 renderedEdges = localEdges.filter((edge) => rendered.has(edge.caller));
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 (getFunctionCallSiteContexts(snapshot.owner, renderedEdges).some((calls) => {
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
- for (const candidate of candidates) if ((candidate.owner ? getFunctionCallSiteContexts(candidate.owner, localEdges) : [/* @__PURE__ */ new Map()]).some((calls) => {
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.2.4"
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 instanceType = getTupleMemberType(returnType, 2, node);
153
- if (name === "createStore" && !instanceType) return "createStore";
154
- if (name !== "bindStore" || !instanceType) return void 0;
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(instanceType, "storeInstanceHook") ? "bindStore" : void 0;
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 Provider binding names aligned." },
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 instance = getElement(declarator.id, 2);
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 instanceMatch = instance && /^use(?<name>[A-Z][A-Za-z0-9]*)Instance$/u.exec(instance.name);
314
- const inferredName = hookMatch?.groups?.name ?? providerMatch?.groups?.name ?? instanceMatch?.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 (instance && (!instanceMatch || name && instanceMatch.groups?.name !== name)) context.report({
325
- node: instance,
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 separate dynamic call-site contexts for one local function. */
446
- function getFunctionCallSiteContexts(target, edges) {
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
- const contexts = [];
454
- /** Trace callers independently so states from different invocation paths never merge globally. */
455
- const trace = (fn, calls, stack) => {
456
- const edgesForFunction = incoming.get(fn) ?? [];
457
- let advanced = false;
458
- for (const edge of edgesForFunction) {
459
- if (stack.has(edge.caller)) continue;
460
- advanced = true;
461
- const nextCalls = new Map(calls);
462
- nextCalls.set(fn, edge.site);
463
- const nextStack = new Set(stack);
464
- nextStack.add(edge.caller);
465
- trace(edge.caller, nextCalls, nextStack);
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
- if (!advanced) contexts.push(calls);
489
+ return false;
468
490
  };
469
- trace(target, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set([target]));
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 renderedEdges = localEdges.filter((edge) => rendered.has(edge.caller));
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 (getFunctionCallSiteContexts(snapshot.owner, renderedEdges).some((calls) => {
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
- for (const candidate of candidates) if ((candidate.owner ? getFunctionCallSiteContexts(candidate.owner, localEdges) : [/* @__PURE__ */ new Map()]).some((calls) => {
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.2.4"
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.2.4",
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.2.4",
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
  },