candor-ts 0.5.4 → 0.5.5
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/package.json +1 -1
- package/scan.mjs +67 -29
package/package.json
CHANGED
package/scan.mjs
CHANGED
|
@@ -323,6 +323,32 @@ const nodeName = new WeakMap(); // declaration node -> qualified name
|
|
|
323
323
|
const entityTables = new Map(); // ClassDeclaration node -> table name
|
|
324
324
|
const interfaceImpls = new Map(); // InterfaceDeclaration node -> implementing ClassDeclarations (CHA universe)
|
|
325
325
|
const classOverrides = new Map(); // base-method MemberDeclaration node -> overriding subclass member nodes (class-CHA)
|
|
326
|
+
// Resolve `extends X` to X's LOCAL ClassDeclaration (through an import alias), or null. Module-level
|
|
327
|
+
// so both the class-CHA INDEX (below) and the dispatch site's RECEIVER-SUBTREE scoping share one
|
|
328
|
+
// definition of the local inheritance edge.
|
|
329
|
+
function localBaseClassOf(cls) {
|
|
330
|
+
for (const h of cls.heritageClauses ?? []) {
|
|
331
|
+
if (h.token !== ts.SyntaxKind.ExtendsKeyword) continue;
|
|
332
|
+
const t = h.types?.[0];
|
|
333
|
+
if (!t) continue;
|
|
334
|
+
let sym = checker.getSymbolAtLocation(t.expression);
|
|
335
|
+
if (sym && sym.flags & ts.SymbolFlags.Alias) { try { sym = checker.getAliasedSymbol(sym); } catch { /* keep */ } }
|
|
336
|
+
const bd = (sym?.declarations ?? []).find((d) => ts.isClassDeclaration(d));
|
|
337
|
+
if (bd && projectFiles.has(path.resolve(bd.getSourceFile().fileName))) return bd;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
// Is `cls` in the subtree rooted at `root` (i.e. cls === root, or cls transitively `extends` root
|
|
342
|
+
// through LOCAL classes)? Used to scope a base-member override fan-out to the RECEIVER's static type
|
|
343
|
+
// — a sibling subclass's override lives OUTSIDE this subtree and must not contaminate the verdict.
|
|
344
|
+
function classInSubtree(cls, root) {
|
|
345
|
+
let cur = cls, guard = 0;
|
|
346
|
+
while (cur && guard++ < 64) {
|
|
347
|
+
if (cur === root) return true;
|
|
348
|
+
cur = localBaseClassOf(cur);
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
326
352
|
function moduleOf(sf) {
|
|
327
353
|
const rel = path.relative(rootDir, path.resolve(sf.fileName)).replace(/\.[mc]?[tj]sx?$/, "");
|
|
328
354
|
return rel.split(path.sep).join(".");
|
|
@@ -467,19 +493,7 @@ for (const sf of sources) {
|
|
|
467
493
|
{
|
|
468
494
|
const memberName = (m) => (ts.isMethodDeclaration(m) || ts.isGetAccessorDeclaration(m)
|
|
469
495
|
|| ts.isSetAccessorDeclaration(m) || ts.isPropertyDeclaration(m)) && m.name?.getText?.();
|
|
470
|
-
|
|
471
|
-
const baseClassOf = (cls) => {
|
|
472
|
-
for (const h of cls.heritageClauses ?? []) {
|
|
473
|
-
if (h.token !== ts.SyntaxKind.ExtendsKeyword) continue;
|
|
474
|
-
const t = h.types?.[0];
|
|
475
|
-
if (!t) continue;
|
|
476
|
-
let sym = checker.getSymbolAtLocation(t.expression);
|
|
477
|
-
if (sym && sym.flags & ts.SymbolFlags.Alias) { try { sym = checker.getAliasedSymbol(sym); } catch { /* keep */ } }
|
|
478
|
-
const bd = (sym?.declarations ?? []).find((d) => ts.isClassDeclaration(d));
|
|
479
|
-
if (bd && projectFiles.has(path.resolve(bd.getSourceFile().fileName))) return bd;
|
|
480
|
-
}
|
|
481
|
-
return null;
|
|
482
|
-
};
|
|
496
|
+
const baseClassOf = localBaseClassOf;
|
|
483
497
|
for (const sf of sources) {
|
|
484
498
|
(function scan(node) {
|
|
485
499
|
if (ts.isClassDeclaration(node)) {
|
|
@@ -697,24 +711,48 @@ function visitCalls(node) {
|
|
|
697
711
|
// receiver already resolved to the leaf (`new Dog()` -> Dog.speak, no overrides) so this is
|
|
698
712
|
// inert there — no double-count. A base method NO subclass overrides has no entry: today's
|
|
699
713
|
// behavior (just the base) is preserved exactly.
|
|
700
|
-
const
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
714
|
+
const allOverrides = classOverrides.get(decl);
|
|
715
|
+
if (allOverrides && allOverrides.length > 0) {
|
|
716
|
+
// PRECISION: scope the fan-out to the RECEIVER's static-type subtree. A base-member
|
|
717
|
+
// dispatch on a receiver statically typed as subclass `Cat` can only ever bind to a
|
|
718
|
+
// `Cat`-subtree body — a SIBLING `Dog.speak` override is type-impossible on this path,
|
|
719
|
+
// so propagating its effect over-reports on an unreachable receiver (fabrication-
|
|
720
|
+
// adjacent). When we can pin the receiver's static class (a property/element access
|
|
721
|
+
// whose receiver-expression type is a LOCAL class), keep only overrides whose owning
|
|
722
|
+
// class lies in that class's subtree; `viaBase(a: Animal)` keeps Dog (Dog ∈ Animal-
|
|
723
|
+
// subtree, the soundness edge), `noOverride(c: Cat)` drops Dog (Dog ∉ Cat-subtree) and
|
|
724
|
+
// stays pure. SOUNDNESS-PRESERVING FALLBACK: if the receiver's static class can't be
|
|
725
|
+
// pinned to a LOCAL class (no property access, a union/interface/`any` receiver, an
|
|
726
|
+
// external/unresolved type), we do NOT narrow — the full override set is kept, exactly
|
|
727
|
+
// the pre-precision behavior, so we never silently drop an effect we can't rule out.
|
|
728
|
+
let overrides = allOverrides;
|
|
729
|
+
const recvExpr = (ts.isPropertyAccessExpression(node.expression)
|
|
730
|
+
|| ts.isElementAccessExpression(node.expression)) ? node.expression.expression : null;
|
|
731
|
+
if (recvExpr) {
|
|
732
|
+
const rt = checker.getTypeAtLocation(recvExpr);
|
|
733
|
+
const rootClass = (rt?.symbol?.declarations ?? []).find((d) =>
|
|
734
|
+
ts.isClassDeclaration(d) && projectFiles.has(path.resolve(d.getSourceFile().fileName)));
|
|
735
|
+
if (rootClass) overrides = allOverrides.filter((om) =>
|
|
736
|
+
ts.isClassDeclaration(om.parent) && classInSubtree(om.parent, rootClass));
|
|
737
|
+
}
|
|
738
|
+
if (overrides.length > 0) {
|
|
739
|
+
if (overrides.length <= 12) {
|
|
740
|
+
let allResolved = true;
|
|
741
|
+
const oTargets = [];
|
|
742
|
+
for (const om of overrides) {
|
|
743
|
+
const ot = nodeName.get(om);
|
|
744
|
+
if (ot) oTargets.push(ot);
|
|
745
|
+
else allResolved = false;
|
|
746
|
+
}
|
|
747
|
+
for (const ot of oTargets) rec.edges.add(ot);
|
|
748
|
+
if (!allResolved) {
|
|
749
|
+
rec.direct.add("Unknown");
|
|
750
|
+
rec.why.add(`override:${decl.name?.getText?.() ?? "member"}`);
|
|
751
|
+
}
|
|
752
|
+
} else {
|
|
753
|
+
rec.direct.add("Unknown"); // override family too wide to enumerate soundly
|
|
713
754
|
rec.why.add(`override:${decl.name?.getText?.() ?? "member"}`);
|
|
714
755
|
}
|
|
715
|
-
} else {
|
|
716
|
-
rec.direct.add("Unknown"); // override family too wide to enumerate soundly
|
|
717
|
-
rec.why.add(`override:${decl.name?.getText?.() ?? "member"}`);
|
|
718
756
|
}
|
|
719
757
|
}
|
|
720
758
|
// record what each argument position received (callback-flow, see callbackArgs)
|