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