candor-ts 0.5.17 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scan.mjs +40 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.5.17",
3
+ "version": "0.5.18",
4
4
  "description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.5)",
5
5
  "type": "module",
6
6
  "dependencies": {
package/scan.mjs CHANGED
@@ -614,6 +614,31 @@ function realDecl(sym) {
614
614
  return sym.valueDeclaration ?? sym.declarations?.[0];
615
615
  }
616
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
+
617
642
  // Accessor resolution (the silent-pure-accessor fix): a property READ (`x.raw`) or property
618
643
  // ASSIGNMENT target (`x.path = v`) may resolve to a getter/setter whose body performs effects. We
619
644
  // resolve the property-name symbol to its declarations and look for an accessor of the matching
@@ -879,8 +904,22 @@ function visitCalls(node) {
879
904
  invokedRef = (node.arguments ?? [])[0] ?? null;
880
905
  if (invokedRef && (ts.isIdentifier(invokedRef) || ts.isPropertyAccessExpression(invokedRef))) {
881
906
  const d2 = realDecl(checker.getSymbolAtLocation(invokedRef));
882
- const t = d2 && nodeName.get(d2);
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);
883
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
+ }
884
923
  }
885
924
  }
886
925
  if (mod === "<local>") {