candor-ts 0.5.10 → 0.5.12

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 +49 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
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
@@ -788,6 +788,27 @@ function visitCalls(node) {
788
788
  if (t) rec.edges.add(t);
789
789
  }
790
790
  }
791
+ // `fn.call(thisArg, …)` / `fn.apply(thisArg, args)` INVOKE the receiver function reference, and
792
+ // `Reflect.apply(fn, …)` / `Reflect.construct(Ctor, …)` invoke their FIRST ARGUMENT. The resolved
793
+ // signature lands on the es-lib `CallableFunction.call/apply` / `Reflect.apply` member, so the
794
+ // function actually invoked (the receiver, or arg0) was never followed → the caller read
795
+ // silent-pure (HIGH: a common reflective-invoke shape). Edge to the referenced unit, mirroring the
796
+ // HOF-ref arm: a pure ref edges to a pure unit (no fabrication); a non-fn receiver/arg resolves to
797
+ // no minted unit (`nodeName` miss) and adds nothing; an unresolvable ref stays opaque/Unknown.
798
+ if (ts.isPropertyAccessExpression(node.expression)) {
799
+ const m = node.expression.name.text;
800
+ const recv = node.expression.expression;
801
+ const recvText = recv.getText().replace(/\s+/g, "");
802
+ let invokedRef = null;
803
+ if ((m === "call" || m === "apply") && recvText !== "Reflect") invokedRef = recv;
804
+ else if (recvText === "Reflect" && (m === "apply" || m === "construct"))
805
+ invokedRef = (node.arguments ?? [])[0] ?? null;
806
+ if (invokedRef && (ts.isIdentifier(invokedRef) || ts.isPropertyAccessExpression(invokedRef))) {
807
+ const d2 = realDecl(checker.getSymbolAtLocation(invokedRef));
808
+ const t = d2 && nodeName.get(d2);
809
+ if (t) rec.edges.add(t);
810
+ }
811
+ }
791
812
  if (mod === "<local>") {
792
813
  const targetName = nodeName.get(decl);
793
814
  if (targetName) {
@@ -917,12 +938,35 @@ function visitCalls(node) {
917
938
  // entropy were invisible).
918
939
  const name = decl.name ? decl.name.getText() : "";
919
940
  const parent = decl.parent && decl.parent.name ? decl.parent.name.getText() : "";
941
+ // `eval(code)` executes an arbitrary code string — it can perform ANY effect, so it is genuinely
942
+ // Unknown (the same posture as `new Function(s)`, which already reads Unknown via the no-decl
943
+ // path). The es-lib declares `eval` as a top-level ambient function (parent is the global/source
944
+ // file, no type name), and `globalThis.eval`/`window.eval`/`self.eval` all resolve to the same
945
+ // `eval` declaration — so keying on the resolved name catches every access form. Without this it
946
+ // resolved to a benign es-lib member and read SILENT-PURE (a code-execution sink reported pure).
947
+ if (name === "eval" && parent !== "Math" && parent !== "JSON") {
948
+ rec.direct.add("Unknown");
949
+ rec.why.add("call:eval");
950
+ }
920
951
  if ((parent === "DateConstructor" && name === "now") || (parent === "Performance" && name === "now"))
921
952
  rec.direct.add("Clock");
922
953
  if (parent === "Math" && name === "random") rec.direct.add("Rand");
923
954
  if (ts.isNewExpression(node) && (node.arguments ?? []).length === 0
924
955
  && checker.getTypeAtLocation(node.expression)?.symbol?.name === "DateConstructor")
925
956
  rec.direct.add("Clock");
957
+ // Browser/runtime NETWORK globals declared in lib.dom — no importable module for the κ table to
958
+ // key on, so they read SILENT-PURE. `XMLHttpRequest.send`/`.open` issue the HTTP request; the
959
+ // `EventSource`/`WebSocket` constructors open a connection on construction. Net. (Found by a
960
+ // Net-deep sweep. The npm `ws` package is already κ-covered; this is the bare browser global.)
961
+ if (parent === "XMLHttpRequest" && (name === "send" || name === "open")) rec.direct.add("Net");
962
+ // `new EventSource(url)` / `new WebSocket(url)`: the constructor is declared on an anonymous
963
+ // `declare var` object type (symbol `__type`, no usable parent name), but reaching the es-lib
964
+ // branch already proves the ctor resolved to lib.dom (not a project class shadowing the name),
965
+ // so the constructed identifier is the real browser global.
966
+ if (ts.isNewExpression(node)) {
967
+ const ctorName = node.expression.getText();
968
+ if (ctorName === "EventSource" || ctorName === "WebSocket") rec.direct.add("Net");
969
+ }
926
970
  } else {
927
971
  // The member token κ matches: the resolved declaration's name, EXCEPT a `new X()` call,
928
972
  // whose declaration is a Constructor (empty name) — synthesize "new" so a rule can exempt
@@ -1077,6 +1121,11 @@ function visitCalls(node) {
1077
1121
  && !(checker.getSymbolAtLocation(callee)?.declarations ?? [])
1078
1122
  .some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName))))
1079
1123
  geff = "Net";
1124
+ // the fully-qualified global fetch — `globalThis.fetch`/`window.fetch`/`self.fetch` — is a
1125
+ // PropertyAccess callee the bare-identifier guard above misses, so it read silent-pure. Mirror the
1126
+ // `eval` global-qualifier handling (a runtime global a project would not shadow).
1127
+ else if (ctext === "globalThis.fetch" || ctext === "window.fetch" || ctext === "self.fetch")
1128
+ geff = "Net";
1080
1129
  if (geff) {
1081
1130
  const owner = enclosing(node);
1082
1131
  if (owner) fns.get(owner).direct.add(geff);