@violetflux/eslint-plugin-kerros 0.2.2 → 0.2.3

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.
Files changed (3) hide show
  1. package/dist/index.cjs +514 -508
  2. package/dist/index.mjs +514 -508
  3. package/package.json +2 -2
package/dist/index.cjs CHANGED
@@ -448,28 +448,265 @@ const modelConvention = createRule({
448
448
  }
449
449
  });
450
450
  //#endregion
451
+ //#region src/internal/semantic.ts
452
+ const arrayMutationMethods = /* @__PURE__ */ new Set([
453
+ "copyWithin",
454
+ "fill",
455
+ "pop",
456
+ "push",
457
+ "reverse",
458
+ "shift",
459
+ "sort",
460
+ "splice",
461
+ "unshift"
462
+ ]);
463
+ const mapMutationMethods = /* @__PURE__ */ new Set([
464
+ "clear",
465
+ "delete",
466
+ "set"
467
+ ]);
468
+ const setMutationMethods = /* @__PURE__ */ new Set([
469
+ "add",
470
+ "clear",
471
+ "delete"
472
+ ]);
473
+ /** Build separate dynamic call-site contexts for one local function. */
474
+ function getFunctionCallSiteContexts(target, edges) {
475
+ const incoming = /* @__PURE__ */ new Map();
476
+ for (const edge of edges) {
477
+ const existing = incoming.get(edge.callee) ?? [];
478
+ existing.push(edge);
479
+ incoming.set(edge.callee, existing);
480
+ }
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);
494
+ }
495
+ if (!advanced) contexts.push(calls);
496
+ };
497
+ trace(target, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set([target]));
498
+ return contexts;
499
+ }
500
+ /** Track assignment sources and resolve definitions that reach a concrete reference point. */
501
+ function createReferenceOriginTracker(program) {
502
+ const events = /* @__PURE__ */ new Map();
503
+ /** Find the function execution scope containing one syntax node. */
504
+ const getOwner = (input) => {
505
+ let node = input.parent;
506
+ while (node) {
507
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression") return node;
508
+ node = node.parent;
509
+ }
510
+ };
511
+ /** Test whether one syntax range contains another. */
512
+ const contains = (container, target) => {
513
+ return container.range[0] <= target.range[0] && container.range[1] >= target.range[1];
514
+ };
515
+ /** Merge branch states without duplicating the same reaching write. */
516
+ const merge = (left, right) => {
517
+ return [.../* @__PURE__ */ new Set([...left, ...right])];
518
+ };
519
+ /** Test whether a simple statement cannot continue into its following sibling. */
520
+ const terminates = (node) => {
521
+ if (node.type === "ReturnStatement" || node.type === "ThrowStatement") return true;
522
+ if (node.type === "BlockStatement") {
523
+ const last = node.body.at(-1);
524
+ return last ? terminates(last) : false;
525
+ }
526
+ if (node.type === "IfStatement" && node.alternate) return terminates(node.consequent) && terminates(node.alternate);
527
+ if (node.type === "LabeledStatement") return terminates(node.body);
528
+ return false;
529
+ };
530
+ /** Record one initializer or assignment after its right-hand side is evaluated. */
531
+ const record = (symbol, source, write) => {
532
+ const existing = events.get(symbol) ?? [];
533
+ existing.push({
534
+ owner: getOwner(write),
535
+ source,
536
+ write
537
+ });
538
+ events.set(symbol, existing);
539
+ };
540
+ /** Resolve the possible definitions reaching one symbol reference. */
541
+ const resolve = (symbol, reference, calls) => {
542
+ const symbolEvents = events.get(symbol) ?? [];
543
+ let functions = [];
544
+ let parent = reference.parent;
545
+ while (parent) {
546
+ if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression") functions.push(parent);
547
+ parent = parent.parent;
548
+ }
549
+ functions.reverse();
550
+ if (calls && calls.size > 0) {
551
+ const dynamicFunctions = [];
552
+ const seenFunctions = /* @__PURE__ */ new Set();
553
+ let fn = getOwner(reference);
554
+ while (fn && !seenFunctions.has(fn)) {
555
+ seenFunctions.add(fn);
556
+ dynamicFunctions.unshift(fn);
557
+ const site = calls.get(fn);
558
+ fn = site ? getOwner(site) : void 0;
559
+ }
560
+ if (dynamicFunctions.length > 0) functions = dynamicFunctions;
561
+ }
562
+ /** Apply straight-line writes in runtime order up to an optional point. */
563
+ const applyEvents = (container, owner, state, limit = container.range[1]) => {
564
+ const applicable = symbolEvents.filter((event) => {
565
+ return event.owner === owner && contains(container, event.write) && event.write.range[1] <= limit;
566
+ }).sort((left, right) => {
567
+ return left.write.range[1] - right.write.range[1] || right.write.range[0] - left.write.range[0];
568
+ });
569
+ for (const event of applicable) state = [event];
570
+ return state;
571
+ };
572
+ /** Evaluate one statement completely, merging simple conditional branches. */
573
+ const flowFull = (node, owner, state) => {
574
+ if (node.type === "BlockStatement") return flowSequence(node.body, owner, state);
575
+ if (node.type !== "IfStatement") return applyEvents(node, owner, state);
576
+ const tested = applyEvents(node.test, owner, state);
577
+ const consequent = flowFull(node.consequent, owner, tested);
578
+ const alternate = node.alternate ? flowFull(node.alternate, owner, tested) : tested;
579
+ const consequentContinues = !terminates(node.consequent);
580
+ const alternateContinues = !node.alternate || !terminates(node.alternate);
581
+ if (!consequentContinues) return alternateContinues ? alternate : [];
582
+ if (!alternateContinues) return consequent;
583
+ return merge(consequent, alternate);
584
+ };
585
+ /** Evaluate one statement only until the requested reference point. */
586
+ const flowUntil = (node, owner, state, target) => {
587
+ if (node.type === "BlockStatement") return flowSequence(node.body, owner, state, target);
588
+ if (node.type !== "IfStatement") return applyEvents(node, owner, state, target.range[0]);
589
+ if (contains(node.test, target)) return applyEvents(node.test, owner, state, target.range[0]);
590
+ const tested = applyEvents(node.test, owner, state);
591
+ if (contains(node.consequent, target)) return flowUntil(node.consequent, owner, tested, target);
592
+ if (node.alternate && contains(node.alternate, target)) return flowUntil(node.alternate, owner, tested, target);
593
+ return tested;
594
+ };
595
+ /** Evaluate a lexical statement sequence, stopping before one nested target. */
596
+ function flowSequence(nodes, owner, input, target) {
597
+ let state = input;
598
+ for (const node of nodes) {
599
+ if (target && contains(node, target)) return flowUntil(node, owner, state, target);
600
+ if (target && node.range[0] >= target.range[0]) return state;
601
+ state = flowFull(node, owner, state);
602
+ }
603
+ return state;
604
+ }
605
+ /** Evaluate one program or function scope up to a nested function/reference. */
606
+ const flowScope = (root, target, state) => {
607
+ const owner = root.type === "Program" ? void 0 : root;
608
+ if (root.type === "Program") return flowSequence(root.body, owner, state, target);
609
+ return root.body.type === "BlockStatement" ? flowSequence(root.body.body, owner, state, target) : flowUntil(root.body, owner, state, target);
610
+ };
611
+ let state = [];
612
+ let root = program;
613
+ for (const fn of functions) {
614
+ state = flowScope(root, calls?.get(fn) ?? fn, state);
615
+ root = fn;
616
+ }
617
+ state = flowScope(root, reference, state);
618
+ return state.map((event) => event.source);
619
+ };
620
+ return {
621
+ record,
622
+ resolve
623
+ };
624
+ }
625
+ /** Return an inline selector from a nominal Store Hook call. */
626
+ function getInlineSelector(node, isStoreHookCall) {
627
+ if (!isStoreHookCall(node)) return void 0;
628
+ const selector = node.arguments[0];
629
+ return selector?.type === "ArrowFunctionExpression" || selector?.type === "FunctionExpression" ? selector : void 0;
630
+ }
631
+ /** Visit a syntax subtree while ignoring parser metadata and optional nested functions. */
632
+ function visitSubtree(root, visitor, skipNestedFunctions = false) {
633
+ const visit = (node) => {
634
+ if (node !== root && skipNestedFunctions && (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration")) return;
635
+ visitor(node);
636
+ for (const key of Object.keys(node)) {
637
+ if (key === "parent" || key === "range" || key === "loc") continue;
638
+ const value = node[key];
639
+ if (Array.isArray(value)) {
640
+ for (const child of value) if (child && typeof child === "object" && "type" in child) visit(child);
641
+ } else if (value && typeof value === "object" && "type" in value) visit(value);
642
+ }
643
+ };
644
+ visit(root);
645
+ }
646
+ /** Return the statically known property name for member access. */
647
+ function getMemberName(node) {
648
+ if (!node.computed && node.property.type === "Identifier") return node.property.name;
649
+ if (node.computed && node.property.type === "Literal" && typeof node.property.value === "string") return node.property.value;
650
+ }
651
+ /** Classify runtime built-ins by TypeScript's default-library declarations. */
652
+ function getBuiltinTypeKind(checker, program, inputType) {
653
+ const type = checker.getBaseConstraintOfType(inputType) ?? inputType;
654
+ if (type.isUnion() || type.isIntersection()) {
655
+ for (const member of type.types) {
656
+ const kind = getBuiltinTypeKind(checker, program, member);
657
+ if (kind) return kind;
658
+ }
659
+ return;
660
+ }
661
+ if (checker.isArrayType(type) || checker.isTupleType(type)) return "array";
662
+ if ((type.flags & typescript.default.TypeFlags.StringLike) !== 0) return "string";
663
+ for (const symbol of [type.getSymbol(), type.aliasSymbol]) {
664
+ const name = symbol?.getName();
665
+ if (!(symbol?.declarations?.some((declaration) => {
666
+ return program.isSourceFileDefaultLibrary(declaration.getSourceFile());
667
+ }) === true)) continue;
668
+ if (name === "Map" || name === "ReadonlyMap") return "map";
669
+ if (name === "Set" || name === "ReadonlySet") return "set";
670
+ if (name === "String") return "string";
671
+ }
672
+ }
673
+ /** Test whether a call invokes a known mutable collection method. */
674
+ function isMutableCollectionCall(node, checker, program, getType) {
675
+ const callee = unwrapExpression(node.callee);
676
+ if (callee.type !== "MemberExpression") return false;
677
+ const name = getMemberName(callee);
678
+ if (!name) return false;
679
+ const kind = getBuiltinTypeKind(checker, program, getType(callee.object));
680
+ if (kind === "array") return arrayMutationMethods.has(name);
681
+ if (kind === "map") return mapMutationMethods.has(name);
682
+ if (kind === "set") return setMutationMethods.has(name);
683
+ return false;
684
+ }
685
+ /** Test whether TypeScript proves a value is primitive across unions and constraints. */
686
+ function isPrimitiveType(checker, inputType) {
687
+ const type = checker.getBaseConstraintOfType(inputType) ?? inputType;
688
+ if (type.isUnion()) return type.types.every((member) => isPrimitiveType(checker, member));
689
+ if (type.isIntersection()) return type.types.some((member) => isPrimitiveType(checker, member));
690
+ const primitiveFlags = typescript.default.TypeFlags.StringLike | typescript.default.TypeFlags.NumberLike | typescript.default.TypeFlags.BigIntLike | typescript.default.TypeFlags.BooleanLike | typescript.default.TypeFlags.ESSymbolLike | typescript.default.TypeFlags.Null | typescript.default.TypeFlags.Undefined | typescript.default.TypeFlags.Void | typescript.default.TypeFlags.Never;
691
+ return (type.flags & primitiveFlags) !== 0;
692
+ }
693
+ //#endregion
451
694
  //#region src/rules/no-broad-store-access.ts
452
695
  const objectEnumerationMethods = /* @__PURE__ */ new Set([
453
696
  "entries",
454
697
  "keys",
455
698
  "values"
456
699
  ]);
457
- /** Test whether a call enumerates or serializes its Store snapshot argument. */
458
- function isBroadConsumer(parent, node) {
459
- if (parent?.type !== "CallExpression" || !parent.arguments.includes(node)) return false;
460
- const { callee } = parent;
461
- if (callee.type !== "MemberExpression" || callee.computed) return false;
700
+ /** Read the snapshot argument from a complete enumeration or serialization call. */
701
+ function getBroadArgument(node) {
702
+ const [argument] = node.arguments;
703
+ if (!argument || argument.type === "SpreadElement") return false;
704
+ const { callee } = node;
705
+ if (callee.type !== "MemberExpression" || callee.computed) return;
462
706
  if (callee.object.type === "Identifier" && callee.property.type === "Identifier") {
463
- if (callee.object.name === "Object" && objectEnumerationMethods.has(callee.property.name)) return true;
464
- return callee.object.name === "JSON" && callee.property.name === "stringify";
707
+ if (callee.object.name === "Object" && objectEnumerationMethods.has(callee.property.name)) return argument;
708
+ if (callee.object.name === "JSON" && callee.property.name === "stringify") return argument;
465
709
  }
466
- return false;
467
- }
468
- /** Test whether object syntax expands the complete Store snapshot. */
469
- function isBroadSyntax(parent, node) {
470
- if (parent?.type === "SpreadElement" && parent.argument === node) return true;
471
- if (parent?.type !== "VariableDeclarator" || parent.init !== node || parent.id.type !== "ObjectPattern") return false;
472
- return parent.id.properties.some((property) => property.type === "RestElement");
473
710
  }
474
711
  const noBroadStoreAccess = createRule({
475
712
  name: "no-broad-store-access",
@@ -481,14 +718,77 @@ const noBroadStoreAccess = createRule({
481
718
  },
482
719
  defaultOptions: [],
483
720
  create(context) {
484
- const { isStoreHookCall } = createKerrosTypeTools(context);
485
- return { CallExpression(node) {
486
- if (node.arguments.length > 0 || !isStoreHookCall(node)) return;
487
- if (isBroadSyntax(node.parent, node) || isBroadConsumer(node.parent, node)) context.report({
488
- node,
721
+ const { getIdentifierSymbol, getType, isStoreHookCall } = createKerrosTypeTools(context);
722
+ const origins = createReferenceOriginTracker(context.sourceCode.ast);
723
+ /** Test whether an expression originates from a selector-free Store snapshot. */
724
+ const isSnapshotDerived = (input, seen = /* @__PURE__ */ new Set()) => {
725
+ const node = unwrapExpression(input);
726
+ if (node.type === "CallExpression") {
727
+ const selector = node.arguments[0];
728
+ return (node.arguments.length === 0 || node.arguments.length === 1 && selector?.type !== "SpreadElement" && (getType(selector).flags & typescript.default.TypeFlags.Undefined) !== 0) && isStoreHookCall(node);
729
+ }
730
+ if (node.type === "AssignmentExpression") return isSnapshotDerived(node.right, seen);
731
+ if (node.type === "MemberExpression") return isSnapshotDerived(node.object, seen);
732
+ if (node.type !== "Identifier") return false;
733
+ const symbol = getIdentifierSymbol(node);
734
+ if (!symbol || seen.has(symbol)) return false;
735
+ seen.add(symbol);
736
+ const derived = origins.resolve(symbol, node).some((source) => isSnapshotDerived(source, seen));
737
+ seen.delete(symbol);
738
+ return derived;
739
+ };
740
+ /** Report one operation that subscribes to every enumerable field. */
741
+ const reportBroadAccess = (expression) => {
742
+ if (isSnapshotDerived(expression)) context.report({
743
+ node: expression,
489
744
  messageId: "broadAccess"
490
745
  });
491
- } };
746
+ };
747
+ /** Track object-valued bindings destructured from a snapshot. */
748
+ const recordObjectBindings = (pattern, source, write) => {
749
+ if (pattern.type === "Identifier") {
750
+ if ((getType(pattern).flags & typescript.default.TypeFlags.Object) === 0) return;
751
+ const symbol = getIdentifierSymbol(pattern);
752
+ if (symbol) origins.record(symbol, source, write);
753
+ return;
754
+ }
755
+ if (pattern.type === "AssignmentPattern") {
756
+ recordObjectBindings(pattern.left, source, write);
757
+ return;
758
+ }
759
+ if (pattern.type === "RestElement") return;
760
+ if (pattern.type !== "ObjectPattern" && pattern.type !== "ArrayPattern") return;
761
+ const entries = pattern.type === "ObjectPattern" ? pattern.properties : pattern.elements;
762
+ for (const entry of entries) {
763
+ if (!entry) continue;
764
+ if (entry.type === "Property") recordObjectBindings(entry.value, source, write);
765
+ else recordObjectBindings(entry, source, write);
766
+ }
767
+ };
768
+ return {
769
+ CallExpression(node) {
770
+ const argument = getBroadArgument(node);
771
+ if (argument) reportBroadAccess(argument);
772
+ },
773
+ SpreadElement(node) {
774
+ reportBroadAccess(node.argument);
775
+ },
776
+ VariableDeclarator(node) {
777
+ if (!node.init) return;
778
+ if (node.id.type === "Identifier") {
779
+ const symbol = getIdentifierSymbol(node.id);
780
+ if (symbol) origins.record(symbol, node.init, node);
781
+ return;
782
+ }
783
+ if (node.id.type === "ObjectPattern" && node.id.properties.some((property) => property.type === "RestElement")) reportBroadAccess(node.init);
784
+ recordObjectBindings(node.id, node.init, node);
785
+ },
786
+ AssignmentExpression(node) {
787
+ if (node.left.type !== "Identifier") return;
788
+ const symbol = getIdentifierSymbol(node.left);
789
+ if (symbol) origins.record(symbol, node.right, node);
790
+ }
791
+ };
492
792
  }
493
793
  });
494
794
  //#endregion
@@ -631,350 +931,107 @@ function getStronglyConnectedComponents(adjacency) {
631
931
  while (stack.length > 0) {
632
932
  const node = stack.pop();
633
933
  if (node === void 0) continue;
634
- component.push(node);
635
- for (const neighbor of reverse[node]) if (visited[neighbor] === 0) {
636
- visited[neighbor] = 1;
637
- stack.push(neighbor);
638
- }
639
- }
640
- components.push(component);
641
- }
642
- return components;
643
- }
644
- /** Compare dependency sites by file and source position for stable diagnostics. */
645
- function compareDependencies(left, right) {
646
- const leftFile = left.site.getSourceFile().fileName;
647
- const rightFile = right.site.getSourceFile().fileName;
648
- return leftFile.localeCompare(rightFile) || left.site.getStart() - right.site.getStart() || left.target.id - right.target.id;
649
- }
650
- /** Select bounded, deterministic diagnostics from cyclic graph components. */
651
- function collectCyclicDependencies(nodes, dependencies) {
652
- const adjacencySets = Array.from({ length: nodes.length }, () => /* @__PURE__ */ new Set());
653
- const dependenciesBySource = /* @__PURE__ */ new Map();
654
- for (const dependency of dependencies) {
655
- adjacencySets[dependency.source.id].add(dependency.target.id);
656
- const existing = dependenciesBySource.get(dependency.source.id) ?? [];
657
- existing.push(dependency);
658
- dependenciesBySource.set(dependency.source.id, existing);
659
- }
660
- const components = getStronglyConnectedComponents(adjacencySets.map((targets) => [...targets]));
661
- const cyclicDependencies = [];
662
- for (const component of components) {
663
- if (!(component.length > 1 || component[0] !== void 0 && adjacencySets[component[0]].has(component[0]))) continue;
664
- const members = new Set(component);
665
- component.sort((left, right) => left - right);
666
- for (const source of component) {
667
- const dependency = (dependenciesBySource.get(source) ?? []).filter((candidate) => members.has(candidate.target.id)).sort(compareDependencies)[0];
668
- if (dependency) cyclicDependencies.push(dependency);
669
- }
670
- }
671
- return cyclicDependencies.sort(compareDependencies);
672
- }
673
- /** Build and cache the complete Store graph once for a TypeScript Program. */
674
- function getProgramStoreGraph(program) {
675
- const cached = programGraphCache.get(program);
676
- if (cached) return cached;
677
- const { nodes, tools } = collectStoreNodes(program);
678
- const storesByHook = new Map(nodes.map((node) => [node.hook, node]));
679
- const graph = { cyclicDependencies: collectCyclicDependencies(nodes, nodes.flatMap((node) => {
680
- return node.kind === "createStore" ? collectModelDependencies(node, storesByHook, tools) : [];
681
- })) };
682
- programGraphCache.set(program, graph);
683
- return graph;
684
- }
685
- /** Read cached cyclic dependency sites belonging to one current source file. */
686
- function getCyclicStoreDependencies(program, sourceFile) {
687
- let sourceFiles = sourceDependencyCache.get(program);
688
- if (!sourceFiles) {
689
- sourceFiles = /* @__PURE__ */ new WeakMap();
690
- sourceDependencyCache.set(program, sourceFiles);
691
- }
692
- const cached = sourceFiles.get(sourceFile);
693
- if (cached) return cached;
694
- const dependencies = getProgramStoreGraph(program).cyclicDependencies.filter((dependency) => dependency.site.getSourceFile() === sourceFile).map((dependency) => ({
695
- site: dependency.site,
696
- source: dependency.source.name,
697
- target: dependency.target.name
698
- }));
699
- sourceFiles.set(sourceFile, dependencies);
700
- return dependencies;
701
- }
702
- //#endregion
703
- //#region src/rules/no-cyclic-store-dependency.ts
704
- const noCyclicStoreDependency = createRule({
705
- name: "no-cyclic-store-dependency",
706
- meta: {
707
- type: "problem",
708
- docs: { description: "Prevent createStore models from forming Store dependency cycles." },
709
- schema: [],
710
- messages: { cyclicDependency: "Store \"{{source}}\" depends on \"{{target}}\" in a dependency cycle." }
711
- },
712
- defaultOptions: [],
713
- create(context) {
714
- const services = getTypeServices(context);
715
- const program = services.program;
716
- const sourceFile = services.esTreeNodeToTSNodeMap.get(context.sourceCode.ast);
717
- return { "Program:exit"() {
718
- if (!typescript.default.isSourceFile(sourceFile)) return;
719
- for (const dependency of getCyclicStoreDependencies(program, sourceFile)) {
720
- const node = services.tsNodeToESTreeNodeMap.get(dependency.site);
721
- if (!node) continue;
722
- context.report({
723
- node,
724
- messageId: "cyclicDependency",
725
- data: {
726
- source: dependency.source,
727
- target: dependency.target
728
- }
729
- });
730
- }
731
- } };
732
- }
733
- });
734
- //#endregion
735
- //#region src/internal/semantic.ts
736
- const arrayMutationMethods = /* @__PURE__ */ new Set([
737
- "copyWithin",
738
- "fill",
739
- "pop",
740
- "push",
741
- "reverse",
742
- "shift",
743
- "sort",
744
- "splice",
745
- "unshift"
746
- ]);
747
- const mapMutationMethods = /* @__PURE__ */ new Set([
748
- "clear",
749
- "delete",
750
- "set"
751
- ]);
752
- const setMutationMethods = /* @__PURE__ */ new Set([
753
- "add",
754
- "clear",
755
- "delete"
756
- ]);
757
- /** Build separate dynamic call-site contexts for one local function. */
758
- function getFunctionCallSiteContexts(target, edges) {
759
- const incoming = /* @__PURE__ */ new Map();
760
- for (const edge of edges) {
761
- const existing = incoming.get(edge.callee) ?? [];
762
- existing.push(edge);
763
- incoming.set(edge.callee, existing);
764
- }
765
- const contexts = [];
766
- /** Trace callers independently so states from different invocation paths never merge globally. */
767
- const trace = (fn, calls, stack) => {
768
- const edgesForFunction = incoming.get(fn) ?? [];
769
- let advanced = false;
770
- for (const edge of edgesForFunction) {
771
- if (stack.has(edge.caller)) continue;
772
- advanced = true;
773
- const nextCalls = new Map(calls);
774
- nextCalls.set(fn, edge.site);
775
- const nextStack = new Set(stack);
776
- nextStack.add(edge.caller);
777
- trace(edge.caller, nextCalls, nextStack);
778
- }
779
- if (!advanced) contexts.push(calls);
780
- };
781
- trace(target, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set([target]));
782
- return contexts;
783
- }
784
- /** Track assignment sources and resolve definitions that reach a concrete reference point. */
785
- function createReferenceOriginTracker(program) {
786
- const events = /* @__PURE__ */ new Map();
787
- /** Find the function execution scope containing one syntax node. */
788
- const getOwner = (input) => {
789
- let node = input.parent;
790
- while (node) {
791
- if (node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression") return node;
792
- node = node.parent;
793
- }
794
- };
795
- /** Test whether one syntax range contains another. */
796
- const contains = (container, target) => {
797
- return container.range[0] <= target.range[0] && container.range[1] >= target.range[1];
798
- };
799
- /** Merge branch states without duplicating the same reaching write. */
800
- const merge = (left, right) => {
801
- return [.../* @__PURE__ */ new Set([...left, ...right])];
802
- };
803
- /** Test whether a simple statement cannot continue into its following sibling. */
804
- const terminates = (node) => {
805
- if (node.type === "ReturnStatement" || node.type === "ThrowStatement") return true;
806
- if (node.type === "BlockStatement") {
807
- const last = node.body.at(-1);
808
- return last ? terminates(last) : false;
809
- }
810
- if (node.type === "IfStatement" && node.alternate) return terminates(node.consequent) && terminates(node.alternate);
811
- if (node.type === "LabeledStatement") return terminates(node.body);
812
- return false;
813
- };
814
- /** Record one initializer or assignment after its right-hand side is evaluated. */
815
- const record = (symbol, source, write) => {
816
- const existing = events.get(symbol) ?? [];
817
- existing.push({
818
- owner: getOwner(write),
819
- source,
820
- write
821
- });
822
- events.set(symbol, existing);
823
- };
824
- /** Resolve the possible definitions reaching one symbol reference. */
825
- const resolve = (symbol, reference, calls) => {
826
- const symbolEvents = events.get(symbol) ?? [];
827
- let functions = [];
828
- let parent = reference.parent;
829
- while (parent) {
830
- if (parent.type === "ArrowFunctionExpression" || parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression") functions.push(parent);
831
- parent = parent.parent;
832
- }
833
- functions.reverse();
834
- if (calls && calls.size > 0) {
835
- const dynamicFunctions = [];
836
- const seenFunctions = /* @__PURE__ */ new Set();
837
- let fn = getOwner(reference);
838
- while (fn && !seenFunctions.has(fn)) {
839
- seenFunctions.add(fn);
840
- dynamicFunctions.unshift(fn);
841
- const site = calls.get(fn);
842
- fn = site ? getOwner(site) : void 0;
843
- }
844
- if (dynamicFunctions.length > 0) functions = dynamicFunctions;
845
- }
846
- /** Apply straight-line writes in runtime order up to an optional point. */
847
- const applyEvents = (container, owner, state, limit = container.range[1]) => {
848
- const applicable = symbolEvents.filter((event) => {
849
- return event.owner === owner && contains(container, event.write) && event.write.range[1] <= limit;
850
- }).sort((left, right) => {
851
- return left.write.range[1] - right.write.range[1] || right.write.range[0] - left.write.range[0];
852
- });
853
- for (const event of applicable) state = [event];
854
- return state;
855
- };
856
- /** Evaluate one statement completely, merging simple conditional branches. */
857
- const flowFull = (node, owner, state) => {
858
- if (node.type === "BlockStatement") return flowSequence(node.body, owner, state);
859
- if (node.type !== "IfStatement") return applyEvents(node, owner, state);
860
- const tested = applyEvents(node.test, owner, state);
861
- const consequent = flowFull(node.consequent, owner, tested);
862
- const alternate = node.alternate ? flowFull(node.alternate, owner, tested) : tested;
863
- const consequentContinues = !terminates(node.consequent);
864
- const alternateContinues = !node.alternate || !terminates(node.alternate);
865
- if (!consequentContinues) return alternateContinues ? alternate : [];
866
- if (!alternateContinues) return consequent;
867
- return merge(consequent, alternate);
868
- };
869
- /** Evaluate one statement only until the requested reference point. */
870
- const flowUntil = (node, owner, state, target) => {
871
- if (node.type === "BlockStatement") return flowSequence(node.body, owner, state, target);
872
- if (node.type !== "IfStatement") return applyEvents(node, owner, state, target.range[0]);
873
- if (contains(node.test, target)) return applyEvents(node.test, owner, state, target.range[0]);
874
- const tested = applyEvents(node.test, owner, state);
875
- if (contains(node.consequent, target)) return flowUntil(node.consequent, owner, tested, target);
876
- if (node.alternate && contains(node.alternate, target)) return flowUntil(node.alternate, owner, tested, target);
877
- return tested;
878
- };
879
- /** Evaluate a lexical statement sequence, stopping before one nested target. */
880
- function flowSequence(nodes, owner, input, target) {
881
- let state = input;
882
- for (const node of nodes) {
883
- if (target && contains(node, target)) return flowUntil(node, owner, state, target);
884
- if (target && node.range[0] >= target.range[0]) return state;
885
- state = flowFull(node, owner, state);
934
+ component.push(node);
935
+ for (const neighbor of reverse[node]) if (visited[neighbor] === 0) {
936
+ visited[neighbor] = 1;
937
+ stack.push(neighbor);
886
938
  }
887
- return state;
888
- }
889
- /** Evaluate one program or function scope up to a nested function/reference. */
890
- const flowScope = (root, target, state) => {
891
- const owner = root.type === "Program" ? void 0 : root;
892
- if (root.type === "Program") return flowSequence(root.body, owner, state, target);
893
- return root.body.type === "BlockStatement" ? flowSequence(root.body.body, owner, state, target) : flowUntil(root.body, owner, state, target);
894
- };
895
- let state = [];
896
- let root = program;
897
- for (const fn of functions) {
898
- state = flowScope(root, calls?.get(fn) ?? fn, state);
899
- root = fn;
900
- }
901
- state = flowScope(root, reference, state);
902
- return state.map((event) => event.source);
903
- };
904
- return {
905
- record,
906
- resolve
907
- };
908
- }
909
- /** Return an inline selector from a nominal Store Hook call. */
910
- function getInlineSelector(node, isStoreHookCall) {
911
- if (!isStoreHookCall(node)) return void 0;
912
- const selector = node.arguments[0];
913
- return selector?.type === "ArrowFunctionExpression" || selector?.type === "FunctionExpression" ? selector : void 0;
914
- }
915
- /** Visit a syntax subtree while ignoring parser metadata and optional nested functions. */
916
- function visitSubtree(root, visitor, skipNestedFunctions = false) {
917
- const visit = (node) => {
918
- if (node !== root && skipNestedFunctions && (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration")) return;
919
- visitor(node);
920
- for (const key of Object.keys(node)) {
921
- if (key === "parent" || key === "range" || key === "loc") continue;
922
- const value = node[key];
923
- if (Array.isArray(value)) {
924
- for (const child of value) if (child && typeof child === "object" && "type" in child) visit(child);
925
- } else if (value && typeof value === "object" && "type" in value) visit(value);
926
939
  }
927
- };
928
- visit(root);
940
+ components.push(component);
941
+ }
942
+ return components;
929
943
  }
930
- /** Return the statically known property name for member access. */
931
- function getMemberName(node) {
932
- if (!node.computed && node.property.type === "Identifier") return node.property.name;
933
- if (node.computed && node.property.type === "Literal" && typeof node.property.value === "string") return node.property.value;
944
+ /** Compare dependency sites by file and source position for stable diagnostics. */
945
+ function compareDependencies(left, right) {
946
+ const leftFile = left.site.getSourceFile().fileName;
947
+ const rightFile = right.site.getSourceFile().fileName;
948
+ return leftFile.localeCompare(rightFile) || left.site.getStart() - right.site.getStart() || left.target.id - right.target.id;
934
949
  }
935
- /** Classify runtime built-ins by TypeScript's default-library declarations. */
936
- function getBuiltinTypeKind(checker, program, inputType) {
937
- const type = checker.getBaseConstraintOfType(inputType) ?? inputType;
938
- if (type.isUnion() || type.isIntersection()) {
939
- for (const member of type.types) {
940
- const kind = getBuiltinTypeKind(checker, program, member);
941
- if (kind) return kind;
942
- }
943
- return;
950
+ /** Select bounded, deterministic diagnostics from cyclic graph components. */
951
+ function collectCyclicDependencies(nodes, dependencies) {
952
+ const adjacencySets = Array.from({ length: nodes.length }, () => /* @__PURE__ */ new Set());
953
+ const dependenciesBySource = /* @__PURE__ */ new Map();
954
+ for (const dependency of dependencies) {
955
+ adjacencySets[dependency.source.id].add(dependency.target.id);
956
+ const existing = dependenciesBySource.get(dependency.source.id) ?? [];
957
+ existing.push(dependency);
958
+ dependenciesBySource.set(dependency.source.id, existing);
944
959
  }
945
- if (checker.isArrayType(type) || checker.isTupleType(type)) return "array";
946
- if ((type.flags & typescript.default.TypeFlags.StringLike) !== 0) return "string";
947
- for (const symbol of [type.getSymbol(), type.aliasSymbol]) {
948
- const name = symbol?.getName();
949
- if (!(symbol?.declarations?.some((declaration) => {
950
- return program.isSourceFileDefaultLibrary(declaration.getSourceFile());
951
- }) === true)) continue;
952
- if (name === "Map" || name === "ReadonlyMap") return "map";
953
- if (name === "Set" || name === "ReadonlySet") return "set";
954
- if (name === "String") return "string";
960
+ const components = getStronglyConnectedComponents(adjacencySets.map((targets) => [...targets]));
961
+ const cyclicDependencies = [];
962
+ for (const component of components) {
963
+ if (!(component.length > 1 || component[0] !== void 0 && adjacencySets[component[0]].has(component[0]))) continue;
964
+ const members = new Set(component);
965
+ component.sort((left, right) => left - right);
966
+ for (const source of component) {
967
+ const dependency = (dependenciesBySource.get(source) ?? []).filter((candidate) => members.has(candidate.target.id)).sort(compareDependencies)[0];
968
+ if (dependency) cyclicDependencies.push(dependency);
969
+ }
955
970
  }
971
+ return cyclicDependencies.sort(compareDependencies);
956
972
  }
957
- /** Test whether a call invokes a known mutable collection method. */
958
- function isMutableCollectionCall(node, checker, program, getType) {
959
- const callee = unwrapExpression(node.callee);
960
- if (callee.type !== "MemberExpression") return false;
961
- const name = getMemberName(callee);
962
- if (!name) return false;
963
- const kind = getBuiltinTypeKind(checker, program, getType(callee.object));
964
- if (kind === "array") return arrayMutationMethods.has(name);
965
- if (kind === "map") return mapMutationMethods.has(name);
966
- if (kind === "set") return setMutationMethods.has(name);
967
- return false;
973
+ /** Build and cache the complete Store graph once for a TypeScript Program. */
974
+ function getProgramStoreGraph(program) {
975
+ const cached = programGraphCache.get(program);
976
+ if (cached) return cached;
977
+ const { nodes, tools } = collectStoreNodes(program);
978
+ const storesByHook = new Map(nodes.map((node) => [node.hook, node]));
979
+ const graph = { cyclicDependencies: collectCyclicDependencies(nodes, nodes.flatMap((node) => {
980
+ return node.kind === "createStore" ? collectModelDependencies(node, storesByHook, tools) : [];
981
+ })) };
982
+ programGraphCache.set(program, graph);
983
+ return graph;
968
984
  }
969
- /** Test whether TypeScript proves a value is primitive across unions and constraints. */
970
- function isPrimitiveType(checker, inputType) {
971
- const type = checker.getBaseConstraintOfType(inputType) ?? inputType;
972
- if (type.isUnion()) return type.types.every((member) => isPrimitiveType(checker, member));
973
- if (type.isIntersection()) return type.types.some((member) => isPrimitiveType(checker, member));
974
- const primitiveFlags = typescript.default.TypeFlags.StringLike | typescript.default.TypeFlags.NumberLike | typescript.default.TypeFlags.BigIntLike | typescript.default.TypeFlags.BooleanLike | typescript.default.TypeFlags.ESSymbolLike | typescript.default.TypeFlags.Null | typescript.default.TypeFlags.Undefined | typescript.default.TypeFlags.Void | typescript.default.TypeFlags.Never;
975
- return (type.flags & primitiveFlags) !== 0;
985
+ /** Read cached cyclic dependency sites belonging to one current source file. */
986
+ function getCyclicStoreDependencies(program, sourceFile) {
987
+ let sourceFiles = sourceDependencyCache.get(program);
988
+ if (!sourceFiles) {
989
+ sourceFiles = /* @__PURE__ */ new WeakMap();
990
+ sourceDependencyCache.set(program, sourceFiles);
991
+ }
992
+ const cached = sourceFiles.get(sourceFile);
993
+ if (cached) return cached;
994
+ const dependencies = getProgramStoreGraph(program).cyclicDependencies.filter((dependency) => dependency.site.getSourceFile() === sourceFile).map((dependency) => ({
995
+ site: dependency.site,
996
+ source: dependency.source.name,
997
+ target: dependency.target.name
998
+ }));
999
+ sourceFiles.set(sourceFile, dependencies);
1000
+ return dependencies;
976
1001
  }
977
1002
  //#endregion
1003
+ //#region src/rules/no-cyclic-store-dependency.ts
1004
+ const noCyclicStoreDependency = createRule({
1005
+ name: "no-cyclic-store-dependency",
1006
+ meta: {
1007
+ type: "problem",
1008
+ docs: { description: "Prevent createStore models from forming Store dependency cycles." },
1009
+ schema: [],
1010
+ messages: { cyclicDependency: "Store \"{{source}}\" depends on \"{{target}}\" in a dependency cycle." }
1011
+ },
1012
+ defaultOptions: [],
1013
+ create(context) {
1014
+ const services = getTypeServices(context);
1015
+ const program = services.program;
1016
+ const sourceFile = services.esTreeNodeToTSNodeMap.get(context.sourceCode.ast);
1017
+ return { "Program:exit"() {
1018
+ if (!typescript.default.isSourceFile(sourceFile)) return;
1019
+ for (const dependency of getCyclicStoreDependencies(program, sourceFile)) {
1020
+ const node = services.tsNodeToESTreeNodeMap.get(dependency.site);
1021
+ if (!node) continue;
1022
+ context.report({
1023
+ node,
1024
+ messageId: "cyclicDependency",
1025
+ data: {
1026
+ source: dependency.source,
1027
+ target: dependency.target
1028
+ }
1029
+ });
1030
+ }
1031
+ } };
1032
+ }
1033
+ });
1034
+ //#endregion
978
1035
  //#region src/internal/typescript.ts
979
1036
  /** Remove TypeScript expression wrappers that preserve runtime identity. */
980
1037
  function unwrapTsExpression(input) {
@@ -2234,150 +2291,6 @@ function isInsideFunction(node, owner) {
2234
2291
  }
2235
2292
  return false;
2236
2293
  }
2237
- const requireCachedSnapshot = createRule({
2238
- name: "require-cached-snapshot",
2239
- meta: {
2240
- type: "problem",
2241
- docs: { description: "Require bindStore snapshots to preserve reference identity between updates." },
2242
- schema: [],
2243
- messages: { uncachedSnapshot: "getSnapshot must return a cached snapshot reference." }
2244
- },
2245
- defaultOptions: [],
2246
- create(context) {
2247
- const { checker, getFactoryKind, getMarkerType, getTsNode, getTsSymbol } = createKerrosTypeTools(context);
2248
- /** Resolve property functions through methods, arrow properties, and shorthand identifiers. */
2249
- const getImplementations = (symbol) => {
2250
- const implementations = /* @__PURE__ */ new Set();
2251
- const seen = /* @__PURE__ */ new Set();
2252
- /** Follow one syntax node to a concrete function body or referenced symbol. */
2253
- const resolveNode = (node) => {
2254
- if (typescript.default.isFunctionDeclaration(node) || typescript.default.isMethodDeclaration(node) || typescript.default.isGetAccessorDeclaration(node) || typescript.default.isArrowFunction(node) || typescript.default.isFunctionExpression(node)) {
2255
- if (node.body) implementations.add(node);
2256
- return;
2257
- }
2258
- if ((typescript.default.isVariableDeclaration(node) || typescript.default.isPropertyDeclaration(node)) && node.initializer) {
2259
- resolveNode(node.initializer);
2260
- return;
2261
- }
2262
- if (typescript.default.isPropertyAssignment(node)) {
2263
- resolveNode(node.initializer);
2264
- return;
2265
- }
2266
- if (typescript.default.isShorthandPropertyAssignment(node)) {
2267
- const value = checker.getShorthandAssignmentValueSymbol(node);
2268
- if (value) resolveSymbol(value);
2269
- return;
2270
- }
2271
- if (typescript.default.isIdentifier(node)) {
2272
- const value = getTsSymbol(node);
2273
- if (value) resolveSymbol(value);
2274
- return;
2275
- }
2276
- if (typescript.default.isParenthesizedExpression(node) || typescript.default.isAsExpression(node) || typescript.default.isNonNullExpression(node) || typescript.default.isSatisfiesExpression(node)) resolveNode(node.expression);
2277
- };
2278
- /** Follow a symbol's declarations once to avoid recursive aliases. */
2279
- function resolveSymbol(candidate) {
2280
- if (seen.has(candidate)) return;
2281
- seen.add(candidate);
2282
- for (const declaration of candidate.declarations ?? []) resolveNode(declaration);
2283
- }
2284
- resolveSymbol(symbol);
2285
- return implementations;
2286
- };
2287
- /** Prove that a snapshot value is primitive or allocated outside the reader invocation. */
2288
- const isCached = (input, owner, seen = /* @__PURE__ */ new Set()) => {
2289
- const node = unwrapTsExpression(input);
2290
- const type = checker.getTypeAtLocation(node);
2291
- if (isPrimitiveType(checker, type)) return true;
2292
- if (typescript.default.isObjectLiteralExpression(node) || typescript.default.isArrayLiteralExpression(node) || typescript.default.isNewExpression(node) || typescript.default.isArrowFunction(node) || typescript.default.isFunctionExpression(node) || typescript.default.isClassExpression(node) || typescript.default.isJsxElement(node) || typescript.default.isJsxSelfClosingElement(node) || typescript.default.isJsxFragment(node) || typescript.default.isRegularExpressionLiteral(node)) return false;
2293
- if (node.kind === typescript.default.SyntaxKind.ThisKeyword) return true;
2294
- if (typescript.default.isPropertyAccessExpression(node) || typescript.default.isElementAccessExpression(node)) {
2295
- const symbol = (typescript.default.isPropertyAccessExpression(node) ? checker.getSymbolAtLocation(node.name) : checker.getSymbolAtLocation(node.argumentExpression)) ?? checker.getSymbolAtLocation(node);
2296
- const getters = symbol?.declarations?.filter(typescript.default.isGetAccessorDeclaration) ?? [];
2297
- if (getters.length > 0) {
2298
- if (!symbol || seen.has(symbol)) return false;
2299
- seen.add(symbol);
2300
- const cached = getters.every((getter) => {
2301
- const returns = getTsReturnExpressions(getter);
2302
- return returns.length > 0 && returns.every((value) => isCached(value, getter, seen));
2303
- });
2304
- seen.delete(symbol);
2305
- return cached;
2306
- }
2307
- if (symbol?.declarations?.some(typescript.default.isPropertyDeclaration)) return true;
2308
- return isCached(node.expression, owner, seen);
2309
- }
2310
- if (typescript.default.isConditionalExpression(node)) return isCached(node.whenTrue, owner, seen) && isCached(node.whenFalse, owner, seen);
2311
- if (typescript.default.isBinaryExpression(node) && (node.operatorToken.kind === typescript.default.SyntaxKind.AmpersandAmpersandToken || node.operatorToken.kind === typescript.default.SyntaxKind.BarBarToken || node.operatorToken.kind === typescript.default.SyntaxKind.QuestionQuestionToken)) return isCached(node.left, owner, seen) && isCached(node.right, owner, seen);
2312
- if (!typescript.default.isIdentifier(node)) return false;
2313
- const symbol = getTsSymbol(node);
2314
- if (!symbol || seen.has(symbol)) return false;
2315
- seen.add(symbol);
2316
- let cached = false;
2317
- for (const declaration of symbol.declarations ?? []) {
2318
- if (!isInsideFunction(declaration, owner)) {
2319
- cached = true;
2320
- break;
2321
- }
2322
- if (typescript.default.isVariableDeclaration(declaration) && declaration.initializer) {
2323
- cached = isCached(declaration.initializer, owner, seen);
2324
- if (cached) break;
2325
- }
2326
- }
2327
- seen.delete(symbol);
2328
- return cached;
2329
- };
2330
- return { CallExpression(node) {
2331
- if (getFactoryKind(node) !== "bindStore") return;
2332
- const tsNode = getTsNode(node);
2333
- if (!typescript.default.isCallExpression(tsNode)) return;
2334
- const signature = checker.getResolvedSignature(tsNode);
2335
- if (!signature) return;
2336
- const returnType = checker.getReturnTypeOfSignature(signature);
2337
- const providerProperty = checker.getPropertyOfType(returnType, "1");
2338
- const providerType = providerProperty ? checker.getTypeOfSymbolAtLocation(providerProperty, tsNode) : void 0;
2339
- const storeType = providerType ? getMarkerType(providerType, "externalStoreProvider", tsNode) : void 0;
2340
- const snapshot = storeType ? getTypeProperty(checker, storeType, "getSnapshot") : void 0;
2341
- if (!snapshot) return;
2342
- if ([...getImplementations(snapshot)].some((implementation) => {
2343
- return getTsReturnExpressions(implementation).some((value) => !isCached(value, implementation));
2344
- })) context.report({
2345
- node,
2346
- messageId: "uncachedSnapshot"
2347
- });
2348
- } };
2349
- }
2350
- });
2351
- //#endregion
2352
- //#region src/rules/require-immediate-store-access.ts
2353
- const transparentParents = /* @__PURE__ */ new Set([
2354
- "ChainExpression",
2355
- "TSAsExpression",
2356
- "TSInstantiationExpression",
2357
- "TSNonNullExpression",
2358
- "TSSatisfiesExpression",
2359
- "TSTypeAssertion"
2360
- ]);
2361
- /** Find the first parent that changes how a Store snapshot is consumed. */
2362
- function getConsumptionParent(node) {
2363
- let current = node;
2364
- let parent = current.parent;
2365
- while (parent && transparentParents.has(parent.type)) {
2366
- current = parent;
2367
- parent = current.parent;
2368
- }
2369
- return {
2370
- current,
2371
- parent
2372
- };
2373
- }
2374
- /** Test whether the snapshot is read immediately without retaining the Proxy. */
2375
- function hasImmediateAccess(node) {
2376
- const { current, parent } = getConsumptionParent(node);
2377
- if (!parent) return false;
2378
- if (parent.type === "MemberExpression" && parent.object === current) return true;
2379
- return parent.type === "VariableDeclarator" && parent.init === current && parent.id.type === "ObjectPattern";
2380
- }
2381
2294
  //#endregion
2382
2295
  //#region src/index.ts
2383
2296
  const rules = {
@@ -2395,23 +2308,116 @@ const rules = {
2395
2308
  "no-whole-store-selector": noWholeStoreSelector,
2396
2309
  "prefer-bind-store": preferBindStore,
2397
2310
  "pure-selector": pureSelector,
2398
- "require-cached-snapshot": requireCachedSnapshot,
2399
- "require-immediate-store-access": createRule({
2400
- name: "require-immediate-store-access",
2311
+ "require-cached-snapshot": createRule({
2312
+ name: "require-cached-snapshot",
2401
2313
  meta: {
2402
2314
  type: "problem",
2403
- docs: { description: "Require immediate property access for selector-free Store Hooks." },
2315
+ docs: { description: "Require bindStore snapshots to preserve reference identity between updates." },
2404
2316
  schema: [],
2405
- messages: { immediateAccess: "Immediately destructure or read a property from a selector-free Store Hook." }
2317
+ messages: { uncachedSnapshot: "getSnapshot must return a cached snapshot reference." }
2406
2318
  },
2407
2319
  defaultOptions: [],
2408
2320
  create(context) {
2409
- const { getType, isStoreHookCall } = createKerrosTypeTools(context);
2321
+ const { checker, getFactoryKind, getMarkerType, getTsNode, getTsSymbol } = createKerrosTypeTools(context);
2322
+ /** Resolve property functions through methods, arrow properties, and shorthand identifiers. */
2323
+ const getImplementations = (symbol) => {
2324
+ const implementations = /* @__PURE__ */ new Set();
2325
+ const seen = /* @__PURE__ */ new Set();
2326
+ /** Follow one syntax node to a concrete function body or referenced symbol. */
2327
+ const resolveNode = (node) => {
2328
+ if (typescript.default.isFunctionDeclaration(node) || typescript.default.isMethodDeclaration(node) || typescript.default.isGetAccessorDeclaration(node) || typescript.default.isArrowFunction(node) || typescript.default.isFunctionExpression(node)) {
2329
+ if (node.body) implementations.add(node);
2330
+ return;
2331
+ }
2332
+ if ((typescript.default.isVariableDeclaration(node) || typescript.default.isPropertyDeclaration(node)) && node.initializer) {
2333
+ resolveNode(node.initializer);
2334
+ return;
2335
+ }
2336
+ if (typescript.default.isPropertyAssignment(node)) {
2337
+ resolveNode(node.initializer);
2338
+ return;
2339
+ }
2340
+ if (typescript.default.isShorthandPropertyAssignment(node)) {
2341
+ const value = checker.getShorthandAssignmentValueSymbol(node);
2342
+ if (value) resolveSymbol(value);
2343
+ return;
2344
+ }
2345
+ if (typescript.default.isIdentifier(node)) {
2346
+ const value = getTsSymbol(node);
2347
+ if (value) resolveSymbol(value);
2348
+ return;
2349
+ }
2350
+ if (typescript.default.isParenthesizedExpression(node) || typescript.default.isAsExpression(node) || typescript.default.isNonNullExpression(node) || typescript.default.isSatisfiesExpression(node)) resolveNode(node.expression);
2351
+ };
2352
+ /** Follow a symbol's declarations once to avoid recursive aliases. */
2353
+ function resolveSymbol(candidate) {
2354
+ if (seen.has(candidate)) return;
2355
+ seen.add(candidate);
2356
+ for (const declaration of candidate.declarations ?? []) resolveNode(declaration);
2357
+ }
2358
+ resolveSymbol(symbol);
2359
+ return implementations;
2360
+ };
2361
+ /** Prove that a snapshot value is primitive or allocated outside the reader invocation. */
2362
+ const isCached = (input, owner, seen = /* @__PURE__ */ new Set()) => {
2363
+ const node = unwrapTsExpression(input);
2364
+ const type = checker.getTypeAtLocation(node);
2365
+ if (isPrimitiveType(checker, type)) return true;
2366
+ if (typescript.default.isObjectLiteralExpression(node) || typescript.default.isArrayLiteralExpression(node) || typescript.default.isNewExpression(node) || typescript.default.isArrowFunction(node) || typescript.default.isFunctionExpression(node) || typescript.default.isClassExpression(node) || typescript.default.isJsxElement(node) || typescript.default.isJsxSelfClosingElement(node) || typescript.default.isJsxFragment(node) || typescript.default.isRegularExpressionLiteral(node)) return false;
2367
+ if (node.kind === typescript.default.SyntaxKind.ThisKeyword) return true;
2368
+ if (typescript.default.isPropertyAccessExpression(node) || typescript.default.isElementAccessExpression(node)) {
2369
+ const symbol = (typescript.default.isPropertyAccessExpression(node) ? checker.getSymbolAtLocation(node.name) : checker.getSymbolAtLocation(node.argumentExpression)) ?? checker.getSymbolAtLocation(node);
2370
+ const getters = symbol?.declarations?.filter(typescript.default.isGetAccessorDeclaration) ?? [];
2371
+ if (getters.length > 0) {
2372
+ if (!symbol || seen.has(symbol)) return false;
2373
+ seen.add(symbol);
2374
+ const cached = getters.every((getter) => {
2375
+ const returns = getTsReturnExpressions(getter);
2376
+ return returns.length > 0 && returns.every((value) => isCached(value, getter, seen));
2377
+ });
2378
+ seen.delete(symbol);
2379
+ return cached;
2380
+ }
2381
+ if (symbol?.declarations?.some(typescript.default.isPropertyDeclaration)) return true;
2382
+ return isCached(node.expression, owner, seen);
2383
+ }
2384
+ if (typescript.default.isConditionalExpression(node)) return isCached(node.whenTrue, owner, seen) && isCached(node.whenFalse, owner, seen);
2385
+ if (typescript.default.isBinaryExpression(node) && (node.operatorToken.kind === typescript.default.SyntaxKind.AmpersandAmpersandToken || node.operatorToken.kind === typescript.default.SyntaxKind.BarBarToken || node.operatorToken.kind === typescript.default.SyntaxKind.QuestionQuestionToken)) return isCached(node.left, owner, seen) && isCached(node.right, owner, seen);
2386
+ if (!typescript.default.isIdentifier(node)) return false;
2387
+ const symbol = getTsSymbol(node);
2388
+ if (!symbol || seen.has(symbol)) return false;
2389
+ seen.add(symbol);
2390
+ let cached = false;
2391
+ for (const declaration of symbol.declarations ?? []) {
2392
+ if (!isInsideFunction(declaration, owner)) {
2393
+ cached = true;
2394
+ break;
2395
+ }
2396
+ if (typescript.default.isVariableDeclaration(declaration) && declaration.initializer) {
2397
+ cached = isCached(declaration.initializer, owner, seen);
2398
+ if (cached) break;
2399
+ }
2400
+ }
2401
+ seen.delete(symbol);
2402
+ return cached;
2403
+ };
2410
2404
  return { CallExpression(node) {
2411
- const selector = node.arguments[0];
2412
- if ((!selector || node.arguments.length === 1 && selector.type !== "SpreadElement" && (getType(selector).flags & typescript.default.TypeFlags.Undefined) !== 0) && isStoreHookCall(node) && !hasImmediateAccess(node)) context.report({
2405
+ if (getFactoryKind(node) !== "bindStore") return;
2406
+ const tsNode = getTsNode(node);
2407
+ if (!typescript.default.isCallExpression(tsNode)) return;
2408
+ const signature = checker.getResolvedSignature(tsNode);
2409
+ if (!signature) return;
2410
+ const returnType = checker.getReturnTypeOfSignature(signature);
2411
+ const providerProperty = checker.getPropertyOfType(returnType, "1");
2412
+ const providerType = providerProperty ? checker.getTypeOfSymbolAtLocation(providerProperty, tsNode) : void 0;
2413
+ const storeType = providerType ? getMarkerType(providerType, "externalStoreProvider", tsNode) : void 0;
2414
+ const snapshot = storeType ? getTypeProperty(checker, storeType, "getSnapshot") : void 0;
2415
+ if (!snapshot) return;
2416
+ if ([...getImplementations(snapshot)].some((implementation) => {
2417
+ return getTsReturnExpressions(implementation).some((value) => !isCached(value, implementation));
2418
+ })) context.report({
2413
2419
  node,
2414
- messageId: "immediateAccess"
2420
+ messageId: "uncachedSnapshot"
2415
2421
  });
2416
2422
  } };
2417
2423
  }
@@ -2443,7 +2449,7 @@ const rules = {
2443
2449
  const plugin = {
2444
2450
  meta: {
2445
2451
  name: "@violetflux/eslint-plugin-kerros",
2446
- version: "0.2.2"
2452
+ version: "0.2.3"
2447
2453
  },
2448
2454
  rules
2449
2455
  };