candor-ts 0.5.11 → 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 +39 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.5.11",
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) {
@@ -933,6 +954,19 @@ function visitCalls(node) {
933
954
  if (ts.isNewExpression(node) && (node.arguments ?? []).length === 0
934
955
  && checker.getTypeAtLocation(node.expression)?.symbol?.name === "DateConstructor")
935
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
+ }
936
970
  } else {
937
971
  // The member token κ matches: the resolved declaration's name, EXCEPT a `new X()` call,
938
972
  // whose declaration is a Constructor (empty name) — synthesize "new" so a rule can exempt
@@ -1087,6 +1121,11 @@ function visitCalls(node) {
1087
1121
  && !(checker.getSymbolAtLocation(callee)?.declarations ?? [])
1088
1122
  .some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName))))
1089
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";
1090
1129
  if (geff) {
1091
1130
  const owner = enclosing(node);
1092
1131
  if (owner) fns.get(owner).direct.add(geff);