candor-ts 0.5.7 → 0.5.8

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 +20 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
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
@@ -724,6 +724,17 @@ function edgeToTargets(rec, decls) {
724
724
  for (const d of decls) { const t = nodeName.get(d); if (t) rec.edges.add(t); }
725
725
  }
726
726
 
727
+ // Callee names that INVOKE a function/method argument (so a fn-reference passed to one is reachable
728
+ // through it). Array/iterable HOFs, the timer/microtask schedulers, and Promise continuations. A
729
+ // STORE/compare/log sink (`set`/`push`/`add`/`includes`/`indexOf`/`concat`/`log`/`stringify`/…) is
730
+ // deliberately ABSENT — edging there would fabricate the fn's effects on a pure path (the cardinal sin).
731
+ const HOF_INVOKERS = new Set([
732
+ "map", "forEach", "filter", "reduce", "reduceRight", "find", "findIndex", "findLast", "findLastIndex",
733
+ "some", "every", "flatMap", "sort", "group", "groupBy", "partition", "mapValues", "flatMapDeep",
734
+ "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "requestAnimationFrame", "requestIdleCallback",
735
+ "then", "catch", "finally", "nextTick",
736
+ ]);
737
+
727
738
  // ---- pass 2: per call site, the (CLASSIFY)/(EDGE)/(UNKNOWN) resolution of SEMANTICS §4 ------------
728
739
  function visitCalls(node) {
729
740
  if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
@@ -761,7 +772,15 @@ function visitCalls(node) {
761
772
  // the Rust engine's fn-as-value edge. An inline closure is already charged lexically; a non-fn
762
773
  // argument resolves to no minted unit (`nodeName` miss) and adds nothing — no fabrication. Gated
763
774
  // on a non-local callee so a local callee that merely STORES (never invokes) keeps its precision.
764
- if (mod !== "<local>") {
775
+ // ONLY a callee that actually INVOKES its fn argument makes the reference reachable here. The
776
+ // earlier version edged for ANY non-local callee — fabricating the fn's effects onto a pure path
777
+ // (the cardinal sin) for STORE/compare/log sinks that never call it (`map.set(k, fn)`,
778
+ // `arr.push(fn)`, `arr.includes(fn)`, `console.log(fn)`, `[fn]`). Gate on a known INVOKING HOF by
779
+ // callee name; a custom non-local HOF that invokes its arg is an honest under-report (sound),
780
+ // never a fabrication. (A LOCAL callee keeps its precise callback-flow below.)
781
+ const calleeName = ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text
782
+ : ts.isIdentifier(node.expression) ? node.expression.text : null;
783
+ if (mod !== "<local>" && calleeName && HOF_INVOKERS.has(calleeName)) {
765
784
  for (const a of node.arguments ?? []) {
766
785
  if (!ts.isIdentifier(a) && !ts.isPropertyAccessExpression(a)) continue;
767
786
  const d2 = realDecl(checker.getSymbolAtLocation(a));