candor-ts 0.5.16 → 0.5.18
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 +61 -2
package/package.json
CHANGED
package/scan.mjs
CHANGED
|
@@ -442,6 +442,19 @@ function localName(node) {
|
|
|
442
442
|
}
|
|
443
443
|
return null;
|
|
444
444
|
}
|
|
445
|
+
// True when `node` is lexically inside a FUNCTION body — so its bare local name can collide with a
|
|
446
|
+
// module-level or sibling-scope unit of the same name (see the qual disambiguation below). A namespace/
|
|
447
|
+
// module block and the source file are NOT function scopes (their members are top-level units).
|
|
448
|
+
function isFunctionScoped(node) {
|
|
449
|
+
for (let p = node.parent; p; p = p.parent) {
|
|
450
|
+
if (ts.isSourceFile(p) || ts.isModuleBlock(p)) return false;
|
|
451
|
+
if (ts.isFunctionDeclaration(p) || ts.isFunctionExpression(p) || ts.isArrowFunction(p)
|
|
452
|
+
|| ts.isMethodDeclaration(p) || ts.isConstructorDeclaration(p)
|
|
453
|
+
|| ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p))
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
445
458
|
for (const sf of sources) {
|
|
446
459
|
const mod = moduleOf(sf);
|
|
447
460
|
(function collect(node) {
|
|
@@ -491,8 +504,15 @@ for (const sf of sources) {
|
|
|
491
504
|
const n = localName(node);
|
|
492
505
|
const isCjsExport = _lastCjs; // captured immediately: localName set it for THIS node only
|
|
493
506
|
if (n) {
|
|
494
|
-
const qual = `${mod}.${n}`;
|
|
495
507
|
const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart());
|
|
508
|
+
// Disambiguate FUNCTION-SCOPED local function units by position. A `const persist = () => …` (or a
|
|
509
|
+
// nested `function persist(){}`) inside a fn body shares the bare `mod.name` key with a module-level
|
|
510
|
+
// OR sibling-scope unit of the same name, so the second `fns.set` CLOBBERS the first and a call that
|
|
511
|
+
// resolves (correctly, via the checker) to the LOCAL decl reads the OTHER unit's effects off the
|
|
512
|
+
// shared entry — FABRICATING them onto a pure caller. `nodeName` is keyed by NODE identity, so a
|
|
513
|
+
// per-node-unique key keeps resolution exact; only TOP-LEVEL units need the stable bare name a
|
|
514
|
+
// consumer's hash-join targets (a function-scoped local is never an export, so nothing joins to it).
|
|
515
|
+
const qual = isFunctionScoped(node) ? `${mod}.${n}#${line + 1}:${character + 1}` : `${mod}.${n}`;
|
|
496
516
|
fns.set(qual, { local: n, direct: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
|
|
497
517
|
cmds: new Set(), paths: new Set(), blind: new Set(), incomplete: new Set(), why: new Set(), entry: false, isCjsExport,
|
|
498
518
|
loc: `${path.relative(rootDir, sf.fileName)}:${line + 1}:${character + 1}` });
|
|
@@ -594,6 +614,31 @@ function realDecl(sym) {
|
|
|
594
614
|
return sym.valueDeclaration ?? sym.declarations?.[0];
|
|
595
615
|
}
|
|
596
616
|
|
|
617
|
+
// Resolve a value-reference NODE (`expr` in `expr.call(…)`/`expr.apply(…)`) to the qualified name of the
|
|
618
|
+
// FUNCTION UNIT it ultimately denotes — following ONE OR MORE local-variable aliases. The `.call`/`.apply`
|
|
619
|
+
// arm lands on the es-lib member so `getResolvedSignature` never sees the real fn; for a direct identifier
|
|
620
|
+
// (`effectful.call`) `realDecl` → the fn decl → a minted unit. But `const m = effectful; m.call(…)` resolves
|
|
621
|
+
// `m` to its VARIABLE declaration, whose initializer is the bare identifier `effectful` — the variable node
|
|
622
|
+
// itself is NOT a minted unit (only var-decls whose initializer is an arrow/fn-expr are), so the edge was
|
|
623
|
+
// dropped → silent-pure (the cardinal sin). Here we chase the variable's initializer identifier/member to
|
|
624
|
+
// the function it aliases. Returns the unit name, or null if the chain can't be pinned to a function unit.
|
|
625
|
+
// Bounded depth guards a pathological `const a=b, b=a` cycle. NO fabrication: a non-fn binding (or any link
|
|
626
|
+
// that doesn't resolve to a minted fn unit) returns null and the caller adds nothing / discloses Unknown.
|
|
627
|
+
function resolveFnRefUnit(refNode, depth = 0) {
|
|
628
|
+
if (!refNode || depth > 8) return null;
|
|
629
|
+
if (!ts.isIdentifier(refNode) && !ts.isPropertyAccessExpression(refNode)) return null;
|
|
630
|
+
const d = realDecl(checker.getSymbolAtLocation(refNode));
|
|
631
|
+
if (!d) return null;
|
|
632
|
+
// Already a minted unit (the function itself, an arrow/fn-expr const, a class method/property)?
|
|
633
|
+
const direct = nodeName.get(d);
|
|
634
|
+
if (direct) return direct;
|
|
635
|
+
// A local variable / parameter bound to a function reference — follow the initializer alias.
|
|
636
|
+
if ((ts.isVariableDeclaration(d) || ts.isBindingElement(d) || ts.isParameter(d)) && d.initializer
|
|
637
|
+
&& (ts.isIdentifier(d.initializer) || ts.isPropertyAccessExpression(d.initializer)))
|
|
638
|
+
return resolveFnRefUnit(d.initializer, depth + 1);
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
597
642
|
// Accessor resolution (the silent-pure-accessor fix): a property READ (`x.raw`) or property
|
|
598
643
|
// ASSIGNMENT target (`x.path = v`) may resolve to a getter/setter whose body performs effects. We
|
|
599
644
|
// resolve the property-name symbol to its declarations and look for an accessor of the matching
|
|
@@ -859,8 +904,22 @@ function visitCalls(node) {
|
|
|
859
904
|
invokedRef = (node.arguments ?? [])[0] ?? null;
|
|
860
905
|
if (invokedRef && (ts.isIdentifier(invokedRef) || ts.isPropertyAccessExpression(invokedRef))) {
|
|
861
906
|
const d2 = realDecl(checker.getSymbolAtLocation(invokedRef));
|
|
862
|
-
|
|
907
|
+
// Resolve the receiver/arg0 to its function unit, FOLLOWING local-variable aliases
|
|
908
|
+
// (`const m = effectful; m.call(…)`) — the direct-identifier form already landed on a minted
|
|
909
|
+
// unit, but an aliased local var resolves to its VARIABLE decl (not a unit), which dropped the
|
|
910
|
+
// edge silent-pure. `resolveFnRefUnit` chases the initializer alias to the real fn.
|
|
911
|
+
const t = (d2 && nodeName.get(d2)) || resolveFnRefUnit(invokedRef);
|
|
863
912
|
if (t) rec.edges.add(t);
|
|
913
|
+
// HONESTY: the receiver IS a local variable/parameter (it resolved to a value declaration)
|
|
914
|
+
// but we could NOT pin it to a function unit — e.g. bound to a param, a reassigned/branched
|
|
915
|
+
// value, an `any`-typed holder. The `.call`/`.apply` still INVOKES whatever it holds, so a
|
|
916
|
+
// silent-pure verdict would be the cardinal sin. Disclose Unknown instead. (A direct fn
|
|
917
|
+
// identifier / known fn always resolves above, so this never fires for the precise forms; a
|
|
918
|
+
// non-value receiver — a type, a literal — resolves to no decl and stays out, no fabrication.)
|
|
919
|
+
else if (d2 && (ts.isVariableDeclaration(d2) || ts.isBindingElement(d2) || ts.isParameter(d2))) {
|
|
920
|
+
rec.direct.add("Unknown");
|
|
921
|
+
rec.why.add(`call:${recvText.slice(0, 40)}.${m}`);
|
|
922
|
+
}
|
|
864
923
|
}
|
|
865
924
|
}
|
|
866
925
|
if (mod === "<local>") {
|