candor-ts 0.40.0 → 0.40.1
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/policy.mjs +8 -0
- package/scan.mjs +961 -88
package/scan.mjs
CHANGED
|
@@ -40,6 +40,14 @@ import { isTestPath, kappa, kappaKnows, nodeCoreUnreviewed, fsKind, commandHeadE
|
|
|
40
40
|
WEB_WIRE_MEMBERS, CONNECTING_CTORS, NET_ESTABLISHING, NET_ACCEPTING, NET_REQUEST_NAMED, FS_USE_VERBS,
|
|
41
41
|
EXEC_USE_VERBS, RESERVED_SIDECAR_SEGMENTS } from "./scan-core.mjs";
|
|
42
42
|
import { emitSurface } from "./surface.mjs";
|
|
43
|
+
// ⟨R815⟩ reach marker for the class-definition-time attribution in `enclosing` (declared here, not beside it:
|
|
44
|
+
// `enclosing` runs from module-level code above its own definition, so a later `const` is in its TDZ).
|
|
45
|
+
const R815_REACH = process.env.CANDOR_R815_REACH ? (k) => console.error(`R815-REACH ${k}`) : null;
|
|
46
|
+
// ⟨R934/R935⟩ reach marker: a charge that came from a PARAMETER or PROPERTY environment location.
|
|
47
|
+
const R246_REACH = process.env.CANDOR_R246_REACH ? (k) => console.error(`R246-REACH ${k}`) : null;
|
|
48
|
+
let projAccessorNames = null;
|
|
49
|
+
const anyRecvSetResidue = []; // ⟨R246/R1039⟩ { owner, name|null, kind } — see the post-fixpoint disclosure
|
|
50
|
+
const ENVLOC_REACH = process.env.CANDOR_ENVLOC_REACH ? (k) => console.error(`ENVLOC-REACH ${k}`) : null;
|
|
43
51
|
|
|
44
52
|
const ENGINE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
45
53
|
|
|
@@ -2655,7 +2663,16 @@ function declModule(decl) {
|
|
|
2655
2663
|
let m = f.match(/@types\/node\/(.+?)\.d\.ts$/);
|
|
2656
2664
|
if (m) return m[1];
|
|
2657
2665
|
if (/typescript\/lib\/lib\..*\.d\.ts$/.test(f)) return "<es-lib>";
|
|
2658
|
-
|
|
2666
|
+
// ⟨SOUNDNESS R778⟩ The INNERMOST `node_modules/<pkg>/` owns the file, not the first. pnpm's real path is
|
|
2667
|
+
// `node_modules/.pnpm/fs-extra@11.2.0/node_modules/fs-extra/…` and an npm nested install is
|
|
2668
|
+
// `node_modules/outer/node_modules/inner/…`; the first-segment match keyed every foreign package of a pnpm
|
|
2669
|
+
// tree as `.pnpm` and `inner` as `outer`. MEASURED with the installer the only variable (fx/r778-{npm,pnpm},
|
|
2670
|
+
// EXECUTED to write the file): npm read `['Fs']` and `deny Fs` exit 1, pnpm read `[]` + `invisible:['.pnpm']`
|
|
2671
|
+
// and exit 0 — a FALSE disclosure (`.pnpm` is not a package) hiding a κ-classified effect. For a flat npm
|
|
2672
|
+
// layout the first and last segments are the same one, so no npm key moves. A file directly under a dot-store
|
|
2673
|
+
// with no inner `node_modules/` (no package segment at all) asks the nearest `package.json` instead.
|
|
2674
|
+
m = [...f.matchAll(/node_modules\/(@[^/]+\/[^/]+|[^/]+)\//g)].at(-1) ?? null;
|
|
2675
|
+
if (m && m[1].startsWith(".")) m = (() => { const n = nearestPackageName(f); return n ? [null, n] : m; })();
|
|
2659
2676
|
if (m) {
|
|
2660
2677
|
// `@types/X` (DefinitelyTyped) provides types for the RUNTIME package X — map it to X so the curated κ
|
|
2661
2678
|
// tier (keyed by the runtime name: pg/ws/…) fires. Without this a package typed via @types resolved to
|
|
@@ -3461,8 +3478,7 @@ function literalHeadHostUrl(expr) {
|
|
|
3461
3478
|
// (an assignment rooted at it, another call's argument, an alias, a return) refuses the capture.
|
|
3462
3479
|
function determinedUrlObject(expr, call, depth = 0) {
|
|
3463
3480
|
if (!expr || depth > 1) return null;
|
|
3464
|
-
|
|
3465
|
-
|| ts.isSatisfiesExpression?.(expr)) expr = expr.expression;
|
|
3481
|
+
expr = unwrapArgExpr(expr); // R780: the one transparent-wrapper set
|
|
3466
3482
|
if (ts.isNewExpression(expr) && ts.isIdentifier(expr.expression)
|
|
3467
3483
|
&& (expr.expression.text === "URL" || expr.expression.text === "Request")) {
|
|
3468
3484
|
const decls = checker.getSymbolAtLocation(expr.expression)?.declarations ?? [];
|
|
@@ -3516,8 +3532,17 @@ function determinedUrlObject(expr, call, depth = 0) {
|
|
|
3516
3532
|
visit(d.getSourceFile());
|
|
3517
3533
|
return safe ? determinedUrlObject(d.initializer, call, depth + 1) : null;
|
|
3518
3534
|
}
|
|
3519
|
-
//
|
|
3520
|
-
//
|
|
3535
|
+
// ⟨SOUNDNESS R780⟩ THE ONE TRANSPARENT-WRAPPER SET. Parentheses, `as`, `<T>x`, `!` and `satisfies` change no runtime
|
|
3536
|
+
// value, so every "is this expression THAT reference / THAT object?" question asks it through this one function.
|
|
3537
|
+
// There were seventeen hand-rolled loops answering it with FIVE different wrapper sets (parens only; +`as`;
|
|
3538
|
+
// +`!`; +`satisfies`; +`<T>`), and every gap was a spelling the engine went blind on: `(k.m as F).call(k)`,
|
|
3539
|
+
// `Reflect.apply(f as F, …)` and `(<any>globalThis).process.env.HOME` were ABSENT (EXECUTED, lane fixture
|
|
3540
|
+
// `fx/r780`), `(f as F).bind(t)` and `run((f))` were opaque. Two loops are deliberately NOT routed here:
|
|
3541
|
+
// `conversionSourceTracked` stops at `as` because an `as` IS the conversion it is asking about (R958 reads the
|
|
3542
|
+
// assertion separately), and `rootsAtStdStream` is a SUPPRESSOR (it frees `process.stdout.write` of a fabricated
|
|
3543
|
+
// Net) — widening a suppressor removes charges, which is not this fix's direction.
|
|
3544
|
+
// (Also R947: the invoked function's own ARGUMENTS on the reflective routes, or `null` when they are not
|
|
3545
|
+
// visible here — a spread, an `.apply` of a non-literal array. `null` is the safe answer: it captures nothing.)
|
|
3521
3546
|
function unwrapArgExpr(e) {
|
|
3522
3547
|
while (e && (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isTypeAssertionExpression?.(e)
|
|
3523
3548
|
|| ts.isNonNullExpression(e) || ts.isSatisfiesExpression?.(e))) e = e.expression;
|
|
@@ -3701,6 +3726,14 @@ function kappaOfRef(d2, ref) {
|
|
|
3701
3726
|
function netEstablishingVerb(member, mod) {
|
|
3702
3727
|
return NET_ESTABLISHING.has(member) || (/^(node:)?dgram$/.test(mod ?? "") && member === "send");
|
|
3703
3728
|
}
|
|
3729
|
+
// ⟨R966⟩ see the (CLASSIFY) arm. A package whose `listen` is not a network accept: JSON-RPC connections start
|
|
3730
|
+
// reading a transport chosen (and, when it is a socket, charged) where the transport was built.
|
|
3731
|
+
const NON_NETWORK_LISTEN_PKGS = /^(vscode-jsonrpc|vscode-languageserver(-protocol)?|vscode-languageclient)(\/|$)/;
|
|
3732
|
+
function dependencyListenIsAccept(mod, decl) {
|
|
3733
|
+
if (!mod || mod === "<local>" || mod === "<es-lib>" || mod.startsWith("/") || declIsNodeTypes(decl) || isOwnPackageDecl(decl))
|
|
3734
|
+
return false;
|
|
3735
|
+
return !NON_NETWORK_LISTEN_PKGS.test(mod);
|
|
3736
|
+
}
|
|
3704
3737
|
function netAcceptingVerb(member, kMod) {
|
|
3705
3738
|
return NET_ACCEPTING.has(member) || (/^(node:)?inspector(\/promises)?$/.test(kMod ?? "") && member === "open");
|
|
3706
3739
|
}
|
|
@@ -4142,6 +4175,14 @@ const CHA_FANOUT_LIMIT = 12;
|
|
|
4142
4175
|
// rather than inlined for the reason `CHA_FANOUT_LIMIT` was: two literals for one rule is how they drift.
|
|
4143
4176
|
const TYPINGS_CENSUS_CAP = 128;
|
|
4144
4177
|
const classOverrides = new Map();// base-method MemberDeclaration node -> overriding subclass member nodes (class-CHA)
|
|
4178
|
+
// ⟨SOUNDNESS R873⟩ An override that is not a class MEMBER: `this.m = () => …` assigned inside an instance body of
|
|
4179
|
+
// class `C` replaces `m` on every `C` instance. Registered in `classOverrides` like a member, keyed to the class
|
|
4180
|
+
// that performs the assignment here, because the node's `.parent` is an expression, not the class.
|
|
4181
|
+
const assignedOverrideClass = new Map(); // assigned override node -> the class-like node whose instances it overrides
|
|
4182
|
+
// The class an override node belongs to, for receiver-subtree scoping: a member's own class (declaration OR
|
|
4183
|
+
// expression — ⟨R956⟩), or the assigning class of a ⟨R873⟩ assigned override.
|
|
4184
|
+
const overrideOwner = (om) => assignedOverrideClass.get(om)
|
|
4185
|
+
?? ((om?.parent && (ts.isClassDeclaration(om.parent) || ts.isClassExpression(om.parent))) ? om.parent : null);
|
|
4145
4186
|
const classDescendants = new Map();// base ClassDeclaration -> transitive LOCAL subclass ClassDeclarations (coercion-CHA)
|
|
4146
4187
|
// R954 — the conformer registries (filled by the conversion pass after `walkStructural`).
|
|
4147
4188
|
const classConformers = new Map(); // local ClassDeclaration -> conformer nodes (local class, literal, dep class)
|
|
@@ -4226,7 +4267,7 @@ function overrideClosure(seeds) {
|
|
|
4226
4267
|
const out = [];
|
|
4227
4268
|
for (const s of seeds) {
|
|
4228
4269
|
for (const m of [s, ...overrideDescent(s)]) {
|
|
4229
|
-
if (isOverridableMethod(m) && !out.includes(m)) out.push(m);
|
|
4270
|
+
if ((isOverridableMethod(m) || assignedOverrideClass.has(m)) && !out.includes(m)) out.push(m);
|
|
4230
4271
|
if (out.length > CHA_FANOUT_LIMIT + 1) return out;
|
|
4231
4272
|
}
|
|
4232
4273
|
}
|
|
@@ -4277,7 +4318,7 @@ function overrideDescent(decl) {
|
|
|
4277
4318
|
function memberDispatchBodies(decl, rootClass) {
|
|
4278
4319
|
const all = overrideDescent(decl);
|
|
4279
4320
|
const scoped = (!rootClass || all.length === 0) ? all
|
|
4280
|
-
: all.filter((om) =>
|
|
4321
|
+
: all.filter((om) => classInSubtree(overrideOwner(om), rootClass)); // R873/R956: assigned and class-expression overrides
|
|
4281
4322
|
// R954 — …and the CONFORMERS registered at the receiver's class (a value the checker showed
|
|
4282
4323
|
// converted to it, or to a subclass of it), which run for this dispatch exactly as an override does. Scoped
|
|
4283
4324
|
// the same way: registered at `Ct` and its ancestors, so a SUBTYPE receiver never sees them.
|
|
@@ -5153,7 +5194,9 @@ for (const { mod, name, ident } of exportAliasCandidates) {
|
|
|
5153
5194
|
const baseClassOf = localBaseClassOf;
|
|
5154
5195
|
for (const sf of sources) {
|
|
5155
5196
|
(function scan(node) {
|
|
5156
|
-
|
|
5197
|
+
// ⟨SOUNDNESS R956⟩ a CLASS EXPRESSION is a subclass like any other: `new (class extends BaseC { m(){…} })()`
|
|
5198
|
+
// reaching a `BaseC`-typed `b.m()` was ABSENT at every gate (EXECUTED) because this index read declarations only.
|
|
5199
|
+
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
|
5157
5200
|
// Local-DESCENDANT index (the coercion-CHA universe, below). classOverrides is keyed by an
|
|
5158
5201
|
// ANCESTOR MEMBER, so it only sees an override whose base ALSO declares the name. The coercion
|
|
5159
5202
|
// protocol's members are the opposite shape: `class Sub extends Base { toString(){…} }` where
|
|
@@ -5190,6 +5233,7 @@ for (const { mod, name, ident } of exportAliasCandidates) {
|
|
|
5190
5233
|
if (!classOverrides.has(ancestor)) classOverrides.set(ancestor, []);
|
|
5191
5234
|
classOverrides.get(ancestor).push(m);
|
|
5192
5235
|
foundLocal = true;
|
|
5236
|
+
mintClassExprOverride(node, m, name); // R956
|
|
5193
5237
|
break;
|
|
5194
5238
|
}
|
|
5195
5239
|
base = baseClassOf(base);
|
|
@@ -5209,16 +5253,79 @@ for (const { mod, name, ident } of exportAliasCandidates) {
|
|
|
5209
5253
|
if (!foreignClassOverrides.has(fm)) foreignClassOverrides.set(fm, []);
|
|
5210
5254
|
const arr = foreignClassOverrides.get(fm);
|
|
5211
5255
|
if (!arr.includes(m)) arr.push(m);
|
|
5256
|
+
mintClassExprOverride(node, m, name); // R956
|
|
5212
5257
|
}
|
|
5213
5258
|
}
|
|
5214
5259
|
}
|
|
5215
5260
|
}
|
|
5216
5261
|
}
|
|
5262
|
+
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) registerAssignedOverrides(node); // R873
|
|
5217
5263
|
ts.forEachChild(node, scan);
|
|
5218
5264
|
})(sf);
|
|
5219
5265
|
}
|
|
5220
5266
|
}
|
|
5221
5267
|
|
|
5268
|
+
// ⟨SOUNDNESS R956⟩ A class EXPRESSION's member has no unit (`localName` names class DECLARATION members only), so
|
|
5269
|
+
// its body folded into whatever evaluates the expression and no dispatch could land on it. An override needs an
|
|
5270
|
+
// addressable body: mint it the way a structural implementor's member is minted — keyed by `stableUnitTag`, with
|
|
5271
|
+
// ⟨R519⟩'s containment edge so the evaluating unit keeps every charge it had (an ADDITION, never a move). Only a
|
|
5272
|
+
// member that OVERRIDES is minted; a class expression's other members stay where they were.
|
|
5273
|
+
function mintClassExprOverride(cls, m, name) {
|
|
5274
|
+
if (!ts.isClassExpression(cls) || nodeName.has(m)) return;
|
|
5275
|
+
const bodied = (ts.isMethodDeclaration(m) && m.body)
|
|
5276
|
+
|| (ts.isPropertyDeclaration(m) && m.initializer && (ts.isArrowFunction(m.initializer) || ts.isFunctionExpression(m.initializer)));
|
|
5277
|
+
if (!bodied) return; // an unminted override reads unresolved at every reader — disclosed, never silent
|
|
5278
|
+
const sf = m.getSourceFile();
|
|
5279
|
+
mintPositionalStructuralUnit(moduleOf(sf), sf, m, name);
|
|
5280
|
+
if (process.env.CANDOR_R873_REACH) console.error(`R873-REACH classexpr ${name}`);
|
|
5281
|
+
}
|
|
5282
|
+
// ⟨SOUNDNESS R873⟩ `this.m = <value>` inside an INSTANCE body of class `C` (its constructor, a method, an accessor,
|
|
5283
|
+
// an arrow property — not a nested `function`, which rebinds `this`) replaces `m` on that instance. Where `C` or a
|
|
5284
|
+
// local ancestor declares METHOD `m`, the assignment is an override of it: a `Base`-typed `b.m()` on a `C` runs the
|
|
5285
|
+
// assigned value. MEASURED: `class SubC extends BaseC { constructor(){ super(); this.m = () => write } }` with a
|
|
5286
|
+
// `BaseC`-typed `b.m()` was ABSENT at every gate (EXECUTED), the arrow charged to `SubC.constructor` only.
|
|
5287
|
+
// · a function literal is minted (`mintCallTargetUnit`, containment edge kept: the assigning body still carries it)
|
|
5288
|
+
// and registered — a resolution;
|
|
5289
|
+
// · `this.m = this.m.bind(…)` / `this.m = this.m` re-installs the same method and registers nothing;
|
|
5290
|
+
// · any other value (a wrapper's result, another method's `.bind`, a parameter) is registered UNRESOLVED, so every
|
|
5291
|
+
// reader's `allResolved` gate discloses `dispatch:` rather than certifying the declared body alone.
|
|
5292
|
+
// A PROPERTY `m` is not this rule's: a written property slot is ⟨R103⟩'s, which already discloses.
|
|
5293
|
+
function registerAssignedOverrides(cls) {
|
|
5294
|
+
const memberNamed = (c, name) => (c.members ?? []).find((x) => ts.isMethodDeclaration(x) && x.name?.getText?.() === name);
|
|
5295
|
+
for (const mem of cls.members ?? []) {
|
|
5296
|
+
if (ts.getCombinedModifierFlags(mem) & ts.ModifierFlags.Static) continue;
|
|
5297
|
+
const body = (ts.isConstructorDeclaration(mem) || ts.isMethodDeclaration(mem) || ts.isGetAccessorDeclaration(mem)
|
|
5298
|
+
|| ts.isSetAccessorDeclaration(mem)) ? mem.body
|
|
5299
|
+
: (ts.isPropertyDeclaration(mem) && mem.initializer && ts.isArrowFunction(mem.initializer)) ? mem.initializer.body : null;
|
|
5300
|
+
if (!body) continue;
|
|
5301
|
+
(function walk(n) {
|
|
5302
|
+
if (n !== body && (ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isClassLike(n))) return;
|
|
5303
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
5304
|
+
&& ts.isPropertyAccessExpression(n.left) && n.left.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
|
5305
|
+
const name = n.left.name.getText();
|
|
5306
|
+
let anc = memberNamed(cls, name);
|
|
5307
|
+
for (let b = localBaseClassOf(cls), g = 0; !anc && b && g++ < 64; b = localBaseClassOf(b)) anc = memberNamed(b, name);
|
|
5308
|
+
if (anc) {
|
|
5309
|
+
const rhs = unwrapArgExpr(n.right);
|
|
5310
|
+
const bound = rhs && ts.isCallExpression(rhs) ? unwrapBind(rhs) : null;
|
|
5311
|
+
const ref = bound ? bound.ref : rhs;
|
|
5312
|
+
const same = ref && ts.isPropertyAccessExpression(ref) && ref.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
5313
|
+
&& ref.name.getText() === name;
|
|
5314
|
+
if (!same) {
|
|
5315
|
+
const node = rhs && (ts.isArrowFunction(rhs) || ts.isFunctionExpression(rhs)) ? rhs : n;
|
|
5316
|
+
if (node === rhs && !callTargetUnit(rhs)) mintCallTargetUnit(rhs);
|
|
5317
|
+
if (!classOverrides.has(anc)) classOverrides.set(anc, []);
|
|
5318
|
+
const arr = classOverrides.get(anc);
|
|
5319
|
+
if (!arr.includes(node)) { arr.push(node); assignedOverrideClass.set(node, cls); }
|
|
5320
|
+
if (process.env.CANDOR_R873_REACH) console.error(`R873-REACH assigned ${node === rhs ? "fn" : "opaque"} ${name}`);
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
}
|
|
5324
|
+
ts.forEachChild(n, walk);
|
|
5325
|
+
})(body);
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
5328
|
+
|
|
5222
5329
|
// callback-flow bookkeeping (the Rust engine's callback_named move, ported): for every call that
|
|
5223
5330
|
// edges to a LOCAL unit, record what each argument position received — a NAMED local unit (a
|
|
5224
5331
|
// resolvable callback target), or an opaque value (an inline closure stays attributed to the
|
|
@@ -5233,7 +5340,8 @@ const callbackArgs = new Map(); // calleeName -> Map(argIndex -> Array<{calle
|
|
|
5233
5340
|
// What each argument position of a call to LOCAL unit `targetName` received (callback flow, see `callbackArgs`).
|
|
5234
5341
|
// Shared by the ordinary local arm and R955's callee-choice arm, so a callback passed through either is seen.
|
|
5235
5342
|
function registerCallbackArgs(rec, targetName, args) {
|
|
5236
|
-
args.forEach((
|
|
5343
|
+
args.forEach((a0, i) => {
|
|
5344
|
+
const a = unwrapArgExpr(a0); // R780: `run((f))` / `run(f as F)` pass the same function as `run(f)`
|
|
5237
5345
|
const slot = (callbackArgs.get(targetName) ?? callbackArgs.set(targetName, new Map()).get(targetName));
|
|
5238
5346
|
const list = slot.get(i) ?? [];
|
|
5239
5347
|
let target = null, opaque = false;
|
|
@@ -5372,8 +5480,7 @@ if (R103_HITS) process.on("exit", () => {
|
|
|
5372
5480
|
});
|
|
5373
5481
|
function openCallSlot(node) {
|
|
5374
5482
|
if (!ts.isCallExpression(node)) return null; // `new X()` constructs a class, not a slot
|
|
5375
|
-
|
|
5376
|
-
while (ts.isParenthesizedExpression(callee)) callee = callee.expression;
|
|
5483
|
+
const callee = unwrapArgExpr(node.expression); // R780
|
|
5377
5484
|
// ELEMENT ACCESS IS THE SAME SLOT BY ANOTHER SPELLING, and it is here because the first draft of this
|
|
5378
5485
|
// comment asserted the opposite — "an element access already reaches the dynamic-key/`callback:` arms"
|
|
5379
5486
|
// — and that was FALSE when measured: `this["handler"]()` and `this[k]()` (k narrowed to the literal
|
|
@@ -5439,8 +5546,8 @@ function unwrapBind(node, depth = 0) {
|
|
|
5439
5546
|
if (!node || depth > 8) return null;
|
|
5440
5547
|
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return null;
|
|
5441
5548
|
if (node.expression.name.text !== "bind") return null;
|
|
5442
|
-
|
|
5443
|
-
|
|
5549
|
+
// R780: `(f as F).bind(t)` / `(f satisfies F).bind(t)` bind the same function — the ONE wrapper set, not parens only.
|
|
5550
|
+
const recv = unwrapArgExpr(node.expression.expression);
|
|
5444
5551
|
// chained `.bind().bind()` — recurse only when the receiver is ITSELF a `.bind` call, else it's an
|
|
5445
5552
|
// arbitrary call (`getCallback().bind`) whose result we can't pin → unresolvable bind.
|
|
5446
5553
|
if (ts.isCallExpression(recv)) {
|
|
@@ -6091,11 +6198,7 @@ function isConversionPosition(n) {
|
|
|
6091
6198
|
}
|
|
6092
6199
|
// R958 — the value under an assertion chain: parentheses, `as`, `<T>`, `!` and `satisfies` change no runtime value.
|
|
6093
6200
|
let conversionViaAssertion = false; // set while `convert` records a source read THROUGH an assertion
|
|
6094
|
-
|
|
6095
|
-
while (e && (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isTypeAssertionExpression(e)
|
|
6096
|
-
|| ts.isNonNullExpression(e) || ts.isSatisfiesExpression?.(e))) e = e.expression;
|
|
6097
|
-
return e;
|
|
6098
|
-
}
|
|
6201
|
+
const peelAssertions = (e) => unwrapArgExpr(e); // R780: one wrapper set, not a third copy of it
|
|
6099
6202
|
// R82 / PART 87: is the conversion's SOURCE type a fact about the VALUE, or only about a generic signature? A
|
|
6100
6203
|
// call whose declared return type mentions one of its own type parameters (`wrap<T>(x: T): T`) hands back
|
|
6101
6204
|
// whatever type the ARGUMENT had, and that signature proves assignability, never that the value IS the
|
|
@@ -6129,11 +6232,175 @@ function conversionSourceTracked(e, depth = 0) {
|
|
|
6129
6232
|
}
|
|
6130
6233
|
return true;
|
|
6131
6234
|
}
|
|
6235
|
+
// ---- PARAMETER PROVENANCE: what every VISIBLE call site binds to a parameter (SOUNDNESS R934, R246) ----
|
|
6236
|
+
// A parameter's value is whatever its callers pass. For a function whose every reference is a CALL — not
|
|
6237
|
+
// handed out as a value, not `.call`/`.apply`/`.bind`-ed — the visible call sites ARE its bindings, and the
|
|
6238
|
+
// arguments there say what the parameter can hold. Two consumers ask this ONE index rather than each
|
|
6239
|
+
// walking calls: the environment-location model (a parameter every site hands `process.env`, R934) and
|
|
6240
|
+
// the `any`-receiver accessor arm (an `any`-typed parameter whose every site passes a typed value, R246).
|
|
6241
|
+
// `paramArgs` maps a parameter key (its symbol; a destructured parameter's NODE) to the argument expression
|
|
6242
|
+
// at each visible site (`null` where a spread hides the position; the default, or `null` for `undefined`,
|
|
6243
|
+
// where the site omits it). `paramFnEscaped(fnSym)` answers whether the function escapes, batched.
|
|
6244
|
+
const paramArgs = new Map();
|
|
6245
|
+
const paramOwnerOf = new Map(); // param key -> function symbol (for the escape question)
|
|
6246
|
+
const paramDeclOf = new Map(); // param key -> its ParameterDeclaration
|
|
6247
|
+
let paramFnEscaped;
|
|
6248
|
+
// A class some project class `extends` — its members are excluded (an override is invisible here).
|
|
6249
|
+
const extendedClassSyms = new Set();
|
|
6250
|
+
const classSymOf = (cls) => (cls?.name ? checker.getSymbolAtLocation(cls.name) : cls?.symbol) ?? null;
|
|
6251
|
+
{
|
|
6252
|
+
const noteArg = (k, v) => { if (k) (paramArgs.get(k) ?? paramArgs.set(k, []).get(k)).push(v); };
|
|
6253
|
+
const collectHeritage = (node) => {
|
|
6254
|
+
if ((ts.isClassDeclaration(node) || ts.isClassExpression(node)) && node.heritageClauses) {
|
|
6255
|
+
for (const h of node.heritageClauses) if (h.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
6256
|
+
for (const t of h.types) {
|
|
6257
|
+
let s = checker.getSymbolAtLocation(t.expression);
|
|
6258
|
+
if (s && (s.flags & ts.SymbolFlags.Alias)) { try { s = checker.getAliasedSymbol(s); } catch { /* keep */ } }
|
|
6259
|
+
if (s) extendedClassSyms.add(s);
|
|
6260
|
+
}
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
ts.forEachChild(node, collectHeritage);
|
|
6264
|
+
};
|
|
6265
|
+
for (const sf of sources) collectHeritage(sf);
|
|
6266
|
+
const calls = [];
|
|
6267
|
+
const collectCalls = (node) => {
|
|
6268
|
+
if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && (node.arguments?.length ?? 0) > 0) calls.push(node);
|
|
6269
|
+
ts.forEachChild(node, collectCalls);
|
|
6270
|
+
};
|
|
6271
|
+
for (const sf of sources) collectCalls(sf);
|
|
6272
|
+
// ⟨R934⟩ the functions whose parameters are eligible locations: a function declaration, a function or
|
|
6273
|
+
// arrow bound to a `const`, a constructor, or a method of a class with no heritage that nothing extends.
|
|
6274
|
+
// Every reference to it must be a CALL (or an export, a `typeof`, a member read that is not
|
|
6275
|
+
// `.call`/`.apply`/`.bind`) — anything else hands it to code that binds its parameters unseen.
|
|
6276
|
+
const fnEligible = new Map(); // declaration -> boolean
|
|
6277
|
+
const fnSymOf = (decl) => {
|
|
6278
|
+
if (ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl)) return decl.name ? checker.getSymbolAtLocation(decl.name) : null;
|
|
6279
|
+
if ((ts.isArrowFunction(decl) || ts.isFunctionExpression(decl)) && ts.isVariableDeclaration(decl.parent)
|
|
6280
|
+
&& decl.parent.initializer === decl && ts.isIdentifier(decl.parent.name)
|
|
6281
|
+
&& (ts.getCombinedNodeFlags(decl.parent) & ts.NodeFlags.Const)) return checker.getSymbolAtLocation(decl.parent.name);
|
|
6282
|
+
if (ts.isConstructorDeclaration(decl)) return classSymOf(decl.parent);
|
|
6283
|
+
return null;
|
|
6284
|
+
};
|
|
6285
|
+
const declShapeOk = (decl) => {
|
|
6286
|
+
if (!decl?.body || !projectFiles.has(path.resolve(decl.getSourceFile().fileName))) return false;
|
|
6287
|
+
if (ts.isFunctionDeclaration(decl) || ts.isArrowFunction(decl) || ts.isFunctionExpression(decl)) return true;
|
|
6288
|
+
if (ts.isConstructorDeclaration(decl)) return true;
|
|
6289
|
+
if (ts.isMethodDeclaration(decl) && (ts.isClassDeclaration(decl.parent) || ts.isClassExpression(decl.parent)))
|
|
6290
|
+
return !decl.parent.heritageClauses?.length && !extendedClassSyms.has(classSymOf(decl.parent));
|
|
6291
|
+
return false;
|
|
6292
|
+
};
|
|
6293
|
+
const escapedFnSyms = new Set();
|
|
6294
|
+
const escapeScannedSyms = new Set();
|
|
6295
|
+
// One traversal per BATCH of function symbols not yet asked about; the answer is cached.
|
|
6296
|
+
const scanEscapes = (cands0) => {
|
|
6297
|
+
const cands = new Set([...cands0].filter((x) => x && !escapeScannedSyms.has(x)));
|
|
6298
|
+
if (!cands.size) return;
|
|
6299
|
+
for (const c of cands) escapeScannedSyms.add(c);
|
|
6300
|
+
const names = new Set([...cands].map((s) => s.name));
|
|
6301
|
+
const SAFE_MEMBER_BLOCK = new Set(["call", "apply", "bind"]);
|
|
6302
|
+
const visit = (node) => {
|
|
6303
|
+
if (ts.isIdentifier(node) && names.has(node.text)) {
|
|
6304
|
+
let s = checker.getSymbolAtLocation(node);
|
|
6305
|
+
if (s && (s.flags & ts.SymbolFlags.Alias)) { try { s = checker.getAliasedSymbol(s); } catch { /* keep */ } }
|
|
6306
|
+
if (s && cands.has(s) && !escapedFnSyms.has(s)) {
|
|
6307
|
+
// climb the transparent wrappers to the position the value lands in
|
|
6308
|
+
let cur = node;
|
|
6309
|
+
const p0 = node.parent;
|
|
6310
|
+
let safe = false;
|
|
6311
|
+
if (p0 && (ts.isPropertyAccessExpression(p0) || ts.isPropertyAssignment(p0) || ts.isMethodDeclaration(p0)
|
|
6312
|
+
|| ts.isFunctionDeclaration(p0) || ts.isClassDeclaration(p0) || ts.isClassExpression(p0)
|
|
6313
|
+
|| ts.isVariableDeclaration(p0) || ts.isPropertyDeclaration(p0)) && p0.name === node) {
|
|
6314
|
+
// a declaration name, or `o.name` — a METHOD reference: safe only as the callee of a call
|
|
6315
|
+
if (ts.isPropertyAccessExpression(p0)) {
|
|
6316
|
+
const pp = p0.parent;
|
|
6317
|
+
safe = !!pp && (ts.isCallExpression(pp) || ts.isNewExpression(pp)) && pp.expression === p0;
|
|
6318
|
+
} else safe = true;
|
|
6319
|
+
} else {
|
|
6320
|
+
while (cur.parent && (ts.isParenthesizedExpression(cur.parent) || ts.isNonNullExpression(cur.parent))) cur = cur.parent;
|
|
6321
|
+
const p = cur.parent;
|
|
6322
|
+
if (!p) safe = true;
|
|
6323
|
+
else if ((ts.isCallExpression(p) || ts.isNewExpression(p)) && p.expression === cur) safe = true;
|
|
6324
|
+
else if (ts.isPropertyAccessExpression(p) && p.expression === cur) safe = !SAFE_MEMBER_BLOCK.has(p.name.text);
|
|
6325
|
+
else if (ts.isExportSpecifier(p) || ts.isExportAssignment(p) || ts.isTypeQueryNode(p)
|
|
6326
|
+
|| ts.isTypeOfExpression(p) || ts.isExpressionWithTypeArguments(p) || ts.isImportSpecifier(p)
|
|
6327
|
+
|| ts.isImportClause(p) || ts.isNamespaceImport(p) || ts.isTypeReferenceNode(p)
|
|
6328
|
+
|| ts.isQualifiedName(p)) safe = true;
|
|
6329
|
+
else if (ts.isBinaryExpression(p) && p.right === cur && p.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) safe = true;
|
|
6330
|
+
}
|
|
6331
|
+
if (!safe) escapedFnSyms.add(s);
|
|
6332
|
+
}
|
|
6333
|
+
}
|
|
6334
|
+
ts.forEachChild(node, visit);
|
|
6335
|
+
};
|
|
6336
|
+
for (const sf of sources) visit(sf);
|
|
6337
|
+
};
|
|
6338
|
+
// Seed the parameter bindings: for every call whose resolved declaration is an eligible shape, bind each
|
|
6339
|
+
// identifier parameter to its argument (or default / undefined). Only calls that hand over at least one
|
|
6340
|
+
// argument are recorded here — a site that passes nothing binds `undefined` to every parameter, which
|
|
6341
|
+
// is recorded by the per-declaration site count below.
|
|
6342
|
+
const sitesByDecl = new Map(); // decl -> number of call sites seen
|
|
6343
|
+
for (const call of calls) {
|
|
6344
|
+
let decl;
|
|
6345
|
+
try { decl = checker.getResolvedSignature(call)?.declaration; } catch { decl = null; }
|
|
6346
|
+
if (!decl || !declShapeOk(decl)) continue;
|
|
6347
|
+
if (!fnEligible.has(decl)) fnEligible.set(decl, !!fnSymOf(decl));
|
|
6348
|
+
if (!fnEligible.get(decl)) continue;
|
|
6349
|
+
sitesByDecl.set(decl, (sitesByDecl.get(decl) ?? 0) + 1);
|
|
6350
|
+
const fsym = fnSymOf(decl);
|
|
6351
|
+
let pos = 0, sawSpread = false;
|
|
6352
|
+
const args = call.arguments ?? [];
|
|
6353
|
+
const argAt = [];
|
|
6354
|
+
for (const a of args) { if (ts.isSpreadElement(a)) { sawSpread = true; break; } argAt[pos++] = a; }
|
|
6355
|
+
decl.parameters.forEach((p, i) => {
|
|
6356
|
+
if (p.dotDotDotToken) return;
|
|
6357
|
+
// a DESTRUCTURED parameter reads its keys when the frame is entered — keyed by the parameter node
|
|
6358
|
+
const ps = ts.isIdentifier(p.name) ? checker.getSymbolAtLocation(p.name)
|
|
6359
|
+
: ts.isObjectBindingPattern(p.name) ? p : null;
|
|
6360
|
+
if (!ps) return;
|
|
6361
|
+
paramOwnerOf.set(ps, fsym);
|
|
6362
|
+
if (i < argAt.length && argAt[i]) noteArg(ps, argAt[i]);
|
|
6363
|
+
else if (sawSpread) noteArg(ps, null);
|
|
6364
|
+
else noteArg(ps, p.initializer ?? null);
|
|
6365
|
+
});
|
|
6366
|
+
}
|
|
6367
|
+
// A call site that resolves to the declaration with NO arguments was not collected above; it binds
|
|
6368
|
+
// every parameter to its default or `undefined`. Count them so a zero-argument site is not lost.
|
|
6369
|
+
{
|
|
6370
|
+
const visitZero = (node) => {
|
|
6371
|
+
if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && (node.arguments?.length ?? 0) === 0) {
|
|
6372
|
+
let decl;
|
|
6373
|
+
try { decl = checker.getResolvedSignature(node)?.declaration; } catch { decl = null; }
|
|
6374
|
+
if (decl && sitesByDecl.has(decl)) for (const p of decl.parameters)
|
|
6375
|
+
if (!p.dotDotDotToken && ts.isIdentifier(p.name)) noteArg(checker.getSymbolAtLocation(p.name), p.initializer ?? null);
|
|
6376
|
+
else if (!p.dotDotDotToken && ts.isObjectBindingPattern(p.name)) noteArg(p, p.initializer ?? null);
|
|
6377
|
+
}
|
|
6378
|
+
ts.forEachChild(node, visitZero);
|
|
6379
|
+
};
|
|
6380
|
+
if (sitesByDecl.size) for (const sf of sources) visitZero(sf);
|
|
6381
|
+
}
|
|
6382
|
+
for (const k of paramArgs.keys()) { const d = ts.isParameter(k) ? k : k.valueDeclaration; if (d && ts.isParameter(d)) paramDeclOf.set(k, d); }
|
|
6383
|
+
paramFnEscaped = (fsym) => { if (!fsym) return true; scanEscapes([fsym]); return escapedFnSyms.has(fsym); };
|
|
6384
|
+
// Batch form: answer for many at once with ONE traversal.
|
|
6385
|
+
paramFnEscaped.batch = (fsyms) => scanEscapes(fsyms);
|
|
6386
|
+
}
|
|
6387
|
+
|
|
6132
6388
|
const CONFORMER_REACH = process.env.CANDOR_CONFORMER_REACH ? (k, n) => console.error(`CONFORMER-REACH ${k} ${n}`) : null;
|
|
6133
6389
|
for (const sf of sources) {
|
|
6134
6390
|
const convert = (src, tt, at, kind) => {
|
|
6135
6391
|
if (!tt || !conversionSourceTracked(src)) return;
|
|
6136
6392
|
let st; try { st = checker.getTypeAtLocation(src); } catch { st = undefined; }
|
|
6393
|
+
// ⟨SOUNDNESS R958, the `any`-PARAMETER half⟩ a source typed `any`/`unknown` names no conformer, so a value
|
|
6394
|
+
// laundered through an `any` parameter reached a closed dispatch silently: `launder(x: any) { qDisp(x) }`
|
|
6395
|
+
// called as `launder(new LocalW())` ran LocalW's `m` (EXECUTED) with `deny Fs` exit 0 on the caller. The
|
|
6396
|
+
// parameter's visible arguments say what it holds — the SAME provenance index R246's accessor arm asks
|
|
6397
|
+
// (`receiverValueTypes`) — so each argument type is recorded as a conversion source. Upcasts only, as
|
|
6398
|
+
// ever (`recordConversion` keeps a target the type is assignable to). A dependency-sourced value and a
|
|
6399
|
+
// lying downcast are untouched: they are R958's by-design residue.
|
|
6400
|
+
if (anyishType(st)) for (const vt of receiverValueTypes(src)) {
|
|
6401
|
+
try { recordConversion(vt, tt); } catch { /* best effort, as the main record below */ }
|
|
6402
|
+
CONFORMER_REACH?.("any-param", `${path.relative(rootDir, sf.fileName)}:${at.getStart()}`);
|
|
6403
|
+
}
|
|
6137
6404
|
conversionViaAssertion = kind !== "conversion";
|
|
6138
6405
|
try {
|
|
6139
6406
|
if (!CONFORMER_REACH) { recordConversion(st, tt); return; }
|
|
@@ -6308,6 +6575,47 @@ function keyLiteralNames(t, depth = 0, seen = new Set()) {
|
|
|
6308
6575
|
if (c && c !== t) return keyLiteralNames(c, depth + 1, seen);
|
|
6309
6576
|
return null; // `string`, `symbol`, `keyof T` for generic T, …
|
|
6310
6577
|
}
|
|
6578
|
+
// SOUNDNESS R246 — THE RECEIVER'S VALUE, WHEN ITS STATIC TYPE SAYS NOTHING. Accessor lookup asks the
|
|
6579
|
+
// receiver's TYPE which properties it declares, and `any` declares none: `(s as any)[k] = v`,
|
|
6580
|
+
// `(s as any).token = v` and `function w(o: any) { o.token = v }` called as `w(new S())` all invoked S's
|
|
6581
|
+
// setter (EXECUTED, 6 of 6 writes observed) and were ABSENT with `deny Fs <fn>` exit 0 — the caller too.
|
|
6582
|
+
// The value is not unknowable; only its static type was thrown away. Two places say what it really is,
|
|
6583
|
+
// and both are facts rather than guesses:
|
|
6584
|
+
// - an ASSERTION changes no runtime value (R958's look-through, the same `peelAssertions`): the operand
|
|
6585
|
+
// under `as any` / `as unknown as T` / `<any>x` has the type the value really carries;
|
|
6586
|
+
// - an `any`/`unknown` PARAMETER holds what its visible call sites pass (the shared PARAMETER PROVENANCE
|
|
6587
|
+
// index, R934) — only for a function whose every reference is a call; an escaped one is unanswered.
|
|
6588
|
+
// Returns the EXTRA types beyond the checker's own (empty when the checker's type is informative and no
|
|
6589
|
+
// assertion intervenes), so a typed receiver's lookup is byte-identical to before. This RESOLVES — it adds
|
|
6590
|
+
// the accessor edges the value's type declares — and never hedges; a receiver neither rule answers is
|
|
6591
|
+
// exactly as it was (the residual is stated in SOUNDNESS R246, not hidden here).
|
|
6592
|
+
function anyishType(t) { return !!t && !!(t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)); } // hoisted: the conversion walk asks it first
|
|
6593
|
+
function receiverValueTypes(expr, depth = 0, seen = new Set()) {
|
|
6594
|
+
if (!expr || depth > 6) return [];
|
|
6595
|
+
let t; try { t = checker.getTypeAtLocation(expr); } catch { t = null; }
|
|
6596
|
+
const inner = peelAssertions(expr);
|
|
6597
|
+
if (inner && inner !== expr) {
|
|
6598
|
+
const below = receiverValueTypes(inner, depth + 1, seen);
|
|
6599
|
+
let ti; try { ti = checker.getTypeAtLocation(inner); } catch { ti = null; }
|
|
6600
|
+
return [...(ti && !anyishType(ti) ? [ti] : []), ...below];
|
|
6601
|
+
}
|
|
6602
|
+
if (!anyishType(t) || !ts.isIdentifier(expr)) return [];
|
|
6603
|
+
const sym = checker.getSymbolAtLocation(expr);
|
|
6604
|
+
const d = sym?.valueDeclaration;
|
|
6605
|
+
if (!sym || !d || !ts.isParameter(d) || seen.has(sym)) return [];
|
|
6606
|
+
seen.add(sym);
|
|
6607
|
+
const args = paramArgs.get(sym);
|
|
6608
|
+
if (!args || paramFnEscaped(paramOwnerOf.get(sym))) return [];
|
|
6609
|
+
const out = [];
|
|
6610
|
+
for (const a of args) {
|
|
6611
|
+
if (!a) continue;
|
|
6612
|
+
let ta; try { ta = checker.getTypeAtLocation(a); } catch { ta = null; }
|
|
6613
|
+
if (ta && !anyishType(ta)) out.push(ta);
|
|
6614
|
+
out.push(...receiverValueTypes(a, depth + 1, seen));
|
|
6615
|
+
}
|
|
6616
|
+
return out;
|
|
6617
|
+
}
|
|
6618
|
+
const dedupeHits = (hits) => { const seen = new Set(); return hits.filter((h) => !seen.has(h.decl) && seen.add(h.decl)); };
|
|
6311
6619
|
function accessorsAt(propNode, kind /* "get" | "set" */) {
|
|
6312
6620
|
if (ts.isElementAccessExpression(propNode)) {
|
|
6313
6621
|
// `c["prop"]` carries no `.name`; resolve the key to the property NAMES it can hold and look each
|
|
@@ -6324,9 +6632,19 @@ function accessorsAt(propNode, kind /* "get" | "set" */) {
|
|
|
6324
6632
|
// property is common to all of them).
|
|
6325
6633
|
for (const t of texts) for (const sym of propertySymbolsAcrossArms(recvType, t))
|
|
6326
6634
|
out.push(...accessorsFromSym(sym, kind));
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6635
|
+
// R246 — and every type the receiver's VALUE can carry under an assertion or an `any` parameter.
|
|
6636
|
+
for (const vt of receiverValueTypes(propNode.expression))
|
|
6637
|
+
for (const t of texts) for (const sym of propertySymbolsAcrossArms(vt, t)) out.push(...accessorsFromSym(sym, kind));
|
|
6638
|
+
return dedupeHits(out);
|
|
6639
|
+
}
|
|
6640
|
+
const direct = accessorsFromSym(checker.getSymbolAtLocation(propNode.name ?? propNode), kind);
|
|
6641
|
+
if (direct.length || !ts.isPropertyAccessExpression(propNode)) return direct;
|
|
6642
|
+
// R246 — `(s as any).token = v` / `o.token = v` with `o: any`: the checker resolves no symbol at all.
|
|
6643
|
+
const out = [];
|
|
6644
|
+
const name = propNode.name.text;
|
|
6645
|
+
for (const vt of receiverValueTypes(propNode.expression))
|
|
6646
|
+
for (const sym of propertySymbolsAcrossArms(vt, name)) out.push(...accessorsFromSym(sym, kind));
|
|
6647
|
+
return dedupeHits(out);
|
|
6330
6648
|
}
|
|
6331
6649
|
// A `Object.defineProperty` descriptor accessor for the forcing site `recv.key` (read → get, assign →
|
|
6332
6650
|
// set), consulted ONLY when the type-level `accessorsAt` came up empty (the checker types target.key as
|
|
@@ -6406,8 +6724,7 @@ function accessorOverrideFanOut(rec, decl, recvExpr) {
|
|
|
6406
6724
|
// SOUNDNESS-PRESERVING FALLBACK, the method path's verbatim: a receiver we cannot pin to a LOCAL
|
|
6407
6725
|
// class (a union, an interface, `any`, an external type) keeps the FULL override set.
|
|
6408
6726
|
const rootClass = localReceiverClass(recvExpr);
|
|
6409
|
-
const direct = rootClass ? allOverrides.filter((om) =>
|
|
6410
|
-
ts.isClassDeclaration(om.parent) && classInSubtree(om.parent, rootClass)) : allOverrides;
|
|
6727
|
+
const direct = rootClass ? allOverrides.filter((om) => classInSubtree(overrideOwner(om), rootClass)) : allOverrides;
|
|
6411
6728
|
const overrides = memberDispatchBodies(decl, rootClass);
|
|
6412
6729
|
if (direct.length <= CHA_FANOUT_LIMIT) probeR871("accessor", decl, overrides.length - direct.length);
|
|
6413
6730
|
if (overrides.length === 0) return;
|
|
@@ -6658,8 +6975,10 @@ function classBodiedGetter(sym) {
|
|
|
6658
6975
|
}
|
|
6659
6976
|
function enumerateGetters(owner, type, srcExpr) {
|
|
6660
6977
|
if (!owner) return;
|
|
6661
|
-
|
|
6662
|
-
|
|
6978
|
+
// R246 — the copied VALUE's types too, when its static type is `any`/`unknown` or asserted away.
|
|
6979
|
+
const vprops = srcExpr ? receiverValueTypes(srcExpr).flatMap((vt) => propertiesAcrossArms(vt)) : [];
|
|
6980
|
+
if ((type && type.getProperties) || vprops.length) {
|
|
6981
|
+
for (const p of [...(type?.getProperties ? propertiesAcrossArms(type) : []), ...vprops]) { // R259 — the disjunction, not the intersection
|
|
6663
6982
|
if (classBodiedGetter(p)) continue; // prototype + non-enumerable → not copied by a spread
|
|
6664
6983
|
for (const hit of accessorsFromSym(p, "get")) recordAccessorHit(owner, hit, p.getName(), srcExpr);
|
|
6665
6984
|
}
|
|
@@ -6670,9 +6989,7 @@ function enumerateGetters(owner, type, srcExpr) {
|
|
|
6670
6989
|
//
|
|
6671
6990
|
// R120 — THREE UNWRAPS AND ONE ALIAS HOP, because the question is which OBJECT is being copied and
|
|
6672
6991
|
// none of these four wrappers changes that answer. Each is a spelling measured absent at e5c60bc.
|
|
6673
|
-
|
|
6674
|
-
while (se && (ts.isParenthesizedExpression(se) || ts.isAsExpression(se)
|
|
6675
|
-
|| ts.isSatisfiesExpression(se) || ts.isNonNullExpression(se))) se = se.expression;
|
|
6992
|
+
const se = unwrapArgExpr(srcExpr); // R780
|
|
6676
6993
|
if (!se || !ts.isIdentifier(se)) return; // parameter / call return / member
|
|
6677
6994
|
let sym0 = checker.getSymbolAtLocation(se);
|
|
6678
6995
|
if (!sym0) return;
|
|
@@ -6682,9 +6999,7 @@ function enumerateGetters(owner, type, srcExpr) {
|
|
|
6682
6999
|
if (sym0.flags & ts.SymbolFlags.Alias) { try { sym0 = checker.getAliasedSymbol(sym0) ?? sym0; } catch { /* unresolved import */ } }
|
|
6683
7000
|
for (const d of sym0.declarations ?? []) {
|
|
6684
7001
|
if (!ts.isVariableDeclaration(d) || !d.initializer) continue;
|
|
6685
|
-
|
|
6686
|
-
while (ts.isParenthesizedExpression(init) || ts.isAsExpression(init)
|
|
6687
|
-
|| ts.isSatisfiesExpression(init) || ts.isNonNullExpression(init)) init = init.expression;
|
|
7002
|
+
const init = unwrapArgExpr(d.initializer); // R780
|
|
6688
7003
|
if (!ts.isObjectLiteralExpression(init)) continue;
|
|
6689
7004
|
for (const pr of init.properties) {
|
|
6690
7005
|
if (!ts.isGetAccessorDeclaration(pr)) continue;
|
|
@@ -6729,8 +7044,7 @@ function enumerateGetters(owner, type, srcExpr) {
|
|
|
6729
7044
|
const provenCopiedKeys = (sources) => {
|
|
6730
7045
|
const keys = new Set();
|
|
6731
7046
|
for (let s of sources) {
|
|
6732
|
-
|
|
6733
|
-
|| ts.isSatisfiesExpression(s) || ts.isNonNullExpression(s))) s = s.expression;
|
|
7047
|
+
s = unwrapArgExpr(s); // R780
|
|
6734
7048
|
if (!s || !ts.isObjectLiteralExpression(s)) return null; // parameter / call return / variable
|
|
6735
7049
|
for (const pr of s.properties) {
|
|
6736
7050
|
if (ts.isSpreadAssignment(pr)) return null; // `{...o}` copies an unknown key set
|
|
@@ -6808,12 +7122,15 @@ function enumerateTargetAccessors(owner, targetExpr, keys, kind /* "get" | "set"
|
|
|
6808
7122
|
if (!owner || !targetExpr) return;
|
|
6809
7123
|
const t = checker.getTypeAtLocation(targetExpr);
|
|
6810
7124
|
if (!t || !t.getProperties) return;
|
|
7125
|
+
// R246 — every property the target's VALUE can carry: its static type's, and (under an assertion or an
|
|
7126
|
+
// `any`/`unknown` parameter) the types the value really has. One list for both branches below.
|
|
7127
|
+
const tprops = [...propertiesAcrossArms(t), ...receiverValueTypes(targetExpr).flatMap((vt) => propertiesAcrossArms(vt))];
|
|
6811
7128
|
// `!keys`, not `keys === null`: a Set is always truthy, so this is exactly "the key set is not
|
|
6812
7129
|
// provable" — and it keeps the tolerance the old `keys && !keys.has(…)` line had for a caller that
|
|
6813
7130
|
// passes nothing. `keys.has` below is now unguarded, so a future third call site omitting the
|
|
6814
7131
|
// argument would throw rather than take this branch.
|
|
6815
7132
|
if (!keys) {
|
|
6816
|
-
for (const p of
|
|
7133
|
+
for (const p of tprops) { // R259 — the disjunction, not the intersection
|
|
6817
7134
|
if (!accessorsFromSym(p, kind).length) continue;
|
|
6818
7135
|
if (!keyCouldNameAccessor(p, unprovable.keyType ?? null)) continue;
|
|
6819
7136
|
const rec = fns.get(owner);
|
|
@@ -6823,7 +7140,9 @@ function enumerateTargetAccessors(owner, targetExpr, keys, kind /* "get" | "set"
|
|
|
6823
7140
|
}
|
|
6824
7141
|
return; // a target declaring no reachable accessor discloses nothing (0 real invocations)
|
|
6825
7142
|
}
|
|
6826
|
-
|
|
7143
|
+
const seenP = new Set();
|
|
7144
|
+
for (const p of tprops) { // R259 — the disjunction, not the intersection
|
|
7145
|
+
if (seenP.has(p)) continue; seenP.add(p);
|
|
6827
7146
|
// NO `classBodiedGetter`-style exclusion here, and the asymmetry is the point rather than an
|
|
6828
7147
|
// oversight: R115 excluded a class-bodied GETTER because a prototype accessor is non-enumerable and
|
|
6829
7148
|
// therefore never COPIED. A prototype SETTER is the opposite — it is found by the assignment's
|
|
@@ -6927,6 +7246,29 @@ function decoratorArgUnit(callNode) {
|
|
|
6927
7246
|
}
|
|
6928
7247
|
return qual;
|
|
6929
7248
|
}
|
|
7249
|
+
// ⟨SOUNDNESS R815⟩ The `extends <expr>` unit of a class that has a constructor unit. The expression runs ONCE, at
|
|
7250
|
+
// class definition (so the evaluating unit edges here, pass 2a′) — and every construction then runs the
|
|
7251
|
+
// constructor of the class it produced through `super`, implicit or explicit, whose body this scan sees only as
|
|
7252
|
+
// the expression's own callees (`mixin()`'s class expression is attributed to `mixin`; a dependency's
|
|
7253
|
+
// `PartialType(X)` is the dependency's ledger/`invisible`). So the constructor edges here too. Attributing the
|
|
7254
|
+
// expression to the constructor ALONE (the old climb) left the definer silent; attributing it to the definer
|
|
7255
|
+
// ALONE drops the super reach — MEASURED: `new D3()` over `class D3 extends Partial(Plain)` from a dependency
|
|
7256
|
+
// that writes in its generated constructor (EXECUTED) went from `invisible:['mixdep']` to ABSENT, and nest's six
|
|
7257
|
+
// `extends PartialType(…)` DTO constructors lost their `@nestjs/mapped-types` disclosure the same way.
|
|
7258
|
+
function heritageUnit(cls, ctorQual) {
|
|
7259
|
+
const qual = ctorQual.replace(/\.constructor$/, ".<heritage>");
|
|
7260
|
+
if (!fns.has(qual)) {
|
|
7261
|
+
const sf = cls.getSourceFile();
|
|
7262
|
+
const h = (cls.heritageClauses ?? []).find((c) => c.token === ts.SyntaxKind.ExtendsKeyword) ?? cls;
|
|
7263
|
+
fns.set(qual, { local: `${fns.get(ctorQual)?.local?.replace(/\.constructor$/, "") ?? "<anonymous>"}.<heritage>`,
|
|
7264
|
+
direct: new Set(), fsKinds: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
|
|
7265
|
+
cmds: new Set(), paths: new Set(), blind: new Set(), incomplete: new Set(), dispatch: new Set(), why: new Set(),
|
|
7266
|
+
entry: false, unitKind: "initializer",
|
|
7267
|
+
loc: `${path.relative(rootDir, sf.fileName)}:${sf.getLineAndCharacterOfPosition(h.getStart()).line + 1}:1`,
|
|
7268
|
+
endLine: sf.getLineAndCharacterOfPosition(h.getEnd()).line + 1 });
|
|
7269
|
+
}
|
|
7270
|
+
return qual;
|
|
7271
|
+
}
|
|
6930
7272
|
// nearest enclosing analyzed function (closures attribute to it — SEMANTICS §2)
|
|
6931
7273
|
function enclosing(node) {
|
|
6932
7274
|
let prev = null;
|
|
@@ -6966,6 +7308,36 @@ function enclosing(node) {
|
|
|
6966
7308
|
if (ts.isDecorator(p)) return null;
|
|
6967
7309
|
const n = nodeName.get(p);
|
|
6968
7310
|
if (n) return n;
|
|
7311
|
+
// ⟨SOUNDNESS R815⟩ THE REST OF THE CLASS-DEFINITION-TIME FAMILY R782/R785 started. Three more pieces of a class
|
|
7312
|
+
// run when the class is EVALUATED, not when it is constructed or a member is called — and the climb
|
|
7313
|
+
// attributed each to a unit that never runs it, leaving the evaluating unit silent (EXECUTED, lane fixture
|
|
7314
|
+
// `fx/r815`: the enclosing function ABSENT, `deny Fs` exit 0; the effect fabricated onto `C.constructor` or the
|
|
7315
|
+
// method):
|
|
7316
|
+
// · a `static x = …` INITIALISER runs at definition, exactly like a `static {}` block, so it lands on the
|
|
7317
|
+
// same `<static-init>` unit (which pass 2a′ wires from the evaluating unit). Climbing on reached the
|
|
7318
|
+
// ClassDeclaration, i.e. `C.constructor` — every `new C()` charged, the definer silent. An instance field
|
|
7319
|
+
// is NOT this (it runs per construction; the constructor is right), and a property that IS a unit
|
|
7320
|
+
// (`static h = () => …` minted as `C.h`) answered above before reaching here.
|
|
7321
|
+
// · an `extends <expr>` HERITAGE expression and
|
|
7322
|
+
// · a COMPUTED member name `[k()]` (class OR object-literal member)
|
|
7323
|
+
// are evaluated in the scope AROUND the class / literal, by whatever evaluates it — so the climb resumes
|
|
7324
|
+
// OUTSIDE the container. (The computed name hung under the member's own node, so the method `C3.[w("k")]`
|
|
7325
|
+
// was charged for its own name.)
|
|
7326
|
+
if (prev && ts.isPropertyDeclaration(p) && p.initializer === prev
|
|
7327
|
+
&& (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Static)
|
|
7328
|
+
&& p.parent && (ts.isClassDeclaration(p.parent) || ts.isClassExpression(p.parent)))
|
|
7329
|
+
{ R815_REACH?.("static"); return staticBlockUnit(p); }
|
|
7330
|
+
if (ts.isHeritageClause(p) && p.token === ts.SyntaxKind.ExtendsKeyword && p.parent
|
|
7331
|
+
&& (ts.isClassDeclaration(p.parent) || ts.isClassExpression(p.parent))) {
|
|
7332
|
+
const ctor = nodeName.get(p.parent);
|
|
7333
|
+
if (ctor) { R815_REACH?.("heritage"); return heritageUnit(p.parent, ctor); }
|
|
7334
|
+
// a class with no constructor unit: the climb already resumes outside it, at the evaluating unit
|
|
7335
|
+
}
|
|
7336
|
+
if (ts.isComputedPropertyName(p) && p.parent?.parent) {
|
|
7337
|
+
const container = p.parent.parent;
|
|
7338
|
+
if (ts.isClassDeclaration(container) || ts.isClassExpression(container)) { R815_REACH?.("ckey"); return enclosing(container.parent); }
|
|
7339
|
+
if (ts.isObjectLiteralExpression(container)) { R815_REACH?.("okey"); return enclosing(container); }
|
|
7340
|
+
}
|
|
6969
7341
|
// Reached the SourceFile with no named unit: a TOP-LEVEL executable statement. Attribute to the
|
|
6970
7342
|
// file's synthesized `<module>` initializer unit (minted lazily here) rather than dropping it.
|
|
6971
7343
|
if (ts.isSourceFile(p)) return moduleUnit(p);
|
|
@@ -7204,8 +7576,7 @@ function recordDispatch(rec, decl, pkg) {
|
|
|
7204
7576
|
function packageProducedReceiver(recvExpr, pkg) {
|
|
7205
7577
|
if (!recvExpr || !pkg) return null;
|
|
7206
7578
|
const unwrap = (x) => {
|
|
7207
|
-
|
|
7208
|
-
|| ts.isAsExpression(x))) x = x.expression;
|
|
7579
|
+
for (x = unwrapArgExpr(x); x && ts.isAwaitExpression(x);) x = unwrapArgExpr(x.expression); // R780: + `await`
|
|
7209
7580
|
return x;
|
|
7210
7581
|
};
|
|
7211
7582
|
let e = unwrap(recvExpr);
|
|
@@ -8357,6 +8728,89 @@ const HOF_INVOKERS = new Set([
|
|
|
8357
8728
|
"then", "catch", "finally", "nextTick",
|
|
8358
8729
|
]);
|
|
8359
8730
|
|
|
8731
|
+
// ⟨SOUNDNESS R803⟩ THE LIST ABOVE IS NO LONGER THE GATE — IT IS THE POSITION MAP FOR THE CALLEES IT NAMES.
|
|
8732
|
+
//
|
|
8733
|
+
// `HOF_INVOKERS` decided, by callee NAME, whether a function reference handed to a non-local callee could be
|
|
8734
|
+
// invoked by it, and every callee it did not name dropped the reference: no edge, no `Unknown`, no row.
|
|
8735
|
+
// Measured on `fx/r803`, every cell EXECUTED by node to run its handler: `e.on`, `e.once`, `e.addListener`,
|
|
8736
|
+
// `process.once`, `http.createServer(h)`, `Array.from(xs, f)`, `new Promise(exec)`, lodash `_.each`/`_.times`,
|
|
8737
|
+
// a dependency `queue.add(f)` and `EventTarget.addEventListener` were all ABSENT, `deny Fs` exit 0 on each
|
|
8738
|
+
// caller. The comment that guarded it called the omission "an honest under-report (sound)" — true about
|
|
8739
|
+
// fabrication, false about silence; it also falsified `KAPPA_PURE`'s premise that lodash/rxjs side effects
|
|
8740
|
+
// "live in visible user callbacks", because this list is what decides whether those callbacks are visible.
|
|
8741
|
+
//
|
|
8742
|
+
// WHY NOT "ASK THE SIGNATURE" ALONE — measured by the ts vein analysis on the 28-entry roster: of 253 sites
|
|
8743
|
+
// where a callee's parameter is callable-typed and the argument is a local function reference, about half are
|
|
8744
|
+
// REMOVALS or STORES (`off`, `removeListener`, `removeEventListener`, `Map.set`, `Object.assign`), and 15 of the
|
|
8745
|
+
// 25 effect-bearing ones never invoke. A signature rule alone fabricates on those.
|
|
8746
|
+
//
|
|
8747
|
+
// So the rule is: an UNLISTED non-local callee invokes every argument position its resolved signature
|
|
8748
|
+
// POSITIVELY declares callable (`calleeParamIsCallable === true`), unless the callee is on the DENYLIST below.
|
|
8749
|
+
// The failure directions, stated because they are the reason for the shape (`candor-denylist-over-allowlist`):
|
|
8750
|
+
// · a non-invoker MISSING from the denylist → an over-charge (its reference's effects charged to the
|
|
8751
|
+
// caller). Visible, and the corpus A/B is the instrument that catches it.
|
|
8752
|
+
// · an invoker the old list missed → now charged. Nothing an invoker can do lands it back in silence
|
|
8753
|
+
// except being DENYLISTED, and the denylist only names declarations whose semantics are a removal or a
|
|
8754
|
+
// store — the removal NAMES on any receiver, and reviewed ES-lib / node-core members by owner.
|
|
8755
|
+
// · a callee whose parameter is `any`/`unknown`/unresolvable (`null`) is NOT admitted here. That is a
|
|
8756
|
+
// residual, named rather than hidden: `console.log(f)`/`assert.equal(f, …)` take `any` and never invoke,
|
|
8757
|
+
// and admitting `null` charges them — priced on the roster before deciding (see the commit message).
|
|
8758
|
+
// The listed names keep their own position map (`hofInvokesArg`/`hofArgIsNeverCallback`) unchanged.
|
|
8759
|
+
//
|
|
8760
|
+
// Removal by NAME on ANY receiver: a method named `removeListener` that invokes the listener it is removing
|
|
8761
|
+
// would be perverse, and these four are the registration API's inverse everywhere they appear (node events,
|
|
8762
|
+
// DOM `EventTarget`, eventemitter3, socket.io's emitter). Asked of the callee's spelling AND of the declaration
|
|
8763
|
+
// it resolved to, because `const processOff = process.off.bind(process); processOff(sig, h)` (vitest) and
|
|
8764
|
+
// got's `removeSessionSocketListener` are the same removal under a local name.
|
|
8765
|
+
//
|
|
8766
|
+
// Everything else is asked of the RESOLVED REVIEWED-LIBRARY declaration (ES lib, @types/node), never of a
|
|
8767
|
+
// name, so a DEPENDENCY's `add(fn)` (p-queue — a real roster silence) is not excused by sharing a name with
|
|
8768
|
+
// `Set.add`. The rule there is the GENERIC VALUE SLOT: a parameter whose declared type is a bare type
|
|
8769
|
+
// parameter (alone or beside non-callable members such as `PromiseLike<T>`) whose constraint has no call
|
|
8770
|
+
// signature. Such a callee is handed a value it knows nothing about and cannot call without a cast — `Map.set`'s
|
|
8771
|
+
// `V`, `Set.add`/`includes`/`indexOf`'s `T`, `Object.assign`/`defineProperties`/`freeze`'s `T`, a promise
|
|
8772
|
+
// executor's `resolve(value: T | PromiseLike<T>)`, `new Proxy(target: T)`, `new WeakRef(target: T)`. The
|
|
8773
|
+
// argument reads callable only because the CALL instantiated `T` with a function type. Restricted to the
|
|
8774
|
+
// reviewed library declarations on purpose: a dependency CAN cast and call (`(x as any)()`), so there the
|
|
8775
|
+
// generic slot is an assumption, and an assumption is not denylist evidence.
|
|
8776
|
+
// ⟨R241⟩ lib.dom interfaces whose every member touches a persistent browser store (see the es-lib call arm).
|
|
8777
|
+
const BROWSER_STORE_IFACES = new Set(["IDBFactory", "IDBDatabase", "IDBObjectStore", "IDBIndex", "IDBCursor",
|
|
8778
|
+
"IDBCursorWithValue", "IDBTransaction", "CacheStorage", "Cache", "StorageManager", "FileSystemHandle",
|
|
8779
|
+
"FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemWritableFileStream", "FileSystemSyncAccessHandle", "CookieStore"]);
|
|
8780
|
+
const HOF_REMOVAL_NAMES = new Set(["off", "removeListener", "removeEventListener", "removeAllListeners"]);
|
|
8781
|
+
// NOT non-invokers — they invoke their first argument — but the reflective arm below answers them with the
|
|
8782
|
+
// invoked function's OWN arguments (R947's locator), and this arm, which cannot see those, would add a second
|
|
8783
|
+
// locator-less charge beside it (measured: `Reflect.apply(fs.readFileSync, undefined, ["/tmp/ok/b"])` lost its
|
|
8784
|
+
// `allow Fs /tmp/ok` certification). Owned elsewhere, so excluded here.
|
|
8785
|
+
const HOF_OWNED_ELSEWHERE = new Set(["Reflect.apply", "Reflect.construct"]);
|
|
8786
|
+
const hofOwnerMember = (decl) => {
|
|
8787
|
+
if (!decl) return null;
|
|
8788
|
+
const member = ts.isConstructSignatureDeclaration(decl) ? "new" : decl.name?.getText?.();
|
|
8789
|
+
const holder = decl.parent && ts.isModuleBlock(decl.parent) ? decl.parent.parent : decl.parent; // `namespace Reflect {…}`
|
|
8790
|
+
const owner = holder?.name?.getText?.() ?? null;
|
|
8791
|
+
return owner && member ? `${owner}.${member}` : null;
|
|
8792
|
+
};
|
|
8793
|
+
const declIsReviewedLib = (decl) => !!decl && (declModule(decl) === "<es-lib>" || declIsNodeTypes(decl));
|
|
8794
|
+
const hofCalleeNeverInvokes = (decl, name) => {
|
|
8795
|
+
if ((name && HOF_REMOVAL_NAMES.has(name)) || HOF_REMOVAL_NAMES.has(decl?.name?.getText?.() ?? "")) return true;
|
|
8796
|
+
return declIsReviewedLib(decl) && HOF_OWNED_ELSEWHERE.has(hofOwnerMember(decl) ?? "");
|
|
8797
|
+
};
|
|
8798
|
+
const calleeParamIsGenericSlot = (node, i) => {
|
|
8799
|
+
const sd = checker.getResolvedSignature?.(node)?.declaration;
|
|
8800
|
+
const ps = sd?.parameters;
|
|
8801
|
+
if (!ps?.length || i >= ps.length || !ps[i].type || ps[i].dotDotDotToken) return false;
|
|
8802
|
+
const t = checker.getTypeFromTypeNode(ps[i].type);
|
|
8803
|
+
const isFreeTP = (u) => !!(u.flags & ts.TypeFlags.TypeParameter)
|
|
8804
|
+
&& !(checker.getBaseConstraintOfType(u)?.getCallSignatures?.().length > 0);
|
|
8805
|
+
const parts = t.types ?? [t];
|
|
8806
|
+
return parts.some(isFreeTP) && !parts.some((u) => !isFreeTP(u) && u.getCallSignatures?.().length > 0);
|
|
8807
|
+
};
|
|
8808
|
+
// ⟨R1031⟩ reviewed-library callees whose object-literal argument's callable members are INSTALLED, not
|
|
8809
|
+
// invoked: a property descriptor (`get`/`set`) or a descriptor map. Asked of the resolved declaration.
|
|
8810
|
+
const OBJ_ARG_INSTALLS = new Set(["ObjectConstructor.defineProperty", "ObjectConstructor.defineProperties",
|
|
8811
|
+
"ObjectConstructor.create", "Reflect.defineProperty"]);
|
|
8812
|
+
const HOF_REACH = process.env.CANDOR_HOF_REACH ? (k, n) => console.error(`HOF-REACH ${k} ${n}`) : null;
|
|
8813
|
+
|
|
8360
8814
|
// ---- process.env recognition: the direct dot access (`process.env.KEY`) is the JVM System.getenv twin,
|
|
8361
8815
|
// but the same environment READ is spelled several other ways that all read silent-pure without help:
|
|
8362
8816
|
// bracket access (`process.env[k]`), a local const-alias (`const env = process.env; env.KEY`),
|
|
@@ -8387,14 +8841,25 @@ const declImportsNodeProcess = (decl) => {
|
|
|
8387
8841
|
// downstream consult — the fix in one place, per brief §F1 item 3 ("make the two paths share one
|
|
8388
8842
|
// authority", not patch the losing copy). Populated by the pre-pass immediately below.
|
|
8389
8843
|
const processAliasSymbols = new Set();
|
|
8390
|
-
const identIsGlobalProcess = (
|
|
8844
|
+
const identIsGlobalProcess = (id0) => {
|
|
8845
|
+
// ⟨SOUNDNESS R936⟩ through the ONE transparent-wrapper set (R780): `(process as any).argv`, `(<any>process).argv`
|
|
8846
|
+
// and even `(process).argv` were ABSENT because the argv arm handed this the wrapped node and this matched an
|
|
8847
|
+
// Identifier only — the env arm unwrapped first and the argv arm did not. Unwrapping HERE gives every caller
|
|
8848
|
+
// the same answer instead of each remembering to.
|
|
8849
|
+
const id = unwrapArgExpr(id0);
|
|
8391
8850
|
if (!id) return false;
|
|
8851
|
+
// ⟨SOUNDNESS R936⟩ `require("process")` / `require("node:process")` IS the process object — the module-binding
|
|
8852
|
+
// spelling of the same global (CJS, `--allow-js` trees, and `createRequire`). `require("process").env.HOME` read
|
|
8853
|
+
// nothing (EXECUTED). Keyed on a callee spelled `require` and the literal specifier; a project function that
|
|
8854
|
+
// happens to be called `require` and returns something else is the only false match, and it over-charges.
|
|
8855
|
+
if (ts.isCallExpression(id) && ts.isIdentifier(id.expression) && id.expression.text === "require"
|
|
8856
|
+
&& id.arguments.length === 1 && ts.isStringLiteralLike(id.arguments[0])
|
|
8857
|
+
&& (id.arguments[0].text === "process" || id.arguments[0].text === "node:process")) return true;
|
|
8392
8858
|
// `globalThis.process` / `global.process` — the SAME process object reached off the global (isomorphic code:
|
|
8393
8859
|
// `globalThis.process?.env`, often `(globalThis as any).process.env`). Unwrap parens/`as` casts around the
|
|
8394
8860
|
// root; the `globalThis`/`global` root must be the ambient global, not a project shadow.
|
|
8395
8861
|
if (ts.isPropertyAccessExpression(id) && id.name.text === "process") {
|
|
8396
|
-
|
|
8397
|
-
while (ts.isParenthesizedExpression(root) || ts.isAsExpression(root) || ts.isNonNullExpression(root)) root = root.expression;
|
|
8862
|
+
const root = unwrapArgExpr(id.expression); // R780: `(<any>globalThis).process` is the same object
|
|
8398
8863
|
if (ts.isIdentifier(root) && (root.text === "globalThis" || root.text === "global")) {
|
|
8399
8864
|
const gd = checker.getSymbolAtLocation(root)?.declarations ?? [];
|
|
8400
8865
|
return !gd.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)));
|
|
@@ -8423,9 +8888,7 @@ const identIsGlobalProcess = (id) => {
|
|
|
8423
8888
|
{
|
|
8424
8889
|
const isGlobalProcessInitializer = (expr) => {
|
|
8425
8890
|
if (!expr) return false;
|
|
8426
|
-
|
|
8427
|
-
while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isNonNullExpression(e)) e = e.expression;
|
|
8428
|
-
return identIsGlobalProcess(e); // handles `globalThis.process` / `global.process` / bare ambient `process`
|
|
8891
|
+
return identIsGlobalProcess(unwrapArgExpr(expr)); // R780 // handles `globalThis.process` / `global.process` / bare ambient `process`
|
|
8429
8892
|
};
|
|
8430
8893
|
const collectProcessAliases = (node) => {
|
|
8431
8894
|
if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name) && node.initializer) {
|
|
@@ -8434,8 +8897,7 @@ const identIsGlobalProcess = (id) => {
|
|
|
8434
8897
|
if (sym) processAliasSymbols.add(sym);
|
|
8435
8898
|
}
|
|
8436
8899
|
} else if (ts.isVariableDeclaration(node) && node.name && ts.isObjectBindingPattern(node.name) && node.initializer) {
|
|
8437
|
-
|
|
8438
|
-
while (ts.isParenthesizedExpression(root) || ts.isAsExpression(root) || ts.isNonNullExpression(root)) root = root.expression;
|
|
8900
|
+
const root = unwrapArgExpr(node.initializer); // R780
|
|
8439
8901
|
if (ts.isIdentifier(root) && (root.text === "globalThis" || root.text === "global")) {
|
|
8440
8902
|
const gd = checker.getSymbolAtLocation(root)?.declarations ?? [];
|
|
8441
8903
|
if (!gd.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)))) {
|
|
@@ -8476,11 +8938,7 @@ const identIsGlobalProcess = (id) => {
|
|
|
8476
8938
|
// EXPRESSION — that is the model this adopts, with identity widened from a node to a value.
|
|
8477
8939
|
//
|
|
8478
8940
|
// TRANSPARENT: the wrappers that change only the static type, never the value.
|
|
8479
|
-
const unwrapEnvTransparent = (e) =>
|
|
8480
|
-
while (e && (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isTypeAssertionExpression(e)
|
|
8481
|
-
|| ts.isNonNullExpression(e) || ts.isSatisfiesExpression(e))) e = e.expression;
|
|
8482
|
-
return e;
|
|
8483
|
-
};
|
|
8941
|
+
const unwrapEnvTransparent = (e) => unwrapArgExpr(e); // R780: one wrapper set, not a fourth copy of it
|
|
8484
8942
|
// `process.env` / `process["env"]` / `globalThis.process?.env` / `(process as any).env` — the global
|
|
8485
8943
|
// process object's `env` member. The `process` side keeps `identIsGlobalProcess`'s shadow guard, so a
|
|
8486
8944
|
// project-local `process` (a parameter, a module `const`) matches nothing.
|
|
@@ -8536,16 +8994,86 @@ const envValueLeaves = (expr, out = []) => {
|
|
|
8536
8994
|
// READ was not even `Unknown` — it was absent).
|
|
8537
8995
|
// A binding is a `VariableDeclaration` with an identifier name (no initializer = a binding with no env
|
|
8538
8996
|
// leaf), a plain or logical (`??=`/`||=`/`&&=`) assignment to an identifier, or `{ env } = process` /
|
|
8539
|
-
// `{ env: e } = process` (wrappers unwrapped).
|
|
8540
|
-
//
|
|
8997
|
+
// `{ env: e } = process` (wrappers unwrapped).
|
|
8998
|
+
//
|
|
8999
|
+
// ⟨R934/R935⟩ THE SAME QUESTION OVER EVERY STORAGE LOCATION WHOSE BINDINGS THIS PASS CAN ENUMERATE, not
|
|
9000
|
+
// over locals only. The identity above was asked of a VARIABLE; the environment object also travels
|
|
9001
|
+
// through a PARAMETER (R934: `function rd(e) { return e.K }` called as `rd(process.env)` — the read runs
|
|
9002
|
+
// in `rd`'s frame and `rd` was absent) and through a heap FIELD (R935: `const cfg = { env: process.env }`
|
|
9003
|
+
// at module level, read later as `cfg.env.K` by a function that never calls the module initializer —
|
|
9004
|
+
// absent; only `<module>` carried Env). Both are one defect: the alias model stopped at the first
|
|
9005
|
+
// location that was not a local, so the value was lost the moment it was stored anywhere else. The fix
|
|
9006
|
+
// is not a second analysis per location kind; it is the SAME binding table and the SAME two fixpoints
|
|
9007
|
+
// with two more kinds of location in them:
|
|
9008
|
+
//
|
|
9009
|
+
// PARAMETER — its bindings are the arguments at every VISIBLE call site (positional, up to a spread;
|
|
9010
|
+
// a site that omits it binds its default, else `undefined`). It joins MUST only — every visible site
|
|
9011
|
+
// hands it the environment — and NEVER MAY. That asymmetry is pass 2b's ruling, transferred: a
|
|
9012
|
+
// parameter fed the environment at one site and `{}` at another is the DIVERGENT shared-HOF shape,
|
|
9013
|
+
// and charging the callee would pool the read onto the caller that passed `{}` (a fabrication). The
|
|
9014
|
+
// caller that passes the environment is already charged by the Env arm (a call argument is not an
|
|
9015
|
+
// inert consumer), so every gate above the callee still fires — the callee's own scoped gate is the
|
|
9016
|
+
// only one that stays 0, exactly as it does for a divergent HOF. Only functions whose every reference
|
|
9017
|
+
// is a CALL are eligible: a function that escapes as a value (a callback, `.call`, `.bind`, stored)
|
|
9018
|
+
// has bindings this pass cannot see. Methods are eligible only on a class with no heritage that no
|
|
9019
|
+
// project class extends — a dispatched call resolves to the base declaration, and an override's
|
|
9020
|
+
// parameter would otherwise look fed by fewer sites than it is.
|
|
9021
|
+
// PROPERTY — its bindings are the initializer of every VALUE-BEARING declaration (an object-literal
|
|
9022
|
+
// property, a class field, a constructor parameter property — which is bound exactly as its
|
|
9023
|
+
// parameter is) and the right side of every assignment to it. An absent initializer is not a binding:
|
|
9024
|
+
// a field that is `undefined` until it is assigned the environment cannot make a reader read a
|
|
9025
|
+
// DIFFERENT object's keys, and that is the only thing MUST has to rule out. MUST charges a reader Env;
|
|
9026
|
+
// MAY (rebound to something else somewhere) discloses `Unknown[env-maybe-read]`, the local-alias
|
|
9027
|
+
// posture, because unlike a parameter there is no caller in the reader's chain that carries it. A
|
|
9028
|
+
// property declared by an interface or a type literal (`PropertySignature`) is NOT a location here:
|
|
9029
|
+
// any conforming object can be its value, so its bindings are an open set — the residual is stated
|
|
9030
|
+
// in SOUNDNESS, not hidden. A field of a class some project class extends is excluded for the
|
|
9031
|
+
// override reason above.
|
|
9032
|
+
//
|
|
9033
|
+
// Writing the environment INTO a property stays charged where it happens (the Env arm's "store" rule);
|
|
9034
|
+
// this adds the READERS and removes nothing.
|
|
8541
9035
|
const envAliasSymbols = new Set();
|
|
8542
9036
|
const envMayAliasSymbols = new Set();
|
|
9037
|
+
// ⟨R935⟩ property locations, kept apart from the local sets so a property name never reaches the
|
|
9038
|
+
// identifier-keyed consumers (2c's `envSourceKind`, the alias-binding inert rule) by accident.
|
|
9039
|
+
const envPropMustSymbols = new Set();
|
|
9040
|
+
const envPropMaySymbols = new Set();
|
|
9041
|
+
// ⟨R935⟩ a property location's identity: its first declaration node (see `memberSymOf` for why not the symbol).
|
|
9042
|
+
const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
|
|
8543
9043
|
{
|
|
8544
9044
|
const ENV_DESTRUCTURED = Symbol("env-destructured");
|
|
8545
|
-
const bindings = new Map(); // symbol -> [value expr | null | ENV_DESTRUCTURED]
|
|
9045
|
+
const bindings = new Map(); // symbol -> [value expr | null | ENV_DESTRUCTURED | { same: symbol }]
|
|
8546
9046
|
const note = (sym, v) => { if (sym) (bindings.get(sym) ?? bindings.set(sym, []).get(sym)).push(v); };
|
|
8547
9047
|
const LOGICAL_ASSIGN = new Set([ts.SyntaxKind.EqualsToken, ts.SyntaxKind.QuestionQuestionEqualsToken,
|
|
8548
9048
|
ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.AmpersandAmpersandEqualsToken]);
|
|
9049
|
+
const paramSyms = new Set(); // ⟨R934⟩ parameters with at least one recorded binding
|
|
9050
|
+
const propSyms = new Set(); // ⟨R935⟩ eligible property locations with at least one binding
|
|
9051
|
+
const propIneligible = new Set();
|
|
9052
|
+
// The LOCATION KEY of a member reference: `o.p` / `o["p"]` → the property's first declaration node.
|
|
9053
|
+
// Keyed by DECLARATION, not symbol: an object literal's type is WIDENED when it is bound, and the
|
|
9054
|
+
// widened type carries fresh transient property symbols — `cfg.env` at a read site and `env:` in the
|
|
9055
|
+
// literal are two symbol objects for one declaration (measured: the read never matched the binding).
|
|
9056
|
+
const memberRawSym = (n) => (ts.isPropertyAccessExpression(n) ? checker.getSymbolAtLocation(n.name)
|
|
9057
|
+
: (ts.isElementAccessExpression(n) && n.argumentExpression && ts.isStringLiteralLike(n.argumentExpression))
|
|
9058
|
+
? checker.getSymbolAtLocation(n.argumentExpression) : null) ?? null;
|
|
9059
|
+
const memberSymOf = (n) => envLocKey(memberRawSym(n));
|
|
9060
|
+
// Is this property symbol a location whose bindings we can enumerate? Every declaration must be
|
|
9061
|
+
// value-bearing, in a project file, and not a member of an extended class.
|
|
9062
|
+
const propEligible = (sym) => {
|
|
9063
|
+
if (!sym || propIneligible.has(sym)) return false;
|
|
9064
|
+
const decls = sym.declarations ?? [];
|
|
9065
|
+
const ok = decls.length > 0 && decls.every((d) => {
|
|
9066
|
+
if (!projectFiles.has(path.resolve(d.getSourceFile().fileName))) return false;
|
|
9067
|
+
if (ts.isPropertyAssignment(d) || ts.isShorthandPropertyAssignment(d)) return ts.isObjectLiteralExpression(d.parent);
|
|
9068
|
+
if (ts.isPropertyDeclaration(d)) return !d.modifiers?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword)
|
|
9069
|
+
&& !extendedClassSyms.has(classSymOf(d.parent));
|
|
9070
|
+
if (ts.isParameter(d)) return ts.isConstructorDeclaration(d.parent) && !extendedClassSyms.has(classSymOf(d.parent.parent));
|
|
9071
|
+
return false;
|
|
9072
|
+
});
|
|
9073
|
+
if (!ok) propIneligible.add(sym);
|
|
9074
|
+
return ok;
|
|
9075
|
+
};
|
|
9076
|
+
const noteProp = (sym, v) => { if (propEligible(sym)) { const k = envLocKey(sym); propSyms.add(k); note(k, v); } };
|
|
8549
9077
|
const collect = (node) => {
|
|
8550
9078
|
if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
|
|
8551
9079
|
note(checker.getSymbolAtLocation(node.name), node.initializer ?? null);
|
|
@@ -8558,28 +9086,97 @@ const envMayAliasSymbols = new Set();
|
|
|
8558
9086
|
}
|
|
8559
9087
|
} else if (ts.isBinaryExpression(node) && LOGICAL_ASSIGN.has(node.operatorToken.kind) && ts.isIdentifier(node.left)) {
|
|
8560
9088
|
note(checker.getSymbolAtLocation(node.left), node.right);
|
|
9089
|
+
} else if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment
|
|
9090
|
+
&& node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
|
9091
|
+
&& (ts.isPropertyAccessExpression(node.left) || ts.isElementAccessExpression(node.left))) {
|
|
9092
|
+
// ⟨R935⟩ an assignment to a property location; a compound operator (`+=`) binds a non-environment.
|
|
9093
|
+
const s = memberRawSym(node.left);
|
|
9094
|
+
if (s) noteProp(s, LOGICAL_ASSIGN.has(node.operatorToken.kind) ? node.right : null);
|
|
9095
|
+
} else if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {
|
|
9096
|
+
const s = checker.getSymbolAtLocation(node.name);
|
|
9097
|
+
if (s && (s.flags & ts.SymbolFlags.Property)) noteProp(s, node.initializer);
|
|
9098
|
+
} else if (ts.isShorthandPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {
|
|
9099
|
+
const s = checker.getSymbolAtLocation(node.name);
|
|
9100
|
+
const v = checker.getShorthandAssignmentValueSymbol(node);
|
|
9101
|
+
if (s && (s.flags & ts.SymbolFlags.Property)) noteProp(s, v ? { same: v } : null);
|
|
9102
|
+
} else if (ts.isPropertyDeclaration(node) && node.initializer && node.name) {
|
|
9103
|
+
noteProp(checker.getSymbolAtLocation(node.name), node.initializer);
|
|
9104
|
+
} else if (ts.isParameter(node) && ts.isIdentifier(node.name) && ts.isConstructorDeclaration(node.parent)
|
|
9105
|
+
&& ts.getModifiers?.(node)?.some((m) => m.kind === ts.SyntaxKind.PublicKeyword
|
|
9106
|
+
|| m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword
|
|
9107
|
+
|| m.kind === ts.SyntaxKind.ReadonlyKeyword)) {
|
|
9108
|
+
// a constructor parameter property is bound exactly as its parameter is
|
|
9109
|
+
const [ps, fs] = checker.getSymbolsOfParameterPropertyDeclaration(node, node.name.text);
|
|
9110
|
+
if (ps && fs) noteProp(fs, { same: ps });
|
|
9111
|
+
}
|
|
9112
|
+
// ⟨R935⟩ WRITES THE TABLE ABOVE CANNOT NAME: a reflective write into the object (`Object.assign(o, …)`,
|
|
9113
|
+
// `Object.defineProperty`, `Reflect.set`) or a computed-key store `o[k] = v` can rebind ANY of the
|
|
9114
|
+
// object's properties. Every eligible property of the target's type gets a non-environment binding,
|
|
9115
|
+
// so a location written that way can be MAY at best — a reader discloses `Unknown` rather than being
|
|
9116
|
+
// charged `Env` over a value that was replaced off the books (executed: an `Object.assign` rebind read
|
|
9117
|
+
// the replacement, and the first cut of this charged it `Env`).
|
|
9118
|
+
const reflectTarget = ts.isCallExpression(node)
|
|
9119
|
+
&& /^(Object\.(assign|defineProperty|defineProperties)|Reflect\.(set|defineProperty))$/.test(
|
|
9120
|
+
(ts.isPropertyAccessExpression(node.expression) ? node.expression.getText() : "").replace(/\s+/g, ""))
|
|
9121
|
+
? node.arguments[0]
|
|
9122
|
+
: (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
9123
|
+
&& ts.isElementAccessExpression(node.left) && node.left.argumentExpression
|
|
9124
|
+
&& !ts.isStringLiteralLike(node.left.argumentExpression) && !ts.isNumericLiteral(node.left.argumentExpression))
|
|
9125
|
+
? node.left.expression : null;
|
|
9126
|
+
if (reflectTarget) {
|
|
9127
|
+
let props = [];
|
|
9128
|
+
try { props = checker.getTypeAtLocation(reflectTarget)?.getProperties?.() ?? []; } catch { props = []; }
|
|
9129
|
+
for (const ps of props) if (propEligible(ps)) noteProp(ps, null);
|
|
8561
9130
|
}
|
|
8562
9131
|
ts.forEachChild(node, collect);
|
|
8563
9132
|
};
|
|
8564
9133
|
for (const sf of sources) collect(sf);
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
const
|
|
8568
|
-
|
|
9134
|
+
// ⟨R934⟩ the parameter bindings come from the shared PARAMETER PROVENANCE index above.
|
|
9135
|
+
for (const [ps, vals] of paramArgs) { paramSyms.add(ps); for (const v of vals) note(ps, v); }
|
|
9136
|
+
const leafIn = (leaf, set, propSet) => isProcessEnvExpr(leaf)
|
|
9137
|
+
|| (ts.isIdentifier(leaf) && set.has(checker.getSymbolAtLocation(leaf)))
|
|
9138
|
+
|| (!!propSet && propSet.has(memberSymOf(leaf)));
|
|
9139
|
+
const hasEnvLeaf = (v, set, propSet) => v === ENV_DESTRUCTURED
|
|
9140
|
+
|| (!!v && typeof v === "object" && "same" in v ? set.has(v.same) || propSet?.has(v.same) === true
|
|
9141
|
+
: (!!v && typeof v === "object" && envValueLeaves(v).some((l) => leafIn(l, set, propSet))));
|
|
9142
|
+
|
|
9143
|
+
|
|
9144
|
+
// MAY: least fixpoint of "some binding has a leaf that is process.env or already MAY" — LOCALS and
|
|
9145
|
+
// PROPERTIES (a property's MAY reads a local's MAY and vice versa). Parameters never join MAY (above).
|
|
8569
9146
|
for (let changed = true, n = 0; changed && n < 64; n++) {
|
|
8570
9147
|
changed = false;
|
|
8571
9148
|
for (const [sym, vals] of bindings) {
|
|
8572
|
-
if (
|
|
9149
|
+
if (paramSyms.has(sym)) continue;
|
|
9150
|
+
const isProp = propSyms.has(sym);
|
|
9151
|
+
const set = isProp ? envPropMaySymbols : envMayAliasSymbols;
|
|
9152
|
+
if (!set.has(sym) && vals.some((v) => hasEnvLeaf(v, envMayAliasSymbols, envPropMaySymbols))) { set.add(sym); changed = true; }
|
|
8573
9153
|
}
|
|
8574
9154
|
}
|
|
8575
9155
|
// MUST: least fixpoint (from empty) of "every binding has a leaf that is process.env or already MUST" —
|
|
8576
|
-
// least, so a cycle that never touches process.env directly stays MAY (the disclosing side).
|
|
8577
|
-
|
|
8578
|
-
|
|
9156
|
+
// least, so a cycle that never touches process.env directly stays MAY (the disclosing side). Locals and
|
|
9157
|
+
// properties start from their MAY sets; parameters are tried directly (MUST-only). A parameter whose
|
|
9158
|
+
// function escapes is dropped before it can join.
|
|
9159
|
+
const paramCands = [...paramSyms].filter((s) => bindings.get(s)?.some((v) => v !== null));
|
|
9160
|
+
let batched = false;
|
|
9161
|
+
const mustTry = () => {
|
|
9162
|
+
let changed = false;
|
|
8579
9163
|
for (const sym of envMayAliasSymbols) {
|
|
8580
|
-
if (!envAliasSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols))) { envAliasSymbols.add(sym); changed = true; }
|
|
9164
|
+
if (!envAliasSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) { envAliasSymbols.add(sym); changed = true; }
|
|
8581
9165
|
}
|
|
8582
|
-
|
|
9166
|
+
for (const sym of envPropMaySymbols) {
|
|
9167
|
+
if (!envPropMustSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) { envPropMustSymbols.add(sym); changed = true; }
|
|
9168
|
+
}
|
|
9169
|
+
for (const sym of paramCands) {
|
|
9170
|
+
if (envAliasSymbols.has(sym)) continue;
|
|
9171
|
+
if (!bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) continue;
|
|
9172
|
+
const fsym = paramOwnerOf.get(sym);
|
|
9173
|
+
if (!batched) { batched = true; paramFnEscaped.batch(paramCands.map((p) => paramOwnerOf.get(p)).filter(Boolean)); }
|
|
9174
|
+
if (paramFnEscaped(fsym)) continue;
|
|
9175
|
+
envAliasSymbols.add(sym); changed = true;
|
|
9176
|
+
}
|
|
9177
|
+
return changed;
|
|
9178
|
+
};
|
|
9179
|
+
for (let n = 0; mustTry() && n < 64; n++) { /* to fixpoint */ }
|
|
8583
9180
|
}
|
|
8584
9181
|
// True when `id` is an identifier resolving to a confirmed process.env alias local.
|
|
8585
9182
|
const identIsEnvAlias = (id) => {
|
|
@@ -8595,11 +9192,26 @@ const identIsEnvMayAlias = (id) => {
|
|
|
8595
9192
|
};
|
|
8596
9193
|
// The env kind of an expression's VALUE: "env" if some leaf is process.env or a MUST-alias, "may" if
|
|
8597
9194
|
// some leaf is only a MAY-alias, else null. The one answer the Env arm, 2c and the shared helpers ask.
|
|
9195
|
+
// ⟨R935⟩ a member reference `o.p` / `o["p"]` whose property is an environment location.
|
|
9196
|
+
const envMemberSym = (n) => {
|
|
9197
|
+
if (!(envPropMaySymbols.size > 0)) return null;
|
|
9198
|
+
if (ts.isPropertyAccessExpression(n)) return envLocKey(checker.getSymbolAtLocation(n.name));
|
|
9199
|
+
if (ts.isElementAccessExpression(n) && n.argumentExpression && ts.isStringLiteralLike(n.argumentExpression))
|
|
9200
|
+
return envLocKey(checker.getSymbolAtLocation(n.argumentExpression));
|
|
9201
|
+
return null;
|
|
9202
|
+
};
|
|
9203
|
+
const envPropKind = (n) => {
|
|
9204
|
+
const s = envMemberSym(n);
|
|
9205
|
+
if (!s || !envPropMaySymbols.has(s)) return null;
|
|
9206
|
+
return envPropMustSymbols.has(s) ? "env" : "may";
|
|
9207
|
+
};
|
|
8598
9208
|
const envValueKind = (expr) => {
|
|
8599
9209
|
let kind = null;
|
|
8600
9210
|
for (const l of envValueLeaves(expr)) {
|
|
8601
9211
|
if (isProcessEnvExpr(l) || identIsEnvAlias(l)) return "env";
|
|
8602
|
-
|
|
9212
|
+
const pk = envPropKind(l);
|
|
9213
|
+
if (pk === "env") return "env";
|
|
9214
|
+
if (identIsEnvMayAlias(l) || pk === "may") kind = "may";
|
|
8603
9215
|
}
|
|
8604
9216
|
return kind;
|
|
8605
9217
|
};
|
|
@@ -8612,8 +9224,21 @@ const envIsAssignOp = (k) => k >= ts.SyntaxKind.FirstAssignment && k <= ts.Synta
|
|
|
8612
9224
|
let envAliasNames = null;
|
|
8613
9225
|
const envRefKind = (node) => {
|
|
8614
9226
|
if (isProcessEnvExpr(node)) return "env";
|
|
9227
|
+
// ⟨R935⟩ a READ of an environment property location. Not an assignment target, a `delete` operand, a
|
|
9228
|
+
// destructuring target or a `++`/`--` operand: those write the location, they do not read the object.
|
|
9229
|
+
if (envPropMaySymbols.size > 0 && (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node))) {
|
|
9230
|
+
const pk = envPropKind(node);
|
|
9231
|
+
if (!pk) return null;
|
|
9232
|
+
const p = node.parent;
|
|
9233
|
+
if (p && ts.isBinaryExpression(p) && p.left === node && envIsAssignOp(p.operatorToken.kind)) return null;
|
|
9234
|
+
if (p && (ts.isDeleteExpression(p) || ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p))
|
|
9235
|
+
&& (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)))) return null;
|
|
9236
|
+
if (isDestructuringAssignTarget(node)) return null;
|
|
9237
|
+
return pk;
|
|
9238
|
+
}
|
|
8615
9239
|
if (!ts.isIdentifier(node)) return null;
|
|
8616
|
-
|
|
9240
|
+
// ⟨R934⟩ MUST-parameters are in `envAliasSymbols` but never in the MAY set, so the name filter reads both.
|
|
9241
|
+
envAliasNames ??= new Set([...envMayAliasSymbols, ...envAliasSymbols].map((s) => s.name));
|
|
8617
9242
|
if (!envAliasNames.has(node.text)) return null;
|
|
8618
9243
|
const p = node.parent;
|
|
8619
9244
|
if (!p) return null;
|
|
@@ -8628,8 +9253,9 @@ const envRefKind = (node) => {
|
|
|
8628
9253
|
&& (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)) return null;
|
|
8629
9254
|
const sym = ts.isShorthandPropertyAssignment(p) && p.name === node
|
|
8630
9255
|
? checker.getShorthandAssignmentValueSymbol(p) : checker.getSymbolAtLocation(node);
|
|
8631
|
-
if (!sym
|
|
8632
|
-
|
|
9256
|
+
if (!sym) return null;
|
|
9257
|
+
if (envAliasSymbols.has(sym)) return "env";
|
|
9258
|
+
return envMayAliasSymbols.has(sym) ? "may" : null;
|
|
8633
9259
|
};
|
|
8634
9260
|
// ⟨R928/R804⟩ Does the CONSUMER of this env value provably touch no key? Climb through what passes the
|
|
8635
9261
|
// value on unchanged (the transparent wrappers, both arms of `?:`, either side of `??`/`||`, the right of
|
|
@@ -8761,14 +9387,10 @@ const globalBuiltinCallee = (callee) => {
|
|
|
8761
9387
|
if (!callee || !ts.isPropertyAccessExpression(callee)) return null;
|
|
8762
9388
|
const member = callee.name?.text;
|
|
8763
9389
|
if (!member) return null;
|
|
8764
|
-
|
|
8765
|
-
while (owner && (ts.isParenthesizedExpression(owner) || ts.isAsExpression(owner)
|
|
8766
|
-
|| ts.isNonNullExpression(owner))) owner = owner.expression;
|
|
9390
|
+
const owner = unwrapArgExpr(callee.expression); // R780
|
|
8767
9391
|
if (ts.isIdentifier(owner)) return identIsGlobal(owner) ? `${owner.text}.${member}` : null;
|
|
8768
9392
|
if (!ts.isPropertyAccessExpression(owner) || !owner.name?.text) return null;
|
|
8769
|
-
|
|
8770
|
-
while (root && (ts.isParenthesizedExpression(root) || ts.isAsExpression(root)
|
|
8771
|
-
|| ts.isNonNullExpression(root))) root = root.expression;
|
|
9393
|
+
const root = unwrapArgExpr(owner.expression); // R780
|
|
8772
9394
|
if (!ts.isIdentifier(root) || !GLOBAL_ROOTS.has(root.text) || !identIsGlobal(root)) return null;
|
|
8773
9395
|
return `${owner.name.text}.${member}`;
|
|
8774
9396
|
};
|
|
@@ -8805,9 +9427,7 @@ const globalBareCallee = (callee) => {
|
|
|
8805
9427
|
if (!callee) return null;
|
|
8806
9428
|
if (ts.isIdentifier(callee)) return identIsGlobal(callee) ? callee.text : null;
|
|
8807
9429
|
if (!ts.isPropertyAccessExpression(callee) || !callee.name?.text) return null;
|
|
8808
|
-
|
|
8809
|
-
while (root && (ts.isParenthesizedExpression(root) || ts.isAsExpression(root)
|
|
8810
|
-
|| ts.isNonNullExpression(root))) root = root.expression;
|
|
9430
|
+
const root = unwrapArgExpr(callee.expression); // R780
|
|
8811
9431
|
// ONLY a global ROOT, never an arbitrary owner: `Object.assign` must not read as the bare global
|
|
8812
9432
|
// `assign`, and `globalThis.Object.assign` must not read as the bare global `Object` — both are the
|
|
8813
9433
|
// member helper's business, and answering them here would be the second copy §G exists to prevent.
|
|
@@ -9143,8 +9763,95 @@ function visitCalls(node) {
|
|
|
9143
9763
|
// never a fabrication. (A LOCAL callee keeps its precise callback-flow below.)
|
|
9144
9764
|
const calleeName = ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text
|
|
9145
9765
|
: ts.isIdentifier(node.expression) ? node.expression.text : null;
|
|
9146
|
-
|
|
9147
|
-
|
|
9766
|
+
// ⟨SOUNDNESS R803⟩ a LISTED name keeps its position map; an UNLISTED non-local callee is admitted at the
|
|
9767
|
+
// positions its signature POSITIVELY declares callable, unless denylisted. See `HOF_REMOVAL_NAMES`.
|
|
9768
|
+
const hofListed = !!calleeName && HOF_INVOKERS.has(calleeName);
|
|
9769
|
+
const hofName = calleeName ?? decl.name?.getText?.() ?? null;
|
|
9770
|
+
const hofOpenAt = (!hofListed && mod !== "<local>" && node.arguments?.length
|
|
9771
|
+
&& !hofCalleeNeverInvokes(decl, hofName))
|
|
9772
|
+
? (i) => calleeParamIsCallable(node, i) === true
|
|
9773
|
+
&& !(declIsReviewedLib(decl) && calleeParamIsGenericSlot(node, i)) : null;
|
|
9774
|
+
// ⟨SOUNDNESS R1031⟩ THE SAME RULE ONE LEVEL DOWN: a function reference handed inside an OBJECT
|
|
9775
|
+
// LITERAL. rxjs `of(1).subscribe({ next: h })`, `{ complete: h }`, `{ error: h }`, `{ next }` and
|
|
9776
|
+
// `{ next: o.m.bind(o) }` were ABSENT with `deny Fs <caller>` exit 0 while the bare `subscribe(h)`
|
|
9777
|
+
// read `['Fs']` (EXECUTED: every handler that the observable fires wrote its file). R803 admits a
|
|
9778
|
+
// POSITION its signature declares callable; the observer object's MEMBERS are positions too, and
|
|
9779
|
+
// the signature declares them just as positively — `Partial<Observer<T>>`'s `next` is
|
|
9780
|
+
// `((value: T) => void) | undefined`. So each property whose value is a reference, at a member the
|
|
9781
|
+
// parameter's (non-callable) type arms declare callable, is handed to the SAME per-argument arm as
|
|
9782
|
+
// a bare reference — one implementation of "a reference the callee may invoke", not a second copy.
|
|
9783
|
+
// Inline arrows and methods in the literal were never dropped (they are structural units, charged
|
|
9784
|
+
// lexically). Same admission as R803's open path: an unlisted non-local callee off the denylist; an
|
|
9785
|
+
// `any`/`unknown`-typed parameter or member admits nothing (the R803 residual, unchanged).
|
|
9786
|
+
const objArgRefs = (argIdx, lit, depth = 0) => {
|
|
9787
|
+
if (depth > 2) return [];
|
|
9788
|
+
// The parameter's DECLARED type, not the call's instantiation of it: `Object.assign(p, { unsubscribe })`
|
|
9789
|
+
// instantiates `U` as `{ unsubscribe: () => void }`, which reads callable only because the CALL made it
|
|
9790
|
+
// so — the signature declares nothing about invoking it (measured: trpc's `Unpromise.then` charged
|
|
9791
|
+
// `callback:unsubscribe` until this read the declaration). A generic slot names no members, so it
|
|
9792
|
+
// admits nothing; `Partial<Observer<T>>` declares `next` callable whatever `T` is.
|
|
9793
|
+
let sd; try { sd = checker.getResolvedSignature?.(node)?.declaration; } catch { sd = null; }
|
|
9794
|
+
const dps = sd?.parameters;
|
|
9795
|
+
if (!dps?.length) return [];
|
|
9796
|
+
const dp = dps[Math.min(argIdx, dps.length - 1)];
|
|
9797
|
+
if (argIdx >= dps.length && !dp.dotDotDotToken) return [];
|
|
9798
|
+
if (!dp.type) return [];
|
|
9799
|
+
let pt; try { pt = checker.getTypeFromTypeNode(dp.type); } catch { pt = null; }
|
|
9800
|
+
if (dp.dotDotDotToken && pt) pt = checker.getIndexTypeOfType?.(pt, ts.IndexKind.Number) ?? null; // `...xs: T[]` → T
|
|
9801
|
+
return objLitRefsFor(pt, lit, depth);
|
|
9802
|
+
};
|
|
9803
|
+
const callableT = (t) => !!t && (t.getCallSignatures?.().length > 0 || (t.types ?? []).some((u) => u.getCallSignatures?.().length > 0));
|
|
9804
|
+
const objLitRefsFor = (pt, lit, depth) => {
|
|
9805
|
+
if (!pt || (pt.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))) return [];
|
|
9806
|
+
const arms = (pt.types ?? [pt]).filter((u) => !(u.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Any | ts.TypeFlags.Unknown))
|
|
9807
|
+
&& !(u.getCallSignatures?.().length > 0));
|
|
9808
|
+
const out = [];
|
|
9809
|
+
for (const pr of lit.properties) {
|
|
9810
|
+
const nm = pr.name && (ts.isIdentifier(pr.name) || ts.isStringLiteralLike(pr.name)) ? pr.name.text : null;
|
|
9811
|
+
if (!nm) continue;
|
|
9812
|
+
const memberTypes = arms.map((u) => { const ms = u.getProperty?.(nm); return ms ? checker.getTypeOfSymbolAtLocation(ms, lit) : null; }).filter(Boolean);
|
|
9813
|
+
if (ts.isShorthandPropertyAssignment(pr)) {
|
|
9814
|
+
if (memberTypes.some(callableT)) out.push({ shorthand: pr });
|
|
9815
|
+
continue;
|
|
9816
|
+
}
|
|
9817
|
+
if (!ts.isPropertyAssignment(pr)) continue; // a method / accessor is a structural unit already
|
|
9818
|
+
const v = unwrapArgExpr(pr.initializer);
|
|
9819
|
+
if (ts.isObjectLiteralExpression(v)) { for (const mt of memberTypes) out.push(...objLitRefsFor(mt, v, depth + 1)); continue; }
|
|
9820
|
+
if (memberTypes.some(callableT)) out.push({ ref: pr.initializer });
|
|
9821
|
+
}
|
|
9822
|
+
return out;
|
|
9823
|
+
};
|
|
9824
|
+
if (mod !== "<local>" && (hofListed || hofOpenAt)) {
|
|
9825
|
+
const hofArgArm = (a0, argIdx) => {
|
|
9826
|
+
// ⟨R1031⟩ an object literal at an admitted callee: its referenced members are the arguments.
|
|
9827
|
+
if (!hofListed) {
|
|
9828
|
+
const lit = unwrapArgExpr(a0);
|
|
9829
|
+
if (lit && ts.isObjectLiteralExpression(lit)) {
|
|
9830
|
+
// A property DESCRIPTOR installs its `get`/`set` and invokes neither; the access sites are
|
|
9831
|
+
// edged to the installed accessor by `definePropAccessors`. Charging the definer would be the
|
|
9832
|
+
// over-charge R803's generic-slot denylist exists for, one level down.
|
|
9833
|
+
if (declIsReviewedLib(decl) && OBJ_ARG_INSTALLS.has(hofOwnerMember(decl) ?? "")) return;
|
|
9834
|
+
for (const r of objArgRefs(argIdx, lit)) {
|
|
9835
|
+
if (r.ref) { HOF_REACH?.("objref", `${hofName}`); hofArgArm(r.ref, -1 - argIdx); continue; }
|
|
9836
|
+
// `{ next }` — the shorthand's VALUE symbol, followed one alias hop to what it holds.
|
|
9837
|
+
const vs = checker.getShorthandAssignmentValueSymbol(r.shorthand);
|
|
9838
|
+
const vd = vs?.valueDeclaration;
|
|
9839
|
+
HOF_REACH?.("objref", `${hofName}`);
|
|
9840
|
+
if (vd && nodeName.get(vd)) rec.edges.add(nodeName.get(vd));
|
|
9841
|
+
else if (vd && ts.isVariableDeclaration(vd) && vd.initializer && !ts.isArrowFunction(vd.initializer)
|
|
9842
|
+
&& !ts.isFunctionExpression(vd.initializer)) hofArgArm(vd.initializer, -1 - argIdx);
|
|
9843
|
+
else if (vd && ts.isVariableDeclaration(vd) && vd.initializer && nodeName.get(vd.initializer)) rec.edges.add(nodeName.get(vd.initializer));
|
|
9844
|
+
else if (!vd || ts.isParameter(vd) || ts.isVariableDeclaration(vd) || ts.isBindingElement(vd)) {
|
|
9845
|
+
rec.direct.add("Unknown"); rec.why.add(`callback:${r.shorthand.name.text.slice(0, 40)}`);
|
|
9846
|
+
}
|
|
9847
|
+
}
|
|
9848
|
+
return;
|
|
9849
|
+
}
|
|
9850
|
+
}
|
|
9851
|
+
// ⟨R1031⟩ a member reference re-entering the arm carries a NEGATIVE index: it is admitted by its
|
|
9852
|
+
// member's declared type above, and no position map or signature lookup applies to it.
|
|
9853
|
+
const memberRef = argIdx < 0;
|
|
9854
|
+
if (!memberRef && !hofListed && !hofOpenAt(argIdx)) return;
|
|
9148
9855
|
// R947 — a TYPE-ONLY wrapper is the same reference: `xs.forEach(fs.unlinkSync as any)`
|
|
9149
9856
|
// was dropped by the id/property-access gate below (no edge, no κ, no disclosure) while the bare
|
|
9150
9857
|
// spelling was not. `as`/`!`/`satisfies`/parentheses change no runtime value.
|
|
@@ -9159,7 +9866,7 @@ function visitCalls(node) {
|
|
|
9159
9866
|
// the same over-charge the by-reference arm below was fixed for and this arm was not.
|
|
9160
9867
|
// …but it must be the NEGATIVE test, not `!hofInvokesArg`: an unresolved or loosely-typed
|
|
9161
9868
|
// callee signature is not a licence to drop a bound callback. See `hofArgIsNeverCallback`.
|
|
9162
|
-
if (hofArgIsNeverCallback(calleeName, argIdx, node)) return;
|
|
9869
|
+
if (hofListed && hofArgIsNeverCallback(calleeName, argIdx, node)) return;
|
|
9163
9870
|
const bound = unwrapBind(a);
|
|
9164
9871
|
if (bound) {
|
|
9165
9872
|
const bref = bound.ref;
|
|
@@ -9212,7 +9919,8 @@ function visitCalls(node) {
|
|
|
9212
9919
|
if (!ts.isIdentifier(a) && !ts.isPropertyAccessExpression(a)) return;
|
|
9213
9920
|
const d2 = realDecl(checker.getSymbolAtLocation(a));
|
|
9214
9921
|
const t = (d2 && nodeName.get(d2)) || resolveFnRefUnit(a); // pin direct fn OR a local alias chain
|
|
9215
|
-
if (t) {
|
|
9922
|
+
if (t) { if (!hofListed) HOF_REACH?.("edge", `${hofName}->${t}`);
|
|
9923
|
+
rec.edges.add(t); return; } // resolvable named/local callback — keep its analyzed effect
|
|
9216
9924
|
// A DEPENDENCY function passed BY REFERENCE — `xs.forEach(depWrite)`, `setTimeout(dep.tick, 0)`.
|
|
9217
9925
|
// The invoking HOF calls it, so its effects are reachable here, and the dependency's report
|
|
9218
9926
|
// holds them under `<pkg>#depWrite`. Guard (2) below reasons that a ref resolving to a concrete
|
|
@@ -9238,7 +9946,7 @@ function visitCalls(node) {
|
|
|
9238
9946
|
// through the κ/invisible channel, so blanket-Unknown here would over-disclose them.
|
|
9239
9947
|
// (3) CALLABILITY — `argIsCallable` (has a call signature, or `any`/`unknown`/unconstrained
|
|
9240
9948
|
// generic that COULD hold a function).
|
|
9241
|
-
if (!hofInvokesArg(calleeName, argIdx, node)) return;
|
|
9949
|
+
if (hofListed && !hofInvokesArg(calleeName, argIdx, node)) return;
|
|
9242
9950
|
// ⟨SOUNDNESS R558⟩ …and BEFORE the by-reference dependency charge below, without returning:
|
|
9243
9951
|
// a FOREIGN interface member named as a value needs BOTH the visible-implementor join and
|
|
9244
9952
|
// the `invisible`/ledger disclosure `chargeExternalDecl` already gives it, and the two
|
|
@@ -9253,6 +9961,7 @@ function visitCalls(node) {
|
|
|
9253
9961
|
// dep VALUE with a call signature, passes. Guard (1) exists for exactly this shape and its own
|
|
9254
9962
|
// comment names the case (`path.reduce(fn, obj)`); the new arm simply ran before it.
|
|
9255
9963
|
if (d2 && !declIsLocal(d2) && argIsCallable(a)) {
|
|
9964
|
+
if (!hofListed) HOF_REACH?.("dep", `${hofName}->${a.getText().slice(0, 40)}`);
|
|
9256
9965
|
// R947 — A κ-CLASSIFIED BUILTIN PASSED BY REFERENCE. `chargeExternalDecl` is the
|
|
9257
9966
|
// dependency funnel and never asks κ, and `disclosureTail` treats a κ-KNOWN member as covered, so
|
|
9258
9967
|
// `xs.forEach(fs.unlinkSync)`, `setTimeout(fs.unlinkSync, 0, p)`, `.then(cp.execSync)` and
|
|
@@ -9275,11 +9984,24 @@ function visitCalls(node) {
|
|
|
9275
9984
|
const holderIsProjectValue = d2 && (ts.isParameter(d2) || ts.isVariableDeclaration(d2) || ts.isBindingElement(d2))
|
|
9276
9985
|
&& projectFiles.has(path.resolve(d2.getSourceFile().fileName));
|
|
9277
9986
|
if (!(holderIsProjectValue || !d2)) return;
|
|
9987
|
+
// ⟨SOUNDNESS R803⟩ a newly admitted callee handed its OWN caller's parameter (`reg(e, h) { e.on("y", h) }`)
|
|
9988
|
+
// is the callback flow's question, not an opaque value: the parameter is INVOKED, so pass 2b resolves it
|
|
9989
|
+
// to what every visible call site passed (an edge — `deny Fs` on the caller of `reg` fires) and keeps the
|
|
9990
|
+
// honest `callback:param#i` where a call site is opaque or none is visible. The listed names keep their
|
|
9991
|
+
// older `callback:<name>` disclosure — unchanged here so this fix removes nothing it did not add.
|
|
9992
|
+
if (!hofListed && d2 && ts.isParameter(d2) && d2.parent && nodeName.get(d2.parent) && argIsCallable(a)) {
|
|
9993
|
+
const ownerUnit = nodeName.get(d2.parent);
|
|
9994
|
+
HOF_REACH?.("param", `${hofName}->${a.getText().slice(0, 40)}`);
|
|
9995
|
+
(paramInvokes.get(ownerUnit) ?? paramInvokes.set(ownerUnit, new Set()).get(ownerUnit)).add(d2.parent.parameters.indexOf(d2));
|
|
9996
|
+
return;
|
|
9997
|
+
}
|
|
9278
9998
|
if (argIsCallable(a)) {
|
|
9999
|
+
if (!hofListed) HOF_REACH?.("opaque", `${hofName}->${a.getText().slice(0, 40)}`);
|
|
9279
10000
|
rec.direct.add("Unknown");
|
|
9280
10001
|
rec.why.add(`callback:${a.getText().replace(/\s+/g, "").slice(0, 40)}`); // opaque callable invoked by a sync HOF — canonical `callback:`
|
|
9281
10002
|
}
|
|
9282
|
-
}
|
|
10003
|
+
};
|
|
10004
|
+
node.arguments?.forEach(hofArgArm);
|
|
9283
10005
|
}
|
|
9284
10006
|
// `fn.call(thisArg, …)` / `fn.apply(thisArg, args)` INVOKE the receiver function reference, and
|
|
9285
10007
|
// `Reflect.apply(fn, …)` / `Reflect.construct(Ctor, …)` invoke their FIRST ARGUMENT. The resolved
|
|
@@ -9317,6 +10039,11 @@ function visitCalls(node) {
|
|
|
9317
10039
|
if ((m === "call" || m === "apply") && recvText !== "Reflect") invokedRef = recv;
|
|
9318
10040
|
else if (recvText === "Reflect" && (m === "apply" || m === "construct"))
|
|
9319
10041
|
invokedRef = (node.arguments ?? [])[0] ?? null;
|
|
10042
|
+
// ⟨SOUNDNESS R780⟩ `(k.m as F).call(k)`, `(f satisfies F).call(t)` and `Reflect.apply(f as F, …)` invoke what
|
|
10043
|
+
// the bare spelling invokes. A single wrapped reference is not a CHOICE, so R955's leaves (which do unwrap)
|
|
10044
|
+
// returned null and the id/property gate below saw a ParenthesizedExpression: no edge, no `Unknown`, the
|
|
10045
|
+
// caller ABSENT (EXECUTED, `fx/r780`).
|
|
10046
|
+
invokedRef = unwrapArgExpr(invokedRef);
|
|
9320
10047
|
// ⟨SOUNDNESS R587⟩ …and the ELEMENT-ACCESS spelling of the invoked reference, on the same
|
|
9321
10048
|
// grounds as the HOF-ref arm above: `i["roll"].call(null, n)` invokes exactly what
|
|
9322
10049
|
// `i.roll.call(null, n)` invokes. Measured ABSENT at `6a639e6` in every cell
|
|
@@ -9430,8 +10157,7 @@ function visitCalls(node) {
|
|
|
9430
10157
|
const recvExpr = (ts.isPropertyAccessExpression(node.expression)
|
|
9431
10158
|
|| ts.isElementAccessExpression(node.expression)) ? node.expression.expression : null;
|
|
9432
10159
|
const rootClass = localReceiverClass(recvExpr);
|
|
9433
|
-
const direct = rootClass ? allOverrides.filter((om) =>
|
|
9434
|
-
ts.isClassDeclaration(om.parent) && classInSubtree(om.parent, rootClass)) : allOverrides;
|
|
10160
|
+
const direct = rootClass ? allOverrides.filter((om) => classInSubtree(overrideOwner(om), rootClass)) : allOverrides;
|
|
9435
10161
|
const overrides = isSuperReceiver(recvExpr) ? [] : memberDispatchBodies(decl, rootClass);
|
|
9436
10162
|
if (isSuperReceiver(recvExpr)) probeR871("super-call", decl, direct.length);
|
|
9437
10163
|
else if (direct.length <= CHA_FANOUT_LIMIT) probeR871("class", decl, overrides.length - direct.length);
|
|
@@ -9760,6 +10486,22 @@ function visitCalls(node) {
|
|
|
9760
10486
|
rec.direct.add("Unknown");
|
|
9761
10487
|
rec.why.add(`native:Storage.${name}`);
|
|
9762
10488
|
}
|
|
10489
|
+
// SOUNDNESS R241 — THE REST OF THE BROWSER'S PERSISTENT STORES AND NAVIGATION, which the comment
|
|
10490
|
+
// above named as "untouched and still pure" (the documented-limitation shape: writing it down is
|
|
10491
|
+
// what stopped it being measured). IndexedDB, the Cache API and the Origin Private File System
|
|
10492
|
+
// persist across sessions exactly as `Storage` does, and `location.assign`/`replace`/`reload`
|
|
10493
|
+
// navigate (a request leaves the page). All six of the row's spellings read ABSENT under
|
|
10494
|
+
// `lib: ["ES2022","DOM"]`, while `localStorage.setItem` on the same tree read `Unknown[native:…]`.
|
|
10495
|
+
// Same answer as `Storage`, for the same reason: the WHOLE interface (the denylist direction — an
|
|
10496
|
+
// unlisted member would be silent), `Unknown` because the backing store / navigation target is not
|
|
10497
|
+
// modelled, keyed on the RESOLVED DECLARATION'S PARENT so a project's own `class Cache` resolves
|
|
10498
|
+
// `<local>` and never reaches this arm. Reach on the (Node/server) roster is zero by construction;
|
|
10499
|
+
// the price is paid only by code that already touches these hosts.
|
|
10500
|
+
if (BROWSER_STORE_IFACES.has(parent)
|
|
10501
|
+
|| (parent === "Location" && (name === "assign" || name === "replace" || name === "reload"))) {
|
|
10502
|
+
rec.direct.add("Unknown");
|
|
10503
|
+
rec.why.add(`native:${parent}.${name}`);
|
|
10504
|
+
}
|
|
9763
10505
|
// `new EventSource(url)` / `new WebSocket(url)`: the constructor is declared on an anonymous
|
|
9764
10506
|
// `declare var` object type (symbol `__type`, no usable parent name), but reaching the es-lib
|
|
9765
10507
|
// branch already proves the ctor resolved to lib.dom (not a project class shadowing the name),
|
|
@@ -9997,6 +10739,22 @@ function visitCalls(node) {
|
|
|
9997
10739
|
&& (ts.isPropertyAccessExpression(node.expression) || ts.isElementAccessExpression(node.expression))
|
|
9998
10740
|
&& receiverIsProvenNonNetworkStream(node.expression.expression))
|
|
9999
10741
|
{ eff = null; effSuppressed = true; }
|
|
10742
|
+
// ⟨SOUNDNESS R966⟩ A FRAMEWORK'S OWN `listen` IS AN ACCEPT. `NET_ACCEPTING` is asked only of a call κ
|
|
10743
|
+
// classified `Net`, and κ classifies node's servers — so express `app.listen(p)` (declared in
|
|
10744
|
+
// express-serve-static-core), koa `app.listen(p)` and fastify `f.listen({port})` reached neither the effect
|
|
10745
|
+
// nor the accept mark: beside `net.connect(80, "ok.example")`, `allow Net in <fn> ok.example` exited 0
|
|
10746
|
+
// over a server answering anyone (EXECUTED: a client got the handler's reply). An inclusion list decided a
|
|
10747
|
+
// fail-closed question (R781's lesson), so this is the fail-closed reading: a DEPENDENCY member named
|
|
10748
|
+
// `listen` that κ did not answer is a network accept — `Net`, and `netAccepting` marks it `incomplete`.
|
|
10749
|
+
// The failure direction is an over-charge on a non-network `listen`; the one family known to be that is
|
|
10750
|
+
// denylisted by package (a JSON-RPC `connection.listen()` starts reading a transport whose OWN
|
|
10751
|
+
// construction decides whether it is a socket). Measured reach on the pinned roster: 1 unclassified
|
|
10752
|
+
// dependency `listen` (nest's FastifyAdapter), 0 non-network. Not seen: hapi `server.start()`
|
|
10753
|
+
// (`start` is p-queue's and repl's too), Bun.serve/Deno.serve (no types in any roster tree).
|
|
10754
|
+
if (!eff && !effSuppressed && member === "listen" && dependencyListenIsAccept(mod, decl)) {
|
|
10755
|
+
eff = "Net";
|
|
10756
|
+
if (process.env.CANDOR_R966_REACH) console.error(`R966-REACH ${mod}.listen`);
|
|
10757
|
+
}
|
|
10000
10758
|
if (eff) {
|
|
10001
10759
|
rec.direct.add(eff);
|
|
10002
10760
|
// SPEC §2 `fs` — refine an Fs we just PROVED with the direction its verb implies. DIRECT only
|
|
@@ -10327,7 +11085,16 @@ function visitCalls(node) {
|
|
|
10327
11085
|
// `const [,,x] = process.argv`, or handing it to a parser all reach the same process-startup state.
|
|
10328
11086
|
// Marked on the expression itself so every idiom counts without enumerating them.
|
|
10329
11087
|
if (isProcessArgvExpr(node)) markEnv();
|
|
11088
|
+
// ⟨R934⟩ a DESTRUCTURED parameter that every visible call site hands the environment reads its keys
|
|
11089
|
+
// as the frame is entered — the destructuring IS the read, in this function's frame.
|
|
11090
|
+
if (ts.isParameter(node) && ts.isObjectBindingPattern(node.name) && envAliasSymbols.has(node)) { markEnv(); ENVLOC_REACH?.("param-pattern env"); }
|
|
10330
11091
|
const kind = envRefKind(node);
|
|
11092
|
+
if (kind && ENVLOC_REACH && !envConsumerInert(node)) {
|
|
11093
|
+
const vd = ts.isIdentifier(node) ? checker.getSymbolAtLocation(node)?.valueDeclaration : null;
|
|
11094
|
+
const where = `${path.relative(process.cwd(), node.getSourceFile().fileName)}:${node.getStart() >= 0 ? node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).line + 1 : 0}`;
|
|
11095
|
+
if (vd && ts.isParameter(vd)) ENVLOC_REACH(`param ${kind} ${where}`);
|
|
11096
|
+
else if (!ts.isIdentifier(node) && !isProcessEnvExpr(node)) ENVLOC_REACH(`prop ${kind} ${where}`);
|
|
11097
|
+
}
|
|
10331
11098
|
if (kind && !envConsumerInert(node)) {
|
|
10332
11099
|
if (kind === "env") markEnv();
|
|
10333
11100
|
else {
|
|
@@ -10368,6 +11135,24 @@ function visitCalls(node) {
|
|
|
10368
11135
|
&& isWebStorageExpr(node.expression)) {
|
|
10369
11136
|
markStore(memberLabel(node));
|
|
10370
11137
|
}
|
|
11138
|
+
// SOUNDNESS R241 — the two PROPERTY spellings of the same host boundary, which no call arm sees: the
|
|
11139
|
+
// cookie jar (`document.cookie`, read OR written — it is a persistent store, `Storage`'s posture) and a
|
|
11140
|
+
// WRITE to any `Location` property (`location.href = u` navigates; so do `pathname`/`search`/`hash`).
|
|
11141
|
+
// lib.dom declares both as DATA properties, so the accessor arm finds nothing either. Keyed on the
|
|
11142
|
+
// resolved declaration living in the TypeScript lib and on its declaring interface, never on text.
|
|
11143
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
11144
|
+
const hsym = checker.getSymbolAtLocation(node.name);
|
|
11145
|
+
const hdecl = (hsym?.declarations ?? []).find((d) => /typescript\/lib\/lib\..*\.d\.ts$/.test(path.resolve(d.getSourceFile().fileName).replace(/\\/g, "/")));
|
|
11146
|
+
const iface = hdecl?.parent && ts.isInterfaceDeclaration(hdecl.parent) ? hdecl.parent.name.text : null;
|
|
11147
|
+
const pn = node.name.text;
|
|
11148
|
+
const hp = node.parent;
|
|
11149
|
+
const written = (hp && ts.isBinaryExpression(hp) && hp.left === node && envIsAssignOp(hp.operatorToken.kind))
|
|
11150
|
+
|| isDestructuringAssignTarget(node);
|
|
11151
|
+
if ((iface === "Document" && pn === "cookie") || (iface === "Location" && written)) {
|
|
11152
|
+
const owner = enclosing(node);
|
|
11153
|
+
if (owner) { const r = fns.get(owner); r.direct.add("Unknown"); r.why.add(`native:${iface}.${pn}`); }
|
|
11154
|
+
}
|
|
11155
|
+
}
|
|
10371
11156
|
else if (ts.isVariableDeclaration(node) && node.name && ts.isObjectBindingPattern(node.name)
|
|
10372
11157
|
&& node.initializer && isWebStorageExpr(node.initializer)) {
|
|
10373
11158
|
markStore("<destructure>");
|
|
@@ -10788,6 +11573,36 @@ function visitCalls(node) {
|
|
|
10788
11573
|
&& p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && p.operatorToken.kind <= ts.SyntaxKind.LastAssignment;
|
|
10789
11574
|
const recordKind = (kind) => {
|
|
10790
11575
|
const hits = accessorsAt(node, kind);
|
|
11576
|
+
// ⟨R246/R1039⟩ THE RESIDUE: a WRITE (R246) or READ (R1039) through a receiver whose static type is
|
|
11577
|
+
// `any`/`unknown` and whose value neither the assertion look-through nor the parameter provenance could
|
|
11578
|
+
// name. Recorded here; whether it is disclosed is decided after the fixpoint, when it is known which
|
|
11579
|
+
// project accessors of that kind have effects.
|
|
11580
|
+
if (!(hits && hits.length)) {
|
|
11581
|
+
let rt1; try { rt1 = checker.getTypeAtLocation(node.expression); } catch { rt1 = null; }
|
|
11582
|
+
if (anyishType(rt1) && !receiverValueTypes(node.expression).length) {
|
|
11583
|
+
const owner1 = enclosing(node);
|
|
11584
|
+
const nm1 = ts.isPropertyAccessExpression(node) ? node.name.text
|
|
11585
|
+
: (node.argumentExpression && ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null);
|
|
11586
|
+
if (owner1) anyRecvSetResidue.push({ owner: owner1, name: nm1, kind });
|
|
11587
|
+
}
|
|
11588
|
+
}
|
|
11589
|
+
if (R246_REACH) {
|
|
11590
|
+
let rt0; try { rt0 = checker.getTypeAtLocation(node.expression); } catch { rt0 = null; }
|
|
11591
|
+
if (anyishType(rt0) || peelAssertions(node.expression) !== node.expression) {
|
|
11592
|
+
const vts = receiverValueTypes(node.expression);
|
|
11593
|
+
const nm = ts.isPropertyAccessExpression(node) ? node.name.text
|
|
11594
|
+
: (node.argumentExpression && ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null);
|
|
11595
|
+
if (hits && hits.length && vts.length) R246_REACH(`resolved ${kind}`);
|
|
11596
|
+
else if (anyishType(rt0) && !vts.length) {
|
|
11597
|
+
projAccessorNames ??= (() => { const m = { get: new Set(), set: new Set() };
|
|
11598
|
+
const v = (n) => { if ((ts.isGetAccessorDeclaration(n) || ts.isSetAccessorDeclaration(n)) && n.name && !ts.isComputedPropertyName(n.name))
|
|
11599
|
+
(ts.isGetAccessorDeclaration(n) ? m.get : m.set).add(n.name.getText()); ts.forEachChild(n, v); };
|
|
11600
|
+
for (const sf of sources) v(sf); return m; })();
|
|
11601
|
+
const could = nm == null ? projAccessorNames[kind].size > 0 : projAccessorNames[kind].has(nm);
|
|
11602
|
+
R246_REACH(`residue ${kind} ${nm == null ? "dyn" : "named"} ${could ? "could-hit-project-accessor" : "no-project-accessor"}`);
|
|
11603
|
+
}
|
|
11604
|
+
}
|
|
11605
|
+
}
|
|
10791
11606
|
if (hits && hits.length) {
|
|
10792
11607
|
const owner = enclosing(node);
|
|
10793
11608
|
if (!owner) return;
|
|
@@ -10864,7 +11679,10 @@ function visitCalls(node) {
|
|
|
10864
11679
|
// the four symbol rows below); what changed is that the other spelling now asks the same code.
|
|
10865
11680
|
const keyT = ts.isElementAccessExpression(node) && node.argumentExpression
|
|
10866
11681
|
? checker.getTypeAtLocation(node.argumentExpression) : null;
|
|
10867
|
-
|
|
11682
|
+
// R246 — the VALUE's types too: `(s as any)[k] = v`, or `o[k] = v` with `o: any` fed an `S`.
|
|
11683
|
+
const rprops = [...(rt?.getProperties?.() ?? [])];
|
|
11684
|
+
for (const vt of receiverValueTypes(node.expression)) rprops.push(...(vt.getProperties?.() ?? []));
|
|
11685
|
+
for (const prop of rprops) {
|
|
10868
11686
|
if (!accessorsFromSym(prop, kind).length) continue;
|
|
10869
11687
|
if (!keyCouldNameAccessor(prop, keyT)) continue;
|
|
10870
11688
|
const owner = enclosing(node);
|
|
@@ -11282,15 +12100,24 @@ for (const sf of sources) visitCalls(sf);
|
|
|
11282
12100
|
})(cls);
|
|
11283
12101
|
for (const dec of decorators) {
|
|
11284
12102
|
add(localBodiedUnit(dec), "deco"); // the application
|
|
11285
|
-
|
|
11286
|
-
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
|
12103
|
+
const e = unwrapArgExpr(dec.expression); // R780
|
|
11287
12104
|
if (ts.isCallExpression(e)) {
|
|
11288
12105
|
add(localBodiedUnit(e), "deco"); // the factory call
|
|
11289
12106
|
add(`${moduleOf(e.getSourceFile())}.${decoratorArgLocal(e)}`, "arg"); // ⟨R944⟩ the ONE spelling
|
|
11290
12107
|
}
|
|
11291
12108
|
}
|
|
11292
12109
|
for (const m of cls.members ?? [])
|
|
11293
|
-
if (ts.isClassStaticBlockDeclaration(m)
|
|
12110
|
+
if (ts.isClassStaticBlockDeclaration(m)
|
|
12111
|
+
|| (ts.isPropertyDeclaration(m) && m.initializer && (ts.getCombinedModifierFlags(m) & ts.ModifierFlags.Static)))
|
|
12112
|
+
add(staticBlockQual(m), "static"); // R815: a static field initialiser shares the block's unit
|
|
12113
|
+
// R815: the `extends <expr>` unit — run by the definer (here) and reached by every construction (`super`).
|
|
12114
|
+
const ctorQ = nodeName.get(cls);
|
|
12115
|
+
const herQ = ctorQ && ctorQ.endsWith(".constructor") ? ctorQ.replace(/\.constructor$/, ".<heritage>") : null;
|
|
12116
|
+
// Wired only when the expression attributed something: a bare `extends Base` mints the unit (a reference was
|
|
12117
|
+
// attributed) and carries nothing, and an edge to an empty unit is a `calls` entry on every subclass for no answer.
|
|
12118
|
+
const herR = herQ && fns.get(herQ);
|
|
12119
|
+
if (herR && ["direct", "edges", "why", "blind", "hosts", "paths", "cmds", "incomplete", "dispatch", "tables"]
|
|
12120
|
+
.some((k) => herR[k]?.size)) { add(herQ, "heritage"); fns.get(ctorQ)?.edges.add(herQ); }
|
|
11294
12121
|
if (!targets.length) continue;
|
|
11295
12122
|
const from = enclosing(cls.parent);
|
|
11296
12123
|
const rec = from && fns.get(from);
|
|
@@ -11797,6 +12624,52 @@ const inferred = new Map([...fns.keys()].map((k) => [k, new Set(fns.get(k).direc
|
|
|
11797
12624
|
if (!queued.has(c)) { queued.add(c); queue.push(c); }
|
|
11798
12625
|
}
|
|
11799
12626
|
}
|
|
12627
|
+
// ⟨SOUNDNESS R246⟩ THE HEDGE FOR WHAT RESOLUTION CANNOT REACH. A write `o.name = v` / `o[k] = v` through a
|
|
12628
|
+
// receiver typed `any`/`unknown` whose value neither an assertion nor a visible call site names — an escaped
|
|
12629
|
+
// function's parameter, an exported one with no visible caller, an `any` field or return — can still land
|
|
12630
|
+
// on a project SETTER (EXECUTED: `[new Sx()].forEach(w)` with `w(o: any) { o.token = v }` ran Sx's setter, and
|
|
12631
|
+
// `w` was ABSENT, `deny Fs`/`deny Unknown` both exit 0). Which object it is cannot be known, so this
|
|
12632
|
+
// DISCLOSES rather than charges: `Unknown[reflect:accessor:any-receiver]` on the writer, and only when the
|
|
12633
|
+
// project declares a setter that could be the one — the SAME name (or any string-named setter for an
|
|
12634
|
+
// unpinnable key) — AND that setter's body has an effect. A setter-free or pure-setter project pays nothing.
|
|
12635
|
+
// Priced on the 28-entry roster (see the commit for the measured count — a handful of units, <0.1%).
|
|
12636
|
+
// SOUNDNESS R1039 — THE READ SIDE, SAME RULE: `o.name` / `o[k]` through such a receiver can run a project
|
|
12637
|
+
// GETTER (EXECUTED: an escaped `r(o: any) { return o.token }` handed a class whose `get token()` writes a
|
|
12638
|
+
// file — `r` ABSENT, `deny Unknown`/`deny Fs Unknown` exit 0 on v0.40.0). Same admission: a getter WITH A
|
|
12639
|
+
// BODY and an EFFECT that the read could name (the same name, or any string-named one for an unpinnable key).
|
|
12640
|
+
if (anyRecvSetResidue.length) {
|
|
12641
|
+
const accUnits = { get: new Map(), set: new Map() }; // kind -> name -> [unit]
|
|
12642
|
+
const visitS = (n) => {
|
|
12643
|
+
// An accessor WITH A BODY: a body-less declaration (a project typings stub) is already its own `native:`
|
|
12644
|
+
// disclosure, and arming every `any` write on it measured as pure noise (typeorm's bson `ObjectId.set id`).
|
|
12645
|
+
if ((ts.isSetAccessorDeclaration(n) || ts.isGetAccessorDeclaration(n)) && n.body && n.name && !ts.isComputedPropertyName(n.name)) {
|
|
12646
|
+
const u = nodeName.get(n);
|
|
12647
|
+
const m = ts.isSetAccessorDeclaration(n) ? accUnits.set : accUnits.get;
|
|
12648
|
+
if (u) { const k = n.name.getText(); (m.get(k) ?? m.set(k, []).get(k)).push(u); }
|
|
12649
|
+
}
|
|
12650
|
+
ts.forEachChild(n, visitS);
|
|
12651
|
+
};
|
|
12652
|
+
for (const sf of sources) visitS(sf);
|
|
12653
|
+
const effectful = (u) => (inferred.get(u)?.size ?? 0) > 0;
|
|
12654
|
+
const anyEffectful = { get: [...accUnits.get.values()].flat().some(effectful), set: [...accUnits.set.values()].flat().some(effectful) };
|
|
12655
|
+
const hedged = new Set();
|
|
12656
|
+
for (const { owner, name, kind } of anyRecvSetResidue) {
|
|
12657
|
+
const hit = name == null ? anyEffectful[kind] : (accUnits[kind].get(name) ?? []).some(effectful);
|
|
12658
|
+
if (!hit || !fns.has(owner)) continue;
|
|
12659
|
+
const rec = fns.get(owner);
|
|
12660
|
+
rec.direct.add("Unknown"); rec.why.add("reflect:accessor:any-receiver");
|
|
12661
|
+
hedged.add(owner);
|
|
12662
|
+
}
|
|
12663
|
+
// propagate the added `Unknown` to every transitive caller (the fixpoint above has already run)
|
|
12664
|
+
const queue = [...hedged];
|
|
12665
|
+
const seenU = new Set(queue);
|
|
12666
|
+
for (let head = 0; head < queue.length; head++) {
|
|
12667
|
+
const u = queue[head];
|
|
12668
|
+
inferred.get(u)?.add("Unknown");
|
|
12669
|
+
for (const c of callersOf.get(u) ?? [])
|
|
12670
|
+
if (!seenU.has(c) && !inferred.get(c)?.has("Unknown")) { seenU.add(c); queue.push(c); }
|
|
12671
|
+
}
|
|
12672
|
+
}
|
|
11800
12673
|
// `fsKinds` joins the propagated surfaces: kinds TRAVEL the call graph (a caller that transitively only
|
|
11801
12674
|
// writes IS a writer), and the "?" poison travels with them so a caller of an undetermined-kind function
|
|
11802
12675
|
// inherits the SUPPRESSION rather than a half-answer. Pinned by conformance PART 31.
|