candor-ts 0.40.0 → 0.40.2
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/README.md +14 -0
- package/package.json +6 -3
- package/policy.mjs +8 -0
- package/scan.mjs +1356 -108
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
|
|
|
@@ -1645,11 +1653,60 @@ if (NO_SOURCES) {
|
|
|
1645
1653
|
// types). Resolved via the module system, NOT a fixed relative path — npm HOISTS dependencies, so
|
|
1646
1654
|
// in an npx/install tree @types/node sits BESIDE candor-ts, not inside it (the second probe's
|
|
1647
1655
|
// catch). The TARGET's own @types win when present.
|
|
1656
|
+
// ⟨SOUNDNESS R1062⟩ THE ENGINE'S OWN TYPE PACKAGES ARE PART OF THE ENGINE. The compiler (and its `lib.*.d.ts`),
|
|
1657
|
+
// `@types/node` (the fallback root below) and `undici-types` (which `@types/node` re-exports `fetch`/`WebSocket`
|
|
1658
|
+
// from) decide resolutions, so a different version is, in principle, a different engine. A fresh install of the
|
|
1659
|
+
// published v0.40.1 resolved `@types/node` 25.9.9 against a tested 25.9.2 — MEASURED to move nothing on vitest
|
|
1660
|
+
// (the four-row difference first blamed on it was the type-ROOT leak below), and unmeasured everywhere else. The
|
|
1661
|
+
// package now pins them exactly and ships `npm-shrinkwrap.json`; this note covers the installs that ignore a
|
|
1662
|
+
// shrinkwrap (pnpm, yarn) or a hand-edited tree. Advisory: an untested version is not a wrong verdict, but it is
|
|
1663
|
+
// not the one this build's tests measured, and the operator should be told.
|
|
1664
|
+
{
|
|
1665
|
+
try {
|
|
1666
|
+
const lockPath = ["npm-shrinkwrap.json", "package-lock.json"].map((f) => path.join(ENGINE_DIR, f)).find((f) => fs.existsSync(f));
|
|
1667
|
+
if (lockPath) {
|
|
1668
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, "utf8")).packages ?? {};
|
|
1669
|
+
const req = createRequire(path.join(ENGINE_DIR, "scan.mjs"));
|
|
1670
|
+
const loaded = (name, from) => {
|
|
1671
|
+
try { return JSON.parse(fs.readFileSync(createRequire(from).resolve(`${name}/package.json`), "utf8")).version; }
|
|
1672
|
+
catch { return null; }
|
|
1673
|
+
};
|
|
1674
|
+
const nodeTypesPkg = (() => { try { return req.resolve("@types/node/package.json"); } catch { return null; } })();
|
|
1675
|
+
const seen = [["typescript", ts.version], ["@types/node", loaded("@types/node", path.join(ENGINE_DIR, "scan.mjs"))],
|
|
1676
|
+
["undici-types", nodeTypesPkg ? loaded("undici-types", nodeTypesPkg) : null]];
|
|
1677
|
+
for (const [name, got] of seen) {
|
|
1678
|
+
const want = lock[`node_modules/${name}`]?.version;
|
|
1679
|
+
if (want && got && want !== got)
|
|
1680
|
+
console.error(`candor-ts: note — this install loaded ${name} ${got}; candor-ts ${PKG_VERSION} was tested `
|
|
1681
|
+
+ `with ${want}, and type packages change verdicts (SOUNDNESS R1062). Reinstall with npm, which honours the `
|
|
1682
|
+
+ `shipped npm-shrinkwrap.json.`);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
} catch { /* advisory only */ }
|
|
1686
|
+
}
|
|
1648
1687
|
if (!compilerOptions.typeRoots) {
|
|
1649
1688
|
const roots = [path.join(rootDir, "node_modules", "@types")];
|
|
1650
1689
|
try {
|
|
1651
1690
|
const req = createRequire(path.join(ENGINE_DIR, "scan.mjs"));
|
|
1652
|
-
|
|
1691
|
+
const nodeTypes = path.dirname(req.resolve("@types/node/package.json"));
|
|
1692
|
+
const engineTypes = path.dirname(nodeTypes);
|
|
1693
|
+
// ⟨SOUNDNESS R1062⟩ …AND ONLY @types/node. A type ROOT is not just where `types: ["node"]` is looked up: the
|
|
1694
|
+
// compiler also resolves a TARGET's bare import (`import type { Node } from "estree"`) through it when the
|
|
1695
|
+
// target has no package of that name installed. The engine's directory holds whatever else was installed
|
|
1696
|
+
// beside it — in a development tree, eslint's `@types/estree` — so vitest (scanned without node_modules) had
|
|
1697
|
+
// its `estree` imports resolved against the ENGINE's copy, and four rows that are `Unknown` from a clean
|
|
1698
|
+
// `npm install candor-ts` were absent from the same source run out of the repo (MEASURED; the `@types/node`
|
|
1699
|
+
// version, 25.9.2 vs 25.9.9, moved nothing). A verdict must not depend on the engine's neighbours, so when
|
|
1700
|
+
// that directory holds anything but `node`, the root is a private directory holding `node` alone.
|
|
1701
|
+
let others = [];
|
|
1702
|
+
try { others = fs.readdirSync(engineTypes).filter((n) => n !== "node" && !n.startsWith(".")); } catch { others = []; }
|
|
1703
|
+
if (!others.length) roots.push(engineTypes);
|
|
1704
|
+
else {
|
|
1705
|
+
const shim = fs.mkdtempSync(path.join(os.tmpdir(), "candor-ts-types-"));
|
|
1706
|
+
fs.symlinkSync(nodeTypes, path.join(shim, "node"), "junction");
|
|
1707
|
+
process.on("exit", () => { try { fs.rmSync(shim, { recursive: true, force: true }); } catch { /* best effort */ } });
|
|
1708
|
+
roots.push(shim);
|
|
1709
|
+
}
|
|
1653
1710
|
} catch {}
|
|
1654
1711
|
compilerOptions.typeRoots = roots;
|
|
1655
1712
|
}
|
|
@@ -2655,7 +2712,16 @@ function declModule(decl) {
|
|
|
2655
2712
|
let m = f.match(/@types\/node\/(.+?)\.d\.ts$/);
|
|
2656
2713
|
if (m) return m[1];
|
|
2657
2714
|
if (/typescript\/lib\/lib\..*\.d\.ts$/.test(f)) return "<es-lib>";
|
|
2658
|
-
|
|
2715
|
+
// ⟨SOUNDNESS R778⟩ The INNERMOST `node_modules/<pkg>/` owns the file, not the first. pnpm's real path is
|
|
2716
|
+
// `node_modules/.pnpm/fs-extra@11.2.0/node_modules/fs-extra/…` and an npm nested install is
|
|
2717
|
+
// `node_modules/outer/node_modules/inner/…`; the first-segment match keyed every foreign package of a pnpm
|
|
2718
|
+
// tree as `.pnpm` and `inner` as `outer`. MEASURED with the installer the only variable (fx/r778-{npm,pnpm},
|
|
2719
|
+
// EXECUTED to write the file): npm read `['Fs']` and `deny Fs` exit 1, pnpm read `[]` + `invisible:['.pnpm']`
|
|
2720
|
+
// and exit 0 — a FALSE disclosure (`.pnpm` is not a package) hiding a κ-classified effect. For a flat npm
|
|
2721
|
+
// layout the first and last segments are the same one, so no npm key moves. A file directly under a dot-store
|
|
2722
|
+
// with no inner `node_modules/` (no package segment at all) asks the nearest `package.json` instead.
|
|
2723
|
+
m = [...f.matchAll(/node_modules\/(@[^/]+\/[^/]+|[^/]+)\//g)].at(-1) ?? null;
|
|
2724
|
+
if (m && m[1].startsWith(".")) m = (() => { const n = nearestPackageName(f); return n ? [null, n] : m; })();
|
|
2659
2725
|
if (m) {
|
|
2660
2726
|
// `@types/X` (DefinitelyTyped) provides types for the RUNTIME package X — map it to X so the curated κ
|
|
2661
2727
|
// tier (keyed by the runtime name: pg/ws/…) fires. Without this a package typed via @types resolved to
|
|
@@ -3461,8 +3527,7 @@ function literalHeadHostUrl(expr) {
|
|
|
3461
3527
|
// (an assignment rooted at it, another call's argument, an alias, a return) refuses the capture.
|
|
3462
3528
|
function determinedUrlObject(expr, call, depth = 0) {
|
|
3463
3529
|
if (!expr || depth > 1) return null;
|
|
3464
|
-
|
|
3465
|
-
|| ts.isSatisfiesExpression?.(expr)) expr = expr.expression;
|
|
3530
|
+
expr = unwrapArgExpr(expr); // R780: the one transparent-wrapper set
|
|
3466
3531
|
if (ts.isNewExpression(expr) && ts.isIdentifier(expr.expression)
|
|
3467
3532
|
&& (expr.expression.text === "URL" || expr.expression.text === "Request")) {
|
|
3468
3533
|
const decls = checker.getSymbolAtLocation(expr.expression)?.declarations ?? [];
|
|
@@ -3516,8 +3581,17 @@ function determinedUrlObject(expr, call, depth = 0) {
|
|
|
3516
3581
|
visit(d.getSourceFile());
|
|
3517
3582
|
return safe ? determinedUrlObject(d.initializer, call, depth + 1) : null;
|
|
3518
3583
|
}
|
|
3519
|
-
//
|
|
3520
|
-
//
|
|
3584
|
+
// ⟨SOUNDNESS R780⟩ THE ONE TRANSPARENT-WRAPPER SET. Parentheses, `as`, `<T>x`, `!` and `satisfies` change no runtime
|
|
3585
|
+
// value, so every "is this expression THAT reference / THAT object?" question asks it through this one function.
|
|
3586
|
+
// There were seventeen hand-rolled loops answering it with FIVE different wrapper sets (parens only; +`as`;
|
|
3587
|
+
// +`!`; +`satisfies`; +`<T>`), and every gap was a spelling the engine went blind on: `(k.m as F).call(k)`,
|
|
3588
|
+
// `Reflect.apply(f as F, …)` and `(<any>globalThis).process.env.HOME` were ABSENT (EXECUTED, lane fixture
|
|
3589
|
+
// `fx/r780`), `(f as F).bind(t)` and `run((f))` were opaque. Two loops are deliberately NOT routed here:
|
|
3590
|
+
// `conversionSourceTracked` stops at `as` because an `as` IS the conversion it is asking about (R958 reads the
|
|
3591
|
+
// assertion separately), and `rootsAtStdStream` is a SUPPRESSOR (it frees `process.stdout.write` of a fabricated
|
|
3592
|
+
// Net) — widening a suppressor removes charges, which is not this fix's direction.
|
|
3593
|
+
// (Also R947: the invoked function's own ARGUMENTS on the reflective routes, or `null` when they are not
|
|
3594
|
+
// visible here — a spread, an `.apply` of a non-literal array. `null` is the safe answer: it captures nothing.)
|
|
3521
3595
|
function unwrapArgExpr(e) {
|
|
3522
3596
|
while (e && (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isTypeAssertionExpression?.(e)
|
|
3523
3597
|
|| ts.isNonNullExpression(e) || ts.isSatisfiesExpression?.(e))) e = e.expression;
|
|
@@ -3701,6 +3775,14 @@ function kappaOfRef(d2, ref) {
|
|
|
3701
3775
|
function netEstablishingVerb(member, mod) {
|
|
3702
3776
|
return NET_ESTABLISHING.has(member) || (/^(node:)?dgram$/.test(mod ?? "") && member === "send");
|
|
3703
3777
|
}
|
|
3778
|
+
// ⟨R966⟩ see the (CLASSIFY) arm. A package whose `listen` is not a network accept: JSON-RPC connections start
|
|
3779
|
+
// reading a transport chosen (and, when it is a socket, charged) where the transport was built.
|
|
3780
|
+
const NON_NETWORK_LISTEN_PKGS = /^(vscode-jsonrpc|vscode-languageserver(-protocol)?|vscode-languageclient)(\/|$)/;
|
|
3781
|
+
function dependencyListenIsAccept(mod, decl) {
|
|
3782
|
+
if (!mod || mod === "<local>" || mod === "<es-lib>" || mod.startsWith("/") || declIsNodeTypes(decl) || isOwnPackageDecl(decl))
|
|
3783
|
+
return false;
|
|
3784
|
+
return !NON_NETWORK_LISTEN_PKGS.test(mod);
|
|
3785
|
+
}
|
|
3704
3786
|
function netAcceptingVerb(member, kMod) {
|
|
3705
3787
|
return NET_ACCEPTING.has(member) || (/^(node:)?inspector(\/promises)?$/.test(kMod ?? "") && member === "open");
|
|
3706
3788
|
}
|
|
@@ -3990,6 +4072,7 @@ const fns = new Map(); // qualified name -> { direct, edges, hosts, ta
|
|
|
3990
4072
|
const bodylessDecls = new Map(); // qual -> {node, mod, abstract, owner, member}
|
|
3991
4073
|
const unlistedSeen = new Map(); // the κ-coverage ledger: unlisted npm package -> call-site count
|
|
3992
4074
|
const nodeName = new WeakMap(); // declaration node -> qualified name
|
|
4075
|
+
const classExprCtorQual = new Map(); // ⟨R1016⟩ ClassExpression -> its constructor unit (see `mintClassExprCtor`)
|
|
3993
4076
|
// ⟨scan-boundary, export-alias⟩ `module.exports = { thing: _internalImplName }` / `exports.thing =
|
|
3994
4077
|
// _internalImplName` — a RENAMED re-export whose RHS NAMES an EXISTING declaration rather than being an
|
|
3995
4078
|
// inline function. `localName` mints a CJS unit for `module.exports = function(){}` and
|
|
@@ -4142,6 +4225,14 @@ const CHA_FANOUT_LIMIT = 12;
|
|
|
4142
4225
|
// rather than inlined for the reason `CHA_FANOUT_LIMIT` was: two literals for one rule is how they drift.
|
|
4143
4226
|
const TYPINGS_CENSUS_CAP = 128;
|
|
4144
4227
|
const classOverrides = new Map();// base-method MemberDeclaration node -> overriding subclass member nodes (class-CHA)
|
|
4228
|
+
// ⟨SOUNDNESS R873⟩ An override that is not a class MEMBER: `this.m = () => …` assigned inside an instance body of
|
|
4229
|
+
// class `C` replaces `m` on every `C` instance. Registered in `classOverrides` like a member, keyed to the class
|
|
4230
|
+
// that performs the assignment here, because the node's `.parent` is an expression, not the class.
|
|
4231
|
+
const assignedOverrideClass = new Map(); // assigned override node -> the class-like node whose instances it overrides
|
|
4232
|
+
// The class an override node belongs to, for receiver-subtree scoping: a member's own class (declaration OR
|
|
4233
|
+
// expression — ⟨R956⟩), or the assigning class of a ⟨R873⟩ assigned override.
|
|
4234
|
+
const overrideOwner = (om) => assignedOverrideClass.get(om)
|
|
4235
|
+
?? ((om?.parent && (ts.isClassDeclaration(om.parent) || ts.isClassExpression(om.parent))) ? om.parent : null);
|
|
4145
4236
|
const classDescendants = new Map();// base ClassDeclaration -> transitive LOCAL subclass ClassDeclarations (coercion-CHA)
|
|
4146
4237
|
// R954 — the conformer registries (filled by the conversion pass after `walkStructural`).
|
|
4147
4238
|
const classConformers = new Map(); // local ClassDeclaration -> conformer nodes (local class, literal, dep class)
|
|
@@ -4226,7 +4317,7 @@ function overrideClosure(seeds) {
|
|
|
4226
4317
|
const out = [];
|
|
4227
4318
|
for (const s of seeds) {
|
|
4228
4319
|
for (const m of [s, ...overrideDescent(s)]) {
|
|
4229
|
-
if (isOverridableMethod(m) && !out.includes(m)) out.push(m);
|
|
4320
|
+
if ((isOverridableMethod(m) || assignedOverrideClass.has(m)) && !out.includes(m)) out.push(m);
|
|
4230
4321
|
if (out.length > CHA_FANOUT_LIMIT + 1) return out;
|
|
4231
4322
|
}
|
|
4232
4323
|
}
|
|
@@ -4277,7 +4368,7 @@ function overrideDescent(decl) {
|
|
|
4277
4368
|
function memberDispatchBodies(decl, rootClass) {
|
|
4278
4369
|
const all = overrideDescent(decl);
|
|
4279
4370
|
const scoped = (!rootClass || all.length === 0) ? all
|
|
4280
|
-
: all.filter((om) =>
|
|
4371
|
+
: all.filter((om) => classInSubtree(overrideOwner(om), rootClass)); // R873/R956: assigned and class-expression overrides
|
|
4281
4372
|
// R954 — …and the CONFORMERS registered at the receiver's class (a value the checker showed
|
|
4282
4373
|
// converted to it, or to a subclass of it), which run for this dispatch exactly as an override does. Scoped
|
|
4283
4374
|
// the same way: registered at `Ct` and its ancestors, so a SUBTYPE receiver never sees them.
|
|
@@ -4522,6 +4613,7 @@ const STABLE_KEY_SHAPES = {
|
|
|
4522
4613
|
callable: new Set([ts.SyntaxKind.ArrowFunction, ts.SyntaxKind.FunctionExpression]),
|
|
4523
4614
|
decorator: new Set([ts.SyntaxKind.ArrowFunction, ts.SyntaxKind.FunctionExpression]),
|
|
4524
4615
|
"decorator-arg": new Set([ts.SyntaxKind.CallExpression]),
|
|
4616
|
+
classexpr: new Set([ts.SyntaxKind.ClassExpression]), // ⟨R1016⟩ a class expression's constructor unit
|
|
4525
4617
|
computed: null, // a descriptor member of any shape — counted over EVERY node under the anchor
|
|
4526
4618
|
};
|
|
4527
4619
|
const stableKeySanitise = (s) => s.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
@@ -5093,6 +5185,7 @@ for (const sf of sources) {
|
|
|
5093
5185
|
// (a wholly-unresolvable target leaves only the minted unit — nothing to join a forcing site to.)
|
|
5094
5186
|
}
|
|
5095
5187
|
}
|
|
5188
|
+
if (ts.isClassExpression(node)) mintClassExprCtor(node); // ⟨R1016⟩
|
|
5096
5189
|
ts.forEachChild(node, collect);
|
|
5097
5190
|
})(sf);
|
|
5098
5191
|
}
|
|
@@ -5153,7 +5246,9 @@ for (const { mod, name, ident } of exportAliasCandidates) {
|
|
|
5153
5246
|
const baseClassOf = localBaseClassOf;
|
|
5154
5247
|
for (const sf of sources) {
|
|
5155
5248
|
(function scan(node) {
|
|
5156
|
-
|
|
5249
|
+
// ⟨SOUNDNESS R956⟩ a CLASS EXPRESSION is a subclass like any other: `new (class extends BaseC { m(){…} })()`
|
|
5250
|
+
// reaching a `BaseC`-typed `b.m()` was ABSENT at every gate (EXECUTED) because this index read declarations only.
|
|
5251
|
+
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
|
5157
5252
|
// Local-DESCENDANT index (the coercion-CHA universe, below). classOverrides is keyed by an
|
|
5158
5253
|
// ANCESTOR MEMBER, so it only sees an override whose base ALSO declares the name. The coercion
|
|
5159
5254
|
// protocol's members are the opposite shape: `class Sub extends Base { toString(){…} }` where
|
|
@@ -5190,6 +5285,7 @@ for (const { mod, name, ident } of exportAliasCandidates) {
|
|
|
5190
5285
|
if (!classOverrides.has(ancestor)) classOverrides.set(ancestor, []);
|
|
5191
5286
|
classOverrides.get(ancestor).push(m);
|
|
5192
5287
|
foundLocal = true;
|
|
5288
|
+
mintClassExprOverride(node, m, name); // R956
|
|
5193
5289
|
break;
|
|
5194
5290
|
}
|
|
5195
5291
|
base = baseClassOf(base);
|
|
@@ -5209,16 +5305,166 @@ for (const { mod, name, ident } of exportAliasCandidates) {
|
|
|
5209
5305
|
if (!foreignClassOverrides.has(fm)) foreignClassOverrides.set(fm, []);
|
|
5210
5306
|
const arr = foreignClassOverrides.get(fm);
|
|
5211
5307
|
if (!arr.includes(m)) arr.push(m);
|
|
5308
|
+
mintClassExprOverride(node, m, name); // R956
|
|
5212
5309
|
}
|
|
5213
5310
|
}
|
|
5214
5311
|
}
|
|
5215
5312
|
}
|
|
5216
5313
|
}
|
|
5314
|
+
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) registerAssignedOverrides(node); // R873
|
|
5217
5315
|
ts.forEachChild(node, scan);
|
|
5218
5316
|
})(sf);
|
|
5219
5317
|
}
|
|
5220
5318
|
}
|
|
5221
5319
|
|
|
5320
|
+
// ⟨SOUNDNESS R1016⟩ A CLASS EXPRESSION'S CONSTRUCTOR IS A UNIT, as a class declaration's always was. Without one,
|
|
5321
|
+
// `const L = class { constructor() { fs.writeFileSync(…) } }; new L()` and `new Holder.Inner()` over a
|
|
5322
|
+
// `static Inner = class {…}` had nothing to edge to: the constructing function read `['Unknown']` (`deny Fs` exit 0
|
|
5323
|
+
// on a write that ran — EXECUTED), and the body's `Fs` fell through `enclosing` to whatever EVALUATES the class
|
|
5324
|
+
// (`<module>`, `Holder.<static-init>`), which never constructs it (EXECUTED: importing the module writes nothing).
|
|
5325
|
+
// The unit holds what runs at construction — the explicit constructor's body and parameters, and every INSTANCE
|
|
5326
|
+
// field initialiser that is not itself a function value (that one runs when called, and stays where it was) —
|
|
5327
|
+
// so `new` resolves to it through the checker like any constructor, and an implicit constructor is found through
|
|
5328
|
+
// the constructed value's TYPE (`classExprCtorOfNew`). Minted only where there is something to hold.
|
|
5329
|
+
//
|
|
5330
|
+
// WHETHER THE EVALUATING UNIT KEEPS THE CHARGE is ⟨R519⟩'s question, and its answer stands: minting is an
|
|
5331
|
+
// ADDITION unless every construction is provably one this scan edges. A class VALUE that escapes — exported,
|
|
5332
|
+
// returned, passed, stored, `extends`-ed, read as a static — can be constructed where no edge reaches it (another
|
|
5333
|
+
// package, which cannot name an anonymous class's constructor; an `any`-typed `new`), so pass 2a′ keeps the
|
|
5334
|
+
// evaluator's containment edge for it. Only a `const`-bound, unexported class expression whose every reference
|
|
5335
|
+
// is a `new` callee (or a type position, or an `instanceof` operand) drops it — there, every run of the body is
|
|
5336
|
+
// a `new` the scan resolved, and the evaluator's charge was the fabrication R1016 measured.
|
|
5337
|
+
function mintClassExprCtor(cls) {
|
|
5338
|
+
const ctor = (cls.members ?? []).find((m) => ts.isConstructorDeclaration(m) && m.body);
|
|
5339
|
+
const fieldInit = (cls.members ?? []).some((m) => ts.isPropertyDeclaration(m) && m.initializer
|
|
5340
|
+
&& !(ts.getCombinedModifierFlags(m) & ts.ModifierFlags.Static)
|
|
5341
|
+
&& !ts.isArrowFunction(m.initializer) && !ts.isFunctionExpression(m.initializer));
|
|
5342
|
+
if (!ctor && !fieldInit) return null;
|
|
5343
|
+
const sf = cls.getSourceFile();
|
|
5344
|
+
const local = `<class>@${stableUnitTag("classexpr", cls)}.constructor`;
|
|
5345
|
+
const qual = `${moduleOf(sf)}.${local}`;
|
|
5346
|
+
if (!fns.has(qual)) {
|
|
5347
|
+
const at = ctor ?? cls;
|
|
5348
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(at.getStart());
|
|
5349
|
+
fns.set(qual, { local, direct: new Set(), fsKinds: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
|
|
5350
|
+
cmds: new Set(), paths: new Set(), blind: new Set(), incomplete: new Set(), dispatch: new Set(),
|
|
5351
|
+
why: new Set(), entry: false,
|
|
5352
|
+
loc: `${path.relative(rootDir, sf.fileName)}:${line + 1}:${character + 1}`,
|
|
5353
|
+
endLine: sf.getLineAndCharacterOfPosition(at.getEnd()).line + 1 });
|
|
5354
|
+
}
|
|
5355
|
+
if (ctor) nodeName.set(ctor, qual);
|
|
5356
|
+
classExprCtorQual.set(cls, qual);
|
|
5357
|
+
if (process.env.CANDOR_R1016_REACH) console.error(`R1016-REACH mint ${qual}`);
|
|
5358
|
+
return qual;
|
|
5359
|
+
}
|
|
5360
|
+
// The class-expression constructor unit a `new X(…)` runs, read off the constructed value's TYPE — the route an
|
|
5361
|
+
// IMPLICIT constructor needs (the signature then has no declaration, or names the BASE's), and harmless beside an
|
|
5362
|
+
// explicit one, which the signature already names.
|
|
5363
|
+
function classExprCtorOfNew(node) {
|
|
5364
|
+
if (!classExprCtorQual.size || !ts.isNewExpression(node) || !node.expression) return null;
|
|
5365
|
+
let vd;
|
|
5366
|
+
try { vd = checker.getTypeAtLocation(node.expression)?.getSymbol?.()?.valueDeclaration; } catch { vd = undefined; }
|
|
5367
|
+
return vd && ts.isClassExpression(vd) ? (classExprCtorQual.get(vd) ?? null) : null;
|
|
5368
|
+
}
|
|
5369
|
+
// Does this class expression's VALUE reach anything but a `new` this scan resolves? (see `mintClassExprCtor`)
|
|
5370
|
+
function classExprEscapes(cls) {
|
|
5371
|
+
let top = cls;
|
|
5372
|
+
while (top.parent && (ts.isParenthesizedExpression(top.parent) || ts.isAsExpression(top.parent)
|
|
5373
|
+
|| ts.isNonNullExpression(top.parent) || (ts.isSatisfiesExpression?.(top.parent) ?? false))) top = top.parent;
|
|
5374
|
+
const vd = top.parent;
|
|
5375
|
+
if (!vd || !ts.isVariableDeclaration(vd) || vd.initializer !== top || !ts.isIdentifier(vd.name)
|
|
5376
|
+
|| !(ts.getCombinedNodeFlags(vd) & ts.NodeFlags.Const)
|
|
5377
|
+
|| (ts.getCombinedModifierFlags(vd) & ts.ModifierFlags.Export)) return true;
|
|
5378
|
+
const syms = new Set();
|
|
5379
|
+
const outer = checker.getSymbolAtLocation(vd.name);
|
|
5380
|
+
if (!outer) return true;
|
|
5381
|
+
syms.add(outer);
|
|
5382
|
+
if (cls.name) { const inner = checker.getSymbolAtLocation(cls.name); if (inner) syms.add(inner); }
|
|
5383
|
+
const names = new Set([vd.name.text, cls.name?.text].filter(Boolean));
|
|
5384
|
+
let escapes = false;
|
|
5385
|
+
(function visit(n) {
|
|
5386
|
+
if (escapes) return;
|
|
5387
|
+
if (ts.isIdentifier(n) && names.has(n.text) && n !== vd.name && n !== cls.name) {
|
|
5388
|
+
let sym; try { sym = checker.getSymbolAtLocation(n); } catch { sym = undefined; }
|
|
5389
|
+
if (sym && (sym.flags & ts.SymbolFlags.Alias)) { try { sym = checker.getAliasedSymbol(sym); } catch { /* keep */ } }
|
|
5390
|
+
const p0 = n.parent;
|
|
5391
|
+
if (p0 && ts.isShorthandPropertyAssignment(p0) && p0.name === n) { escapes = true; return; }
|
|
5392
|
+
if (sym && syms.has(sym)) {
|
|
5393
|
+
let cur = n;
|
|
5394
|
+
while (cur.parent && (ts.isParenthesizedExpression(cur.parent) || ts.isNonNullExpression(cur.parent)
|
|
5395
|
+
|| ts.isAsExpression(cur.parent) || (ts.isSatisfiesExpression?.(cur.parent) ?? false))) cur = cur.parent;
|
|
5396
|
+
const p = cur.parent;
|
|
5397
|
+
const safe = !!p && ((ts.isNewExpression(p) && p.expression === cur)
|
|
5398
|
+
|| ts.isTypeQueryNode(p)
|
|
5399
|
+
|| (ts.isBinaryExpression(p) && p.right === cur && p.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword));
|
|
5400
|
+
if (!safe) { escapes = true; return; }
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
ts.forEachChild(n, visit);
|
|
5404
|
+
})(vd.getSourceFile());
|
|
5405
|
+
return escapes;
|
|
5406
|
+
}
|
|
5407
|
+
// ⟨SOUNDNESS R956⟩ A class EXPRESSION's member has no unit (`localName` names class DECLARATION members only), so
|
|
5408
|
+
// its body folded into whatever evaluates the expression and no dispatch could land on it. An override needs an
|
|
5409
|
+
// addressable body: mint it the way a structural implementor's member is minted — keyed by `stableUnitTag`, with
|
|
5410
|
+
// ⟨R519⟩'s containment edge so the evaluating unit keeps every charge it had (an ADDITION, never a move). Only a
|
|
5411
|
+
// member that OVERRIDES is minted; a class expression's other members stay where they were.
|
|
5412
|
+
function mintClassExprOverride(cls, m, name) {
|
|
5413
|
+
if (!ts.isClassExpression(cls) || nodeName.has(m)) return;
|
|
5414
|
+
const bodied = (ts.isMethodDeclaration(m) && m.body)
|
|
5415
|
+
|| (ts.isPropertyDeclaration(m) && m.initializer && (ts.isArrowFunction(m.initializer) || ts.isFunctionExpression(m.initializer)));
|
|
5416
|
+
if (!bodied) return; // an unminted override reads unresolved at every reader — disclosed, never silent
|
|
5417
|
+
const sf = m.getSourceFile();
|
|
5418
|
+
mintPositionalStructuralUnit(moduleOf(sf), sf, m, name);
|
|
5419
|
+
if (process.env.CANDOR_R873_REACH) console.error(`R873-REACH classexpr ${name}`);
|
|
5420
|
+
}
|
|
5421
|
+
// ⟨SOUNDNESS R873⟩ `this.m = <value>` inside an INSTANCE body of class `C` (its constructor, a method, an accessor,
|
|
5422
|
+
// an arrow property — not a nested `function`, which rebinds `this`) replaces `m` on that instance. Where `C` or a
|
|
5423
|
+
// local ancestor declares METHOD `m`, the assignment is an override of it: a `Base`-typed `b.m()` on a `C` runs the
|
|
5424
|
+
// assigned value. MEASURED: `class SubC extends BaseC { constructor(){ super(); this.m = () => write } }` with a
|
|
5425
|
+
// `BaseC`-typed `b.m()` was ABSENT at every gate (EXECUTED), the arrow charged to `SubC.constructor` only.
|
|
5426
|
+
// · a function literal is minted (`mintCallTargetUnit`, containment edge kept: the assigning body still carries it)
|
|
5427
|
+
// and registered — a resolution;
|
|
5428
|
+
// · `this.m = this.m.bind(…)` / `this.m = this.m` re-installs the same method and registers nothing;
|
|
5429
|
+
// · any other value (a wrapper's result, another method's `.bind`, a parameter) is registered UNRESOLVED, so every
|
|
5430
|
+
// reader's `allResolved` gate discloses `dispatch:` rather than certifying the declared body alone.
|
|
5431
|
+
// A PROPERTY `m` is not this rule's: a written property slot is ⟨R103⟩'s, which already discloses.
|
|
5432
|
+
function registerAssignedOverrides(cls) {
|
|
5433
|
+
const memberNamed = (c, name) => (c.members ?? []).find((x) => ts.isMethodDeclaration(x) && x.name?.getText?.() === name);
|
|
5434
|
+
for (const mem of cls.members ?? []) {
|
|
5435
|
+
if (ts.getCombinedModifierFlags(mem) & ts.ModifierFlags.Static) continue;
|
|
5436
|
+
const body = (ts.isConstructorDeclaration(mem) || ts.isMethodDeclaration(mem) || ts.isGetAccessorDeclaration(mem)
|
|
5437
|
+
|| ts.isSetAccessorDeclaration(mem)) ? mem.body
|
|
5438
|
+
: (ts.isPropertyDeclaration(mem) && mem.initializer && ts.isArrowFunction(mem.initializer)) ? mem.initializer.body : null;
|
|
5439
|
+
if (!body) continue;
|
|
5440
|
+
(function walk(n) {
|
|
5441
|
+
if (n !== body && (ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isClassLike(n))) return;
|
|
5442
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
5443
|
+
&& ts.isPropertyAccessExpression(n.left) && n.left.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
|
5444
|
+
const name = n.left.name.getText();
|
|
5445
|
+
let anc = memberNamed(cls, name);
|
|
5446
|
+
for (let b = localBaseClassOf(cls), g = 0; !anc && b && g++ < 64; b = localBaseClassOf(b)) anc = memberNamed(b, name);
|
|
5447
|
+
if (anc) {
|
|
5448
|
+
const rhs = unwrapArgExpr(n.right);
|
|
5449
|
+
const bound = rhs && ts.isCallExpression(rhs) ? unwrapBind(rhs) : null;
|
|
5450
|
+
const ref = bound ? bound.ref : rhs;
|
|
5451
|
+
const same = ref && ts.isPropertyAccessExpression(ref) && ref.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
5452
|
+
&& ref.name.getText() === name;
|
|
5453
|
+
if (!same) {
|
|
5454
|
+
const node = rhs && (ts.isArrowFunction(rhs) || ts.isFunctionExpression(rhs)) ? rhs : n;
|
|
5455
|
+
if (node === rhs && !callTargetUnit(rhs)) mintCallTargetUnit(rhs);
|
|
5456
|
+
if (!classOverrides.has(anc)) classOverrides.set(anc, []);
|
|
5457
|
+
const arr = classOverrides.get(anc);
|
|
5458
|
+
if (!arr.includes(node)) { arr.push(node); assignedOverrideClass.set(node, cls); }
|
|
5459
|
+
if (process.env.CANDOR_R873_REACH) console.error(`R873-REACH assigned ${node === rhs ? "fn" : "opaque"} ${name}`);
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
5462
|
+
}
|
|
5463
|
+
ts.forEachChild(n, walk);
|
|
5464
|
+
})(body);
|
|
5465
|
+
}
|
|
5466
|
+
}
|
|
5467
|
+
|
|
5222
5468
|
// callback-flow bookkeeping (the Rust engine's callback_named move, ported): for every call that
|
|
5223
5469
|
// edges to a LOCAL unit, record what each argument position received — a NAMED local unit (a
|
|
5224
5470
|
// resolvable callback target), or an opaque value (an inline closure stays attributed to the
|
|
@@ -5233,7 +5479,8 @@ const callbackArgs = new Map(); // calleeName -> Map(argIndex -> Array<{calle
|
|
|
5233
5479
|
// What each argument position of a call to LOCAL unit `targetName` received (callback flow, see `callbackArgs`).
|
|
5234
5480
|
// Shared by the ordinary local arm and R955's callee-choice arm, so a callback passed through either is seen.
|
|
5235
5481
|
function registerCallbackArgs(rec, targetName, args) {
|
|
5236
|
-
args.forEach((
|
|
5482
|
+
args.forEach((a0, i) => {
|
|
5483
|
+
const a = unwrapArgExpr(a0); // R780: `run((f))` / `run(f as F)` pass the same function as `run(f)`
|
|
5237
5484
|
const slot = (callbackArgs.get(targetName) ?? callbackArgs.set(targetName, new Map()).get(targetName));
|
|
5238
5485
|
const list = slot.get(i) ?? [];
|
|
5239
5486
|
let target = null, opaque = false;
|
|
@@ -5372,8 +5619,7 @@ if (R103_HITS) process.on("exit", () => {
|
|
|
5372
5619
|
});
|
|
5373
5620
|
function openCallSlot(node) {
|
|
5374
5621
|
if (!ts.isCallExpression(node)) return null; // `new X()` constructs a class, not a slot
|
|
5375
|
-
|
|
5376
|
-
while (ts.isParenthesizedExpression(callee)) callee = callee.expression;
|
|
5622
|
+
const callee = unwrapArgExpr(node.expression); // R780
|
|
5377
5623
|
// ELEMENT ACCESS IS THE SAME SLOT BY ANOTHER SPELLING, and it is here because the first draft of this
|
|
5378
5624
|
// comment asserted the opposite — "an element access already reaches the dynamic-key/`callback:` arms"
|
|
5379
5625
|
// — and that was FALSE when measured: `this["handler"]()` and `this[k]()` (k narrowed to the literal
|
|
@@ -5439,8 +5685,8 @@ function unwrapBind(node, depth = 0) {
|
|
|
5439
5685
|
if (!node || depth > 8) return null;
|
|
5440
5686
|
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return null;
|
|
5441
5687
|
if (node.expression.name.text !== "bind") return null;
|
|
5442
|
-
|
|
5443
|
-
|
|
5688
|
+
// R780: `(f as F).bind(t)` / `(f satisfies F).bind(t)` bind the same function — the ONE wrapper set, not parens only.
|
|
5689
|
+
const recv = unwrapArgExpr(node.expression.expression);
|
|
5444
5690
|
// chained `.bind().bind()` — recurse only when the receiver is ITSELF a `.bind` call, else it's an
|
|
5445
5691
|
// arbitrary call (`getCallback().bind`) whose result we can't pin → unresolvable bind.
|
|
5446
5692
|
if (ts.isCallExpression(recv)) {
|
|
@@ -6012,12 +6258,13 @@ function recordConformer(c, t) {
|
|
|
6012
6258
|
}
|
|
6013
6259
|
if (ts.isClassDeclaration(t)) {
|
|
6014
6260
|
if (c.kind === "class" && classInSubtree(c.node, t)) return false; // nominal: the subtree already answers
|
|
6015
|
-
// R958
|
|
6016
|
-
//
|
|
6017
|
-
//
|
|
6018
|
-
// `
|
|
6019
|
-
//
|
|
6020
|
-
|
|
6261
|
+
// ⟨SOUNDNESS R958, the DOWNCAST half⟩ An asserted downcast (`base as Sub`) IS a new conformer when the value is a
|
|
6262
|
+
// `Base`, and nothing here can tell that apart from a truthful one. A refusal stood here, scoped to assertions,
|
|
6263
|
+
// on the claim that "the receiver's class arm answers `Sub`'s subtree" — but the value's class is `Base`, which
|
|
6264
|
+
// is NOT in `Sub`'s subtree, so `Base.m` was the body that ran and the one no arm named. EXECUTED: `dO(bO as
|
|
6265
|
+
// SubO)` wrote a file through `BaseO.m` with `dO`/`rO` ABSENT and `pure a.dO` exit 0, while `dP(bP)` — the same
|
|
6266
|
+
// value without the `as` — was `Fs`, exit 1. An assertion must not delete a charge the unasserted conversion
|
|
6267
|
+
// makes. On a TRUTHFUL downcast the registration over-charges a `Sub`-typed receiver with `Base`'s override.
|
|
6021
6268
|
let added = false;
|
|
6022
6269
|
for (const anc of localAncestorsAndSelf(t)) added = pushUnique(classConformers, anc, c.node) || added;
|
|
6023
6270
|
if (c.kind === "lit") mintStructuralMembers(c.node, true);
|
|
@@ -6090,12 +6337,7 @@ function isConversionPosition(n) {
|
|
|
6090
6337
|
return false;
|
|
6091
6338
|
}
|
|
6092
6339
|
// R958 — the value under an assertion chain: parentheses, `as`, `<T>`, `!` and `satisfies` change no runtime value.
|
|
6093
|
-
|
|
6094
|
-
function peelAssertions(e) {
|
|
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
|
-
}
|
|
6340
|
+
const peelAssertions = (e) => unwrapArgExpr(e); // R780: one wrapper set, not a third copy of it
|
|
6099
6341
|
// R82 / PART 87: is the conversion's SOURCE type a fact about the VALUE, or only about a generic signature? A
|
|
6100
6342
|
// call whose declared return type mentions one of its own type parameters (`wrap<T>(x: T): T`) hands back
|
|
6101
6343
|
// whatever type the ARGUMENT had, and that signature proves assignability, never that the value IS the
|
|
@@ -6129,20 +6371,182 @@ function conversionSourceTracked(e, depth = 0) {
|
|
|
6129
6371
|
}
|
|
6130
6372
|
return true;
|
|
6131
6373
|
}
|
|
6374
|
+
// ---- PARAMETER PROVENANCE: what every VISIBLE call site binds to a parameter (SOUNDNESS R934, R246) ----
|
|
6375
|
+
// A parameter's value is whatever its callers pass. For a function whose every reference is a CALL — not
|
|
6376
|
+
// handed out as a value, not `.call`/`.apply`/`.bind`-ed — the visible call sites ARE its bindings, and the
|
|
6377
|
+
// arguments there say what the parameter can hold. Two consumers ask this ONE index rather than each
|
|
6378
|
+
// walking calls: the environment-location model (a parameter every site hands `process.env`, R934) and
|
|
6379
|
+
// the `any`-receiver accessor arm (an `any`-typed parameter whose every site passes a typed value, R246).
|
|
6380
|
+
// `paramArgs` maps a parameter key (its symbol; a destructured parameter's NODE) to the argument expression
|
|
6381
|
+
// at each visible site (`null` where a spread hides the position; the default, or `null` for `undefined`,
|
|
6382
|
+
// where the site omits it). `paramFnEscaped(fnSym)` answers whether the function escapes, batched.
|
|
6383
|
+
const paramArgs = new Map();
|
|
6384
|
+
const paramOwnerOf = new Map(); // param key -> function symbol (for the escape question)
|
|
6385
|
+
const paramDeclOf = new Map(); // param key -> its ParameterDeclaration
|
|
6386
|
+
let paramFnEscaped;
|
|
6387
|
+
// A class some project class `extends` — its members are excluded (an override is invisible here).
|
|
6388
|
+
const extendedClassSyms = new Set();
|
|
6389
|
+
const classSymOf = (cls) => (cls?.name ? checker.getSymbolAtLocation(cls.name) : cls?.symbol) ?? null;
|
|
6390
|
+
{
|
|
6391
|
+
const noteArg = (k, v) => { if (k) (paramArgs.get(k) ?? paramArgs.set(k, []).get(k)).push(v); };
|
|
6392
|
+
const collectHeritage = (node) => {
|
|
6393
|
+
if ((ts.isClassDeclaration(node) || ts.isClassExpression(node)) && node.heritageClauses) {
|
|
6394
|
+
for (const h of node.heritageClauses) if (h.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
6395
|
+
for (const t of h.types) {
|
|
6396
|
+
let s = checker.getSymbolAtLocation(t.expression);
|
|
6397
|
+
if (s && (s.flags & ts.SymbolFlags.Alias)) { try { s = checker.getAliasedSymbol(s); } catch { /* keep */ } }
|
|
6398
|
+
if (s) extendedClassSyms.add(s);
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
}
|
|
6402
|
+
ts.forEachChild(node, collectHeritage);
|
|
6403
|
+
};
|
|
6404
|
+
for (const sf of sources) collectHeritage(sf);
|
|
6405
|
+
const calls = [];
|
|
6406
|
+
const collectCalls = (node) => {
|
|
6407
|
+
if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && (node.arguments?.length ?? 0) > 0) calls.push(node);
|
|
6408
|
+
ts.forEachChild(node, collectCalls);
|
|
6409
|
+
};
|
|
6410
|
+
for (const sf of sources) collectCalls(sf);
|
|
6411
|
+
// ⟨R934⟩ the functions whose parameters are eligible locations: a function declaration, a function or
|
|
6412
|
+
// arrow bound to a `const`, a constructor, or a method of a class with no heritage that nothing extends.
|
|
6413
|
+
// Every reference to it must be a CALL (or an export, a `typeof`, a member read that is not
|
|
6414
|
+
// `.call`/`.apply`/`.bind`) — anything else hands it to code that binds its parameters unseen.
|
|
6415
|
+
const fnEligible = new Map(); // declaration -> boolean
|
|
6416
|
+
const fnSymOf = (decl) => {
|
|
6417
|
+
if (ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl)) return decl.name ? checker.getSymbolAtLocation(decl.name) : null;
|
|
6418
|
+
if ((ts.isArrowFunction(decl) || ts.isFunctionExpression(decl)) && ts.isVariableDeclaration(decl.parent)
|
|
6419
|
+
&& decl.parent.initializer === decl && ts.isIdentifier(decl.parent.name)
|
|
6420
|
+
&& (ts.getCombinedNodeFlags(decl.parent) & ts.NodeFlags.Const)) return checker.getSymbolAtLocation(decl.parent.name);
|
|
6421
|
+
if (ts.isConstructorDeclaration(decl)) return classSymOf(decl.parent);
|
|
6422
|
+
return null;
|
|
6423
|
+
};
|
|
6424
|
+
const declShapeOk = (decl) => {
|
|
6425
|
+
if (!decl?.body || !projectFiles.has(path.resolve(decl.getSourceFile().fileName))) return false;
|
|
6426
|
+
if (ts.isFunctionDeclaration(decl) || ts.isArrowFunction(decl) || ts.isFunctionExpression(decl)) return true;
|
|
6427
|
+
if (ts.isConstructorDeclaration(decl)) return true;
|
|
6428
|
+
if (ts.isMethodDeclaration(decl) && (ts.isClassDeclaration(decl.parent) || ts.isClassExpression(decl.parent)))
|
|
6429
|
+
return !decl.parent.heritageClauses?.length && !extendedClassSyms.has(classSymOf(decl.parent));
|
|
6430
|
+
return false;
|
|
6431
|
+
};
|
|
6432
|
+
const escapedFnSyms = new Set();
|
|
6433
|
+
const escapeScannedSyms = new Set();
|
|
6434
|
+
// One traversal per BATCH of function symbols not yet asked about; the answer is cached.
|
|
6435
|
+
const scanEscapes = (cands0) => {
|
|
6436
|
+
const cands = new Set([...cands0].filter((x) => x && !escapeScannedSyms.has(x)));
|
|
6437
|
+
if (!cands.size) return;
|
|
6438
|
+
for (const c of cands) escapeScannedSyms.add(c);
|
|
6439
|
+
const names = new Set([...cands].map((s) => s.name));
|
|
6440
|
+
const SAFE_MEMBER_BLOCK = new Set(["call", "apply", "bind"]);
|
|
6441
|
+
const visit = (node) => {
|
|
6442
|
+
if (ts.isIdentifier(node) && names.has(node.text)) {
|
|
6443
|
+
let s = checker.getSymbolAtLocation(node);
|
|
6444
|
+
if (s && (s.flags & ts.SymbolFlags.Alias)) { try { s = checker.getAliasedSymbol(s); } catch { /* keep */ } }
|
|
6445
|
+
if (s && cands.has(s) && !escapedFnSyms.has(s)) {
|
|
6446
|
+
// climb the transparent wrappers to the position the value lands in
|
|
6447
|
+
let cur = node;
|
|
6448
|
+
const p0 = node.parent;
|
|
6449
|
+
let safe = false;
|
|
6450
|
+
if (p0 && (ts.isPropertyAccessExpression(p0) || ts.isPropertyAssignment(p0) || ts.isMethodDeclaration(p0)
|
|
6451
|
+
|| ts.isFunctionDeclaration(p0) || ts.isClassDeclaration(p0) || ts.isClassExpression(p0)
|
|
6452
|
+
|| ts.isVariableDeclaration(p0) || ts.isPropertyDeclaration(p0)) && p0.name === node) {
|
|
6453
|
+
// a declaration name, or `o.name` — a METHOD reference: safe only as the callee of a call
|
|
6454
|
+
if (ts.isPropertyAccessExpression(p0)) {
|
|
6455
|
+
const pp = p0.parent;
|
|
6456
|
+
safe = !!pp && (ts.isCallExpression(pp) || ts.isNewExpression(pp)) && pp.expression === p0;
|
|
6457
|
+
} else safe = true;
|
|
6458
|
+
} else {
|
|
6459
|
+
while (cur.parent && (ts.isParenthesizedExpression(cur.parent) || ts.isNonNullExpression(cur.parent))) cur = cur.parent;
|
|
6460
|
+
const p = cur.parent;
|
|
6461
|
+
if (!p) safe = true;
|
|
6462
|
+
else if ((ts.isCallExpression(p) || ts.isNewExpression(p)) && p.expression === cur) safe = true;
|
|
6463
|
+
else if (ts.isPropertyAccessExpression(p) && p.expression === cur) safe = !SAFE_MEMBER_BLOCK.has(p.name.text);
|
|
6464
|
+
else if (ts.isExportSpecifier(p) || ts.isExportAssignment(p) || ts.isTypeQueryNode(p)
|
|
6465
|
+
|| ts.isTypeOfExpression(p) || ts.isExpressionWithTypeArguments(p) || ts.isImportSpecifier(p)
|
|
6466
|
+
|| ts.isImportClause(p) || ts.isNamespaceImport(p) || ts.isTypeReferenceNode(p)
|
|
6467
|
+
|| ts.isQualifiedName(p)) safe = true;
|
|
6468
|
+
else if (ts.isBinaryExpression(p) && p.right === cur && p.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) safe = true;
|
|
6469
|
+
}
|
|
6470
|
+
if (!safe) escapedFnSyms.add(s);
|
|
6471
|
+
}
|
|
6472
|
+
}
|
|
6473
|
+
ts.forEachChild(node, visit);
|
|
6474
|
+
};
|
|
6475
|
+
for (const sf of sources) visit(sf);
|
|
6476
|
+
};
|
|
6477
|
+
// Seed the parameter bindings: for every call whose resolved declaration is an eligible shape, bind each
|
|
6478
|
+
// identifier parameter to its argument (or default / undefined). Only calls that hand over at least one
|
|
6479
|
+
// argument are recorded here — a site that passes nothing binds `undefined` to every parameter, which
|
|
6480
|
+
// is recorded by the per-declaration site count below.
|
|
6481
|
+
const sitesByDecl = new Map(); // decl -> number of call sites seen
|
|
6482
|
+
for (const call of calls) {
|
|
6483
|
+
let decl;
|
|
6484
|
+
try { decl = checker.getResolvedSignature(call)?.declaration; } catch { decl = null; }
|
|
6485
|
+
if (!decl || !declShapeOk(decl)) continue;
|
|
6486
|
+
if (!fnEligible.has(decl)) fnEligible.set(decl, !!fnSymOf(decl));
|
|
6487
|
+
if (!fnEligible.get(decl)) continue;
|
|
6488
|
+
sitesByDecl.set(decl, (sitesByDecl.get(decl) ?? 0) + 1);
|
|
6489
|
+
const fsym = fnSymOf(decl);
|
|
6490
|
+
let pos = 0, sawSpread = false;
|
|
6491
|
+
const args = call.arguments ?? [];
|
|
6492
|
+
const argAt = [];
|
|
6493
|
+
for (const a of args) { if (ts.isSpreadElement(a)) { sawSpread = true; break; } argAt[pos++] = a; }
|
|
6494
|
+
decl.parameters.forEach((p, i) => {
|
|
6495
|
+
if (p.dotDotDotToken) return;
|
|
6496
|
+
// a DESTRUCTURED parameter reads its keys when the frame is entered — keyed by the parameter node
|
|
6497
|
+
const ps = ts.isIdentifier(p.name) ? checker.getSymbolAtLocation(p.name)
|
|
6498
|
+
: ts.isObjectBindingPattern(p.name) ? p : null;
|
|
6499
|
+
if (!ps) return;
|
|
6500
|
+
paramOwnerOf.set(ps, fsym);
|
|
6501
|
+
if (i < argAt.length && argAt[i]) noteArg(ps, argAt[i]);
|
|
6502
|
+
else if (sawSpread) noteArg(ps, null);
|
|
6503
|
+
else noteArg(ps, p.initializer ?? null);
|
|
6504
|
+
});
|
|
6505
|
+
}
|
|
6506
|
+
// A call site that resolves to the declaration with NO arguments was not collected above; it binds
|
|
6507
|
+
// every parameter to its default or `undefined`. Count them so a zero-argument site is not lost.
|
|
6508
|
+
{
|
|
6509
|
+
const visitZero = (node) => {
|
|
6510
|
+
if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && (node.arguments?.length ?? 0) === 0) {
|
|
6511
|
+
let decl;
|
|
6512
|
+
try { decl = checker.getResolvedSignature(node)?.declaration; } catch { decl = null; }
|
|
6513
|
+
if (decl && sitesByDecl.has(decl)) for (const p of decl.parameters)
|
|
6514
|
+
if (!p.dotDotDotToken && ts.isIdentifier(p.name)) noteArg(checker.getSymbolAtLocation(p.name), p.initializer ?? null);
|
|
6515
|
+
else if (!p.dotDotDotToken && ts.isObjectBindingPattern(p.name)) noteArg(p, p.initializer ?? null);
|
|
6516
|
+
}
|
|
6517
|
+
ts.forEachChild(node, visitZero);
|
|
6518
|
+
};
|
|
6519
|
+
if (sitesByDecl.size) for (const sf of sources) visitZero(sf);
|
|
6520
|
+
}
|
|
6521
|
+
for (const k of paramArgs.keys()) { const d = ts.isParameter(k) ? k : k.valueDeclaration; if (d && ts.isParameter(d)) paramDeclOf.set(k, d); }
|
|
6522
|
+
paramFnEscaped = (fsym) => { if (!fsym) return true; scanEscapes([fsym]); return escapedFnSyms.has(fsym); };
|
|
6523
|
+
// Batch form: answer for many at once with ONE traversal.
|
|
6524
|
+
paramFnEscaped.batch = (fsyms) => scanEscapes(fsyms);
|
|
6525
|
+
}
|
|
6526
|
+
|
|
6132
6527
|
const CONFORMER_REACH = process.env.CANDOR_CONFORMER_REACH ? (k, n) => console.error(`CONFORMER-REACH ${k} ${n}`) : null;
|
|
6133
6528
|
for (const sf of sources) {
|
|
6134
6529
|
const convert = (src, tt, at, kind) => {
|
|
6135
6530
|
if (!tt || !conversionSourceTracked(src)) return;
|
|
6136
6531
|
let st; try { st = checker.getTypeAtLocation(src); } catch { st = undefined; }
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6532
|
+
// ⟨SOUNDNESS R958, the `any`-PARAMETER half⟩ a source typed `any`/`unknown` names no conformer, so a value
|
|
6533
|
+
// laundered through an `any` parameter reached a closed dispatch silently: `launder(x: any) { qDisp(x) }`
|
|
6534
|
+
// called as `launder(new LocalW())` ran LocalW's `m` (EXECUTED) with `deny Fs` exit 0 on the caller. The
|
|
6535
|
+
// parameter's visible arguments say what it holds — the SAME provenance index R246's accessor arm asks
|
|
6536
|
+
// (`receiverValueTypes`) — so each argument type is recorded as a conversion source. Upcasts only, as
|
|
6537
|
+
// ever (`recordConversion` keeps a target the type is assignable to). ⟨R958 widened-local half⟩ and the
|
|
6538
|
+
// same for a source typed `object`/`{}`, and a `const` declared wider than its initializer
|
|
6539
|
+
// (`uninformativeType`). A dependency-sourced value is R958's disclosed residue (`invisible`).
|
|
6540
|
+
if (uninformativeType(st)) for (const vt of receiverValueTypes(src)) {
|
|
6541
|
+
try { recordConversion(vt, tt); } catch { /* best effort, as the main record below */ }
|
|
6542
|
+
CONFORMER_REACH?.("any-param", `${path.relative(rootDir, sf.fileName)}:${at.getStart()}`);
|
|
6543
|
+
}
|
|
6544
|
+
if (!CONFORMER_REACH) { recordConversion(st, tt); return; }
|
|
6545
|
+
const count = () => [...interfaceImpls.values(), ...foreignInterfaceImpls.values(), ...classConformers.values(),
|
|
6546
|
+
...depConformers.values()].reduce((a, x) => a + x.length, 0);
|
|
6547
|
+
const before = count();
|
|
6548
|
+
recordConversion(st, tt);
|
|
6549
|
+
if (count() > before) CONFORMER_REACH(kind, `${path.relative(rootDir, sf.fileName)}:${at.getStart()}`);
|
|
6146
6550
|
};
|
|
6147
6551
|
(function walkConversions(node) {
|
|
6148
6552
|
if (ts.isExpression(node) && !ts.isSpreadElement(node) && isConversionPosition(node)) {
|
|
@@ -6162,9 +6566,53 @@ for (const sf of sources) {
|
|
|
6162
6566
|
let tt; try { tt = checker.getTypeFromTypeNode(node.type); } catch { tt = undefined; }
|
|
6163
6567
|
convert(peelAssertions(node.expression), tt, node, "assertion");
|
|
6164
6568
|
}
|
|
6569
|
+
if (ts.isObjectLiteralExpression(node)) providerSubstitution(node); // ⟨R1061⟩
|
|
6165
6570
|
ts.forEachChild(node, walkConversions);
|
|
6166
6571
|
})(sf);
|
|
6167
6572
|
}
|
|
6573
|
+
// ⟨SOUNDNESS R1061⟩ A DEPENDENCY-INJECTION PROVIDER IS A CONVERSION THE CHECKER NEVER SEES. NestJS (and Angular,
|
|
6574
|
+
// which shares the shape) wires `{ provide: Store, useClass: FileStore }` — or `useValue: <obj>`, `useFactory: () =>
|
|
6575
|
+
// <obj>`, `useExisting: Other` — and the container then hands the substitute to every `store: Store` parameter. No
|
|
6576
|
+
// expression converts the substitute to `Store`, so a `this.store.save()` resolved to `Store.save` alone: EXECUTED on
|
|
6577
|
+
// NestJS 10, `FileStore.save` wrote a file while the calling method was ABSENT and `deny Fs`/`deny Fs Unknown` exited 0
|
|
6578
|
+
// for all three provider forms. The provider entry IS the conversion, so the substitute is registered as a conformer
|
|
6579
|
+
// of the token class — the union of the declared class and the visible substitute, through the R874/R954 conformer
|
|
6580
|
+
// arm every class dispatch already asks. Registered directly, without the checker's assignability test, because the
|
|
6581
|
+
// container does not apply one (a token class with a private member is not structurally assignable from its
|
|
6582
|
+
// substitute, and is still what gets injected). Only a token that names a LOCAL class registers; a string or symbol
|
|
6583
|
+
// token types its parameter by an interface or `any`, which the interface arm already answers or discloses.
|
|
6584
|
+
function providerSubstitution(lit) {
|
|
6585
|
+
const prop = (name) => lit.properties.find((p) => ts.isPropertyAssignment(p) && p.name && !ts.isComputedPropertyName(p.name)
|
|
6586
|
+
&& (p.name.text ?? p.name.getText()) === name);
|
|
6587
|
+
const provide = prop("provide");
|
|
6588
|
+
if (!provide) return;
|
|
6589
|
+
let tsym; try { tsym = checker.getSymbolAtLocation(peelAssertions(provide.initializer)); } catch { tsym = undefined; }
|
|
6590
|
+
if (tsym && (tsym.flags & ts.SymbolFlags.Alias)) { try { tsym = checker.getAliasedSymbol(tsym); } catch { return; } }
|
|
6591
|
+
const target = (tsym?.declarations ?? []).find((d) => ts.isClassDeclaration(d) && isProjectNode(d));
|
|
6592
|
+
if (!target) return;
|
|
6593
|
+
const instanceOf = (e) => {
|
|
6594
|
+
let s; try { s = checker.getSymbolAtLocation(peelAssertions(e)); } catch { s = undefined; }
|
|
6595
|
+
if (s && (s.flags & ts.SymbolFlags.Alias)) { try { s = checker.getAliasedSymbol(s); } catch { s = undefined; } }
|
|
6596
|
+
if (!s || !(s.flags & ts.SymbolFlags.Class)) return null;
|
|
6597
|
+
try { return checker.getDeclaredTypeOfSymbol(s); } catch { return null; }
|
|
6598
|
+
};
|
|
6599
|
+
const sources = [];
|
|
6600
|
+
for (const key of ["useClass", "useExisting"]) { const p = prop(key); const t = p && instanceOf(p.initializer); if (t) sources.push(t); }
|
|
6601
|
+
const uv = prop("useValue");
|
|
6602
|
+
if (uv) { try { sources.push(checker.getTypeAtLocation(peelAssertions(uv.initializer))); } catch { /* none */ } }
|
|
6603
|
+
const uf = prop("useFactory");
|
|
6604
|
+
if (uf) {
|
|
6605
|
+
let ft; try { ft = checker.getTypeAtLocation(uf.initializer); } catch { ft = undefined; }
|
|
6606
|
+
for (const sig of ft?.getCallSignatures?.() ?? []) {
|
|
6607
|
+
let rt; try { rt = sig.getReturnType(); rt = checker.getAwaitedType?.(rt) ?? rt; } catch { rt = undefined; }
|
|
6608
|
+
if (rt) sources.push(rt);
|
|
6609
|
+
}
|
|
6610
|
+
}
|
|
6611
|
+
for (const st of sources) for (const s of st?.isUnion?.() ? st.types : [st]) {
|
|
6612
|
+
const c = conformerOfSourceType(s);
|
|
6613
|
+
if (c && recordConformer(c, target)) CONFORMER_REACH?.("di-provider", `${path.relative(rootDir, lit.getSourceFile().fileName)}:${lit.getStart()}`);
|
|
6614
|
+
}
|
|
6615
|
+
}
|
|
6168
6616
|
// The interface-flow fixpoint: a `J`-typed value converted to `X` carries every conformer of `J` with it.
|
|
6169
6617
|
for (let changed = true, g = 0; changed && g++ < 32; ) {
|
|
6170
6618
|
changed = false;
|
|
@@ -6308,6 +6756,68 @@ function keyLiteralNames(t, depth = 0, seen = new Set()) {
|
|
|
6308
6756
|
if (c && c !== t) return keyLiteralNames(c, depth + 1, seen);
|
|
6309
6757
|
return null; // `string`, `symbol`, `keyof T` for generic T, …
|
|
6310
6758
|
}
|
|
6759
|
+
// SOUNDNESS R246 — THE RECEIVER'S VALUE, WHEN ITS STATIC TYPE SAYS NOTHING. Accessor lookup asks the
|
|
6760
|
+
// receiver's TYPE which properties it declares, and `any` declares none: `(s as any)[k] = v`,
|
|
6761
|
+
// `(s as any).token = v` and `function w(o: any) { o.token = v }` called as `w(new S())` all invoked S's
|
|
6762
|
+
// setter (EXECUTED, 6 of 6 writes observed) and were ABSENT with `deny Fs <fn>` exit 0 — the caller too.
|
|
6763
|
+
// The value is not unknowable; only its static type was thrown away. Two places say what it really is,
|
|
6764
|
+
// and both are facts rather than guesses:
|
|
6765
|
+
// - an ASSERTION changes no runtime value (R958's look-through, the same `peelAssertions`): the operand
|
|
6766
|
+
// under `as any` / `as unknown as T` / `<any>x` has the type the value really carries;
|
|
6767
|
+
// - an `any`/`unknown` PARAMETER holds what its visible call sites pass (the shared PARAMETER PROVENANCE
|
|
6768
|
+
// index, R934) — only for a function whose every reference is a call; an escaped one is unanswered.
|
|
6769
|
+
// Returns the EXTRA types beyond the checker's own (empty when the checker's type is informative and no
|
|
6770
|
+
// assertion intervenes), so a typed receiver's lookup is byte-identical to before. This RESOLVES — it adds
|
|
6771
|
+
// the accessor edges the value's type declares — and never hedges; a receiver neither rule answers is
|
|
6772
|
+
// exactly as it was (the residual is stated in SOUNDNESS R246, not hidden here).
|
|
6773
|
+
function anyishType(t) { return !!t && !!(t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)); } // hoisted: the conversion walk asks it first
|
|
6774
|
+
// ⟨SOUNDNESS R958, the WIDENED-LOCAL half⟩ A type that names no member is as silent about the value as `any`: a local
|
|
6775
|
+
// declared `object` or `{}` holding `new LocalW()` and asserted into an interface (`qDisp(o as Sink)`) ran
|
|
6776
|
+
// `LocalW.m` (EXECUTED) with every unit `[]` and no disclosure — `anyishType` alone let it through. Asked by the
|
|
6777
|
+
// conversion walk and the R246 receiver question alike; it only ever lets MORE value types be recorded.
|
|
6778
|
+
function uninformativeType(t) {
|
|
6779
|
+
if (!t) return false;
|
|
6780
|
+
if (anyishType(t) || (t.flags & ts.TypeFlags.NonPrimitive)) return true;
|
|
6781
|
+
if (!(t.flags & ts.TypeFlags.Object) || t.isUnion?.() || t.isIntersection?.()) return false;
|
|
6782
|
+
try {
|
|
6783
|
+
return checker.getPropertiesOfType(t).length === 0 && t.getCallSignatures().length === 0
|
|
6784
|
+
&& t.getConstructSignatures().length === 0 && !checker.getIndexInfosOfType?.(t)?.length;
|
|
6785
|
+
} catch { return false; }
|
|
6786
|
+
}
|
|
6787
|
+
function receiverValueTypes(expr, depth = 0, seen = new Set()) {
|
|
6788
|
+
if (!expr || depth > 6) return [];
|
|
6789
|
+
let t; try { t = checker.getTypeAtLocation(expr); } catch { t = null; }
|
|
6790
|
+
const inner = peelAssertions(expr);
|
|
6791
|
+
if (inner && inner !== expr) {
|
|
6792
|
+
const below = receiverValueTypes(inner, depth + 1, seen);
|
|
6793
|
+
let ti; try { ti = checker.getTypeAtLocation(inner); } catch { ti = null; }
|
|
6794
|
+
return [...(ti && !anyishType(ti) ? [ti] : []), ...below];
|
|
6795
|
+
}
|
|
6796
|
+
if (!uninformativeType(t) || !ts.isIdentifier(expr)) return [];
|
|
6797
|
+
const sym = checker.getSymbolAtLocation(expr);
|
|
6798
|
+
const d = sym?.valueDeclaration;
|
|
6799
|
+
if (!sym || !d || seen.has(sym)) return [];
|
|
6800
|
+
// ⟨R958, widened-local half⟩ a `const` declared wider than its initializer holds the initializer's value.
|
|
6801
|
+
if (ts.isVariableDeclaration(d) && ts.isIdentifier(d.name) && d.initializer
|
|
6802
|
+
&& (ts.getCombinedNodeFlags(d) & ts.NodeFlags.Const)) {
|
|
6803
|
+
seen.add(sym);
|
|
6804
|
+
let ti; try { ti = checker.getTypeAtLocation(d.initializer); } catch { ti = null; }
|
|
6805
|
+
return [...(ti && !anyishType(ti) ? [ti] : []), ...receiverValueTypes(d.initializer, depth + 1, seen)];
|
|
6806
|
+
}
|
|
6807
|
+
if (!ts.isParameter(d)) return [];
|
|
6808
|
+
seen.add(sym);
|
|
6809
|
+
const args = paramArgs.get(sym);
|
|
6810
|
+
if (!args || paramFnEscaped(paramOwnerOf.get(sym))) return [];
|
|
6811
|
+
const out = [];
|
|
6812
|
+
for (const a of args) {
|
|
6813
|
+
if (!a) continue;
|
|
6814
|
+
let ta; try { ta = checker.getTypeAtLocation(a); } catch { ta = null; }
|
|
6815
|
+
if (ta && !anyishType(ta)) out.push(ta); // a VALUE's own type, even `{}`, is a fact about it
|
|
6816
|
+
out.push(...receiverValueTypes(a, depth + 1, seen));
|
|
6817
|
+
}
|
|
6818
|
+
return out;
|
|
6819
|
+
}
|
|
6820
|
+
const dedupeHits = (hits) => { const seen = new Set(); return hits.filter((h) => !seen.has(h.decl) && seen.add(h.decl)); };
|
|
6311
6821
|
function accessorsAt(propNode, kind /* "get" | "set" */) {
|
|
6312
6822
|
if (ts.isElementAccessExpression(propNode)) {
|
|
6313
6823
|
// `c["prop"]` carries no `.name`; resolve the key to the property NAMES it can hold and look each
|
|
@@ -6324,9 +6834,19 @@ function accessorsAt(propNode, kind /* "get" | "set" */) {
|
|
|
6324
6834
|
// property is common to all of them).
|
|
6325
6835
|
for (const t of texts) for (const sym of propertySymbolsAcrossArms(recvType, t))
|
|
6326
6836
|
out.push(...accessorsFromSym(sym, kind));
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6837
|
+
// R246 — and every type the receiver's VALUE can carry under an assertion or an `any` parameter.
|
|
6838
|
+
for (const vt of receiverValueTypes(propNode.expression))
|
|
6839
|
+
for (const t of texts) for (const sym of propertySymbolsAcrossArms(vt, t)) out.push(...accessorsFromSym(sym, kind));
|
|
6840
|
+
return dedupeHits(out);
|
|
6841
|
+
}
|
|
6842
|
+
const direct = accessorsFromSym(checker.getSymbolAtLocation(propNode.name ?? propNode), kind);
|
|
6843
|
+
if (direct.length || !ts.isPropertyAccessExpression(propNode)) return direct;
|
|
6844
|
+
// R246 — `(s as any).token = v` / `o.token = v` with `o: any`: the checker resolves no symbol at all.
|
|
6845
|
+
const out = [];
|
|
6846
|
+
const name = propNode.name.text;
|
|
6847
|
+
for (const vt of receiverValueTypes(propNode.expression))
|
|
6848
|
+
for (const sym of propertySymbolsAcrossArms(vt, name)) out.push(...accessorsFromSym(sym, kind));
|
|
6849
|
+
return dedupeHits(out);
|
|
6330
6850
|
}
|
|
6331
6851
|
// A `Object.defineProperty` descriptor accessor for the forcing site `recv.key` (read → get, assign →
|
|
6332
6852
|
// set), consulted ONLY when the type-level `accessorsAt` came up empty (the checker types target.key as
|
|
@@ -6406,8 +6926,7 @@ function accessorOverrideFanOut(rec, decl, recvExpr) {
|
|
|
6406
6926
|
// SOUNDNESS-PRESERVING FALLBACK, the method path's verbatim: a receiver we cannot pin to a LOCAL
|
|
6407
6927
|
// class (a union, an interface, `any`, an external type) keeps the FULL override set.
|
|
6408
6928
|
const rootClass = localReceiverClass(recvExpr);
|
|
6409
|
-
const direct = rootClass ? allOverrides.filter((om) =>
|
|
6410
|
-
ts.isClassDeclaration(om.parent) && classInSubtree(om.parent, rootClass)) : allOverrides;
|
|
6929
|
+
const direct = rootClass ? allOverrides.filter((om) => classInSubtree(overrideOwner(om), rootClass)) : allOverrides;
|
|
6411
6930
|
const overrides = memberDispatchBodies(decl, rootClass);
|
|
6412
6931
|
if (direct.length <= CHA_FANOUT_LIMIT) probeR871("accessor", decl, overrides.length - direct.length);
|
|
6413
6932
|
if (overrides.length === 0) return;
|
|
@@ -6658,8 +7177,10 @@ function classBodiedGetter(sym) {
|
|
|
6658
7177
|
}
|
|
6659
7178
|
function enumerateGetters(owner, type, srcExpr) {
|
|
6660
7179
|
if (!owner) return;
|
|
6661
|
-
|
|
6662
|
-
|
|
7180
|
+
// R246 — the copied VALUE's types too, when its static type is `any`/`unknown` or asserted away.
|
|
7181
|
+
const vprops = srcExpr ? receiverValueTypes(srcExpr).flatMap((vt) => propertiesAcrossArms(vt)) : [];
|
|
7182
|
+
if ((type && type.getProperties) || vprops.length) {
|
|
7183
|
+
for (const p of [...(type?.getProperties ? propertiesAcrossArms(type) : []), ...vprops]) { // R259 — the disjunction, not the intersection
|
|
6663
7184
|
if (classBodiedGetter(p)) continue; // prototype + non-enumerable → not copied by a spread
|
|
6664
7185
|
for (const hit of accessorsFromSym(p, "get")) recordAccessorHit(owner, hit, p.getName(), srcExpr);
|
|
6665
7186
|
}
|
|
@@ -6670,9 +7191,7 @@ function enumerateGetters(owner, type, srcExpr) {
|
|
|
6670
7191
|
//
|
|
6671
7192
|
// R120 — THREE UNWRAPS AND ONE ALIAS HOP, because the question is which OBJECT is being copied and
|
|
6672
7193
|
// 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;
|
|
7194
|
+
const se = unwrapArgExpr(srcExpr); // R780
|
|
6676
7195
|
if (!se || !ts.isIdentifier(se)) return; // parameter / call return / member
|
|
6677
7196
|
let sym0 = checker.getSymbolAtLocation(se);
|
|
6678
7197
|
if (!sym0) return;
|
|
@@ -6682,9 +7201,7 @@ function enumerateGetters(owner, type, srcExpr) {
|
|
|
6682
7201
|
if (sym0.flags & ts.SymbolFlags.Alias) { try { sym0 = checker.getAliasedSymbol(sym0) ?? sym0; } catch { /* unresolved import */ } }
|
|
6683
7202
|
for (const d of sym0.declarations ?? []) {
|
|
6684
7203
|
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;
|
|
7204
|
+
const init = unwrapArgExpr(d.initializer); // R780
|
|
6688
7205
|
if (!ts.isObjectLiteralExpression(init)) continue;
|
|
6689
7206
|
for (const pr of init.properties) {
|
|
6690
7207
|
if (!ts.isGetAccessorDeclaration(pr)) continue;
|
|
@@ -6729,8 +7246,7 @@ function enumerateGetters(owner, type, srcExpr) {
|
|
|
6729
7246
|
const provenCopiedKeys = (sources) => {
|
|
6730
7247
|
const keys = new Set();
|
|
6731
7248
|
for (let s of sources) {
|
|
6732
|
-
|
|
6733
|
-
|| ts.isSatisfiesExpression(s) || ts.isNonNullExpression(s))) s = s.expression;
|
|
7249
|
+
s = unwrapArgExpr(s); // R780
|
|
6734
7250
|
if (!s || !ts.isObjectLiteralExpression(s)) return null; // parameter / call return / variable
|
|
6735
7251
|
for (const pr of s.properties) {
|
|
6736
7252
|
if (ts.isSpreadAssignment(pr)) return null; // `{...o}` copies an unknown key set
|
|
@@ -6808,12 +7324,15 @@ function enumerateTargetAccessors(owner, targetExpr, keys, kind /* "get" | "set"
|
|
|
6808
7324
|
if (!owner || !targetExpr) return;
|
|
6809
7325
|
const t = checker.getTypeAtLocation(targetExpr);
|
|
6810
7326
|
if (!t || !t.getProperties) return;
|
|
7327
|
+
// R246 — every property the target's VALUE can carry: its static type's, and (under an assertion or an
|
|
7328
|
+
// `any`/`unknown` parameter) the types the value really has. One list for both branches below.
|
|
7329
|
+
const tprops = [...propertiesAcrossArms(t), ...receiverValueTypes(targetExpr).flatMap((vt) => propertiesAcrossArms(vt))];
|
|
6811
7330
|
// `!keys`, not `keys === null`: a Set is always truthy, so this is exactly "the key set is not
|
|
6812
7331
|
// provable" — and it keeps the tolerance the old `keys && !keys.has(…)` line had for a caller that
|
|
6813
7332
|
// passes nothing. `keys.has` below is now unguarded, so a future third call site omitting the
|
|
6814
7333
|
// argument would throw rather than take this branch.
|
|
6815
7334
|
if (!keys) {
|
|
6816
|
-
for (const p of
|
|
7335
|
+
for (const p of tprops) { // R259 — the disjunction, not the intersection
|
|
6817
7336
|
if (!accessorsFromSym(p, kind).length) continue;
|
|
6818
7337
|
if (!keyCouldNameAccessor(p, unprovable.keyType ?? null)) continue;
|
|
6819
7338
|
const rec = fns.get(owner);
|
|
@@ -6823,7 +7342,9 @@ function enumerateTargetAccessors(owner, targetExpr, keys, kind /* "get" | "set"
|
|
|
6823
7342
|
}
|
|
6824
7343
|
return; // a target declaring no reachable accessor discloses nothing (0 real invocations)
|
|
6825
7344
|
}
|
|
6826
|
-
|
|
7345
|
+
const seenP = new Set();
|
|
7346
|
+
for (const p of tprops) { // R259 — the disjunction, not the intersection
|
|
7347
|
+
if (seenP.has(p)) continue; seenP.add(p);
|
|
6827
7348
|
// NO `classBodiedGetter`-style exclusion here, and the asymmetry is the point rather than an
|
|
6828
7349
|
// oversight: R115 excluded a class-bodied GETTER because a prototype accessor is non-enumerable and
|
|
6829
7350
|
// therefore never COPIED. A prototype SETTER is the opposite — it is found by the assignment's
|
|
@@ -6927,6 +7448,29 @@ function decoratorArgUnit(callNode) {
|
|
|
6927
7448
|
}
|
|
6928
7449
|
return qual;
|
|
6929
7450
|
}
|
|
7451
|
+
// ⟨SOUNDNESS R815⟩ The `extends <expr>` unit of a class that has a constructor unit. The expression runs ONCE, at
|
|
7452
|
+
// class definition (so the evaluating unit edges here, pass 2a′) — and every construction then runs the
|
|
7453
|
+
// constructor of the class it produced through `super`, implicit or explicit, whose body this scan sees only as
|
|
7454
|
+
// the expression's own callees (`mixin()`'s class expression is attributed to `mixin`; a dependency's
|
|
7455
|
+
// `PartialType(X)` is the dependency's ledger/`invisible`). So the constructor edges here too. Attributing the
|
|
7456
|
+
// expression to the constructor ALONE (the old climb) left the definer silent; attributing it to the definer
|
|
7457
|
+
// ALONE drops the super reach — MEASURED: `new D3()` over `class D3 extends Partial(Plain)` from a dependency
|
|
7458
|
+
// that writes in its generated constructor (EXECUTED) went from `invisible:['mixdep']` to ABSENT, and nest's six
|
|
7459
|
+
// `extends PartialType(…)` DTO constructors lost their `@nestjs/mapped-types` disclosure the same way.
|
|
7460
|
+
function heritageUnit(cls, ctorQual) {
|
|
7461
|
+
const qual = ctorQual.replace(/\.constructor$/, ".<heritage>");
|
|
7462
|
+
if (!fns.has(qual)) {
|
|
7463
|
+
const sf = cls.getSourceFile();
|
|
7464
|
+
const h = (cls.heritageClauses ?? []).find((c) => c.token === ts.SyntaxKind.ExtendsKeyword) ?? cls;
|
|
7465
|
+
fns.set(qual, { local: `${fns.get(ctorQual)?.local?.replace(/\.constructor$/, "") ?? "<anonymous>"}.<heritage>`,
|
|
7466
|
+
direct: new Set(), fsKinds: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
|
|
7467
|
+
cmds: new Set(), paths: new Set(), blind: new Set(), incomplete: new Set(), dispatch: new Set(), why: new Set(),
|
|
7468
|
+
entry: false, unitKind: "initializer",
|
|
7469
|
+
loc: `${path.relative(rootDir, sf.fileName)}:${sf.getLineAndCharacterOfPosition(h.getStart()).line + 1}:1`,
|
|
7470
|
+
endLine: sf.getLineAndCharacterOfPosition(h.getEnd()).line + 1 });
|
|
7471
|
+
}
|
|
7472
|
+
return qual;
|
|
7473
|
+
}
|
|
6930
7474
|
// nearest enclosing analyzed function (closures attribute to it — SEMANTICS §2)
|
|
6931
7475
|
function enclosing(node) {
|
|
6932
7476
|
let prev = null;
|
|
@@ -6966,6 +7510,42 @@ function enclosing(node) {
|
|
|
6966
7510
|
if (ts.isDecorator(p)) return null;
|
|
6967
7511
|
const n = nodeName.get(p);
|
|
6968
7512
|
if (n) return n;
|
|
7513
|
+
// ⟨SOUNDNESS R815⟩ THE REST OF THE CLASS-DEFINITION-TIME FAMILY R782/R785 started. Three more pieces of a class
|
|
7514
|
+
// run when the class is EVALUATED, not when it is constructed or a member is called — and the climb
|
|
7515
|
+
// attributed each to a unit that never runs it, leaving the evaluating unit silent (EXECUTED, lane fixture
|
|
7516
|
+
// `fx/r815`: the enclosing function ABSENT, `deny Fs` exit 0; the effect fabricated onto `C.constructor` or the
|
|
7517
|
+
// method):
|
|
7518
|
+
// · a `static x = …` INITIALISER runs at definition, exactly like a `static {}` block, so it lands on the
|
|
7519
|
+
// same `<static-init>` unit (which pass 2a′ wires from the evaluating unit). Climbing on reached the
|
|
7520
|
+
// ClassDeclaration, i.e. `C.constructor` — every `new C()` charged, the definer silent. An instance field
|
|
7521
|
+
// is NOT this (it runs per construction; the constructor is right), and a property that IS a unit
|
|
7522
|
+
// (`static h = () => …` minted as `C.h`) answered above before reaching here.
|
|
7523
|
+
// · an `extends <expr>` HERITAGE expression and
|
|
7524
|
+
// · a COMPUTED member name `[k()]` (class OR object-literal member)
|
|
7525
|
+
// are evaluated in the scope AROUND the class / literal, by whatever evaluates it — so the climb resumes
|
|
7526
|
+
// OUTSIDE the container. (The computed name hung under the member's own node, so the method `C3.[w("k")]`
|
|
7527
|
+
// was charged for its own name.)
|
|
7528
|
+
if (prev && ts.isPropertyDeclaration(p) && p.initializer === prev
|
|
7529
|
+
&& (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Static)
|
|
7530
|
+
&& p.parent && (ts.isClassDeclaration(p.parent) || ts.isClassExpression(p.parent)))
|
|
7531
|
+
{ R815_REACH?.("static"); return staticBlockUnit(p); }
|
|
7532
|
+
// ⟨R1016⟩ an INSTANCE field initialiser of a class expression runs at construction — its constructor unit.
|
|
7533
|
+
if (prev && ts.isPropertyDeclaration(p) && p.initializer === prev
|
|
7534
|
+
&& !(ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Static)
|
|
7535
|
+
&& !ts.isArrowFunction(prev) && !ts.isFunctionExpression(prev)
|
|
7536
|
+
&& p.parent && ts.isClassExpression(p.parent) && classExprCtorQual.has(p.parent))
|
|
7537
|
+
return classExprCtorQual.get(p.parent);
|
|
7538
|
+
if (ts.isHeritageClause(p) && p.token === ts.SyntaxKind.ExtendsKeyword && p.parent
|
|
7539
|
+
&& (ts.isClassDeclaration(p.parent) || ts.isClassExpression(p.parent))) {
|
|
7540
|
+
const ctor = nodeName.get(p.parent);
|
|
7541
|
+
if (ctor) { R815_REACH?.("heritage"); return heritageUnit(p.parent, ctor); }
|
|
7542
|
+
// a class with no constructor unit: the climb already resumes outside it, at the evaluating unit
|
|
7543
|
+
}
|
|
7544
|
+
if (ts.isComputedPropertyName(p) && p.parent?.parent) {
|
|
7545
|
+
const container = p.parent.parent;
|
|
7546
|
+
if (ts.isClassDeclaration(container) || ts.isClassExpression(container)) { R815_REACH?.("ckey"); return enclosing(container.parent); }
|
|
7547
|
+
if (ts.isObjectLiteralExpression(container)) { R815_REACH?.("okey"); return enclosing(container); }
|
|
7548
|
+
}
|
|
6969
7549
|
// Reached the SourceFile with no named unit: a TOP-LEVEL executable statement. Attribute to the
|
|
6970
7550
|
// file's synthesized `<module>` initializer unit (minted lazily here) rather than dropping it.
|
|
6971
7551
|
if (ts.isSourceFile(p)) return moduleUnit(p);
|
|
@@ -7204,8 +7784,7 @@ function recordDispatch(rec, decl, pkg) {
|
|
|
7204
7784
|
function packageProducedReceiver(recvExpr, pkg) {
|
|
7205
7785
|
if (!recvExpr || !pkg) return null;
|
|
7206
7786
|
const unwrap = (x) => {
|
|
7207
|
-
|
|
7208
|
-
|| ts.isAsExpression(x))) x = x.expression;
|
|
7787
|
+
for (x = unwrapArgExpr(x); x && ts.isAwaitExpression(x);) x = unwrapArgExpr(x.expression); // R780: + `await`
|
|
7209
7788
|
return x;
|
|
7210
7789
|
};
|
|
7211
7790
|
let e = unwrap(recvExpr);
|
|
@@ -8357,6 +8936,89 @@ const HOF_INVOKERS = new Set([
|
|
|
8357
8936
|
"then", "catch", "finally", "nextTick",
|
|
8358
8937
|
]);
|
|
8359
8938
|
|
|
8939
|
+
// ⟨SOUNDNESS R803⟩ THE LIST ABOVE IS NO LONGER THE GATE — IT IS THE POSITION MAP FOR THE CALLEES IT NAMES.
|
|
8940
|
+
//
|
|
8941
|
+
// `HOF_INVOKERS` decided, by callee NAME, whether a function reference handed to a non-local callee could be
|
|
8942
|
+
// invoked by it, and every callee it did not name dropped the reference: no edge, no `Unknown`, no row.
|
|
8943
|
+
// Measured on `fx/r803`, every cell EXECUTED by node to run its handler: `e.on`, `e.once`, `e.addListener`,
|
|
8944
|
+
// `process.once`, `http.createServer(h)`, `Array.from(xs, f)`, `new Promise(exec)`, lodash `_.each`/`_.times`,
|
|
8945
|
+
// a dependency `queue.add(f)` and `EventTarget.addEventListener` were all ABSENT, `deny Fs` exit 0 on each
|
|
8946
|
+
// caller. The comment that guarded it called the omission "an honest under-report (sound)" — true about
|
|
8947
|
+
// fabrication, false about silence; it also falsified `KAPPA_PURE`'s premise that lodash/rxjs side effects
|
|
8948
|
+
// "live in visible user callbacks", because this list is what decides whether those callbacks are visible.
|
|
8949
|
+
//
|
|
8950
|
+
// WHY NOT "ASK THE SIGNATURE" ALONE — measured by the ts vein analysis on the 28-entry roster: of 253 sites
|
|
8951
|
+
// where a callee's parameter is callable-typed and the argument is a local function reference, about half are
|
|
8952
|
+
// REMOVALS or STORES (`off`, `removeListener`, `removeEventListener`, `Map.set`, `Object.assign`), and 15 of the
|
|
8953
|
+
// 25 effect-bearing ones never invoke. A signature rule alone fabricates on those.
|
|
8954
|
+
//
|
|
8955
|
+
// So the rule is: an UNLISTED non-local callee invokes every argument position its resolved signature
|
|
8956
|
+
// POSITIVELY declares callable (`calleeParamIsCallable === true`), unless the callee is on the DENYLIST below.
|
|
8957
|
+
// The failure directions, stated because they are the reason for the shape (`candor-denylist-over-allowlist`):
|
|
8958
|
+
// · a non-invoker MISSING from the denylist → an over-charge (its reference's effects charged to the
|
|
8959
|
+
// caller). Visible, and the corpus A/B is the instrument that catches it.
|
|
8960
|
+
// · an invoker the old list missed → now charged. Nothing an invoker can do lands it back in silence
|
|
8961
|
+
// except being DENYLISTED, and the denylist only names declarations whose semantics are a removal or a
|
|
8962
|
+
// store — the removal NAMES on any receiver, and reviewed ES-lib / node-core members by owner.
|
|
8963
|
+
// · a callee whose parameter is `any`/`unknown`/unresolvable (`null`) is NOT admitted here. That is a
|
|
8964
|
+
// residual, named rather than hidden: `console.log(f)`/`assert.equal(f, …)` take `any` and never invoke,
|
|
8965
|
+
// and admitting `null` charges them — priced on the roster before deciding (see the commit message).
|
|
8966
|
+
// The listed names keep their own position map (`hofInvokesArg`/`hofArgIsNeverCallback`) unchanged.
|
|
8967
|
+
//
|
|
8968
|
+
// Removal by NAME on ANY receiver: a method named `removeListener` that invokes the listener it is removing
|
|
8969
|
+
// would be perverse, and these four are the registration API's inverse everywhere they appear (node events,
|
|
8970
|
+
// DOM `EventTarget`, eventemitter3, socket.io's emitter). Asked of the callee's spelling AND of the declaration
|
|
8971
|
+
// it resolved to, because `const processOff = process.off.bind(process); processOff(sig, h)` (vitest) and
|
|
8972
|
+
// got's `removeSessionSocketListener` are the same removal under a local name.
|
|
8973
|
+
//
|
|
8974
|
+
// Everything else is asked of the RESOLVED REVIEWED-LIBRARY declaration (ES lib, @types/node), never of a
|
|
8975
|
+
// name, so a DEPENDENCY's `add(fn)` (p-queue — a real roster silence) is not excused by sharing a name with
|
|
8976
|
+
// `Set.add`. The rule there is the GENERIC VALUE SLOT: a parameter whose declared type is a bare type
|
|
8977
|
+
// parameter (alone or beside non-callable members such as `PromiseLike<T>`) whose constraint has no call
|
|
8978
|
+
// signature. Such a callee is handed a value it knows nothing about and cannot call without a cast — `Map.set`'s
|
|
8979
|
+
// `V`, `Set.add`/`includes`/`indexOf`'s `T`, `Object.assign`/`defineProperties`/`freeze`'s `T`, a promise
|
|
8980
|
+
// executor's `resolve(value: T | PromiseLike<T>)`, `new Proxy(target: T)`, `new WeakRef(target: T)`. The
|
|
8981
|
+
// argument reads callable only because the CALL instantiated `T` with a function type. Restricted to the
|
|
8982
|
+
// reviewed library declarations on purpose: a dependency CAN cast and call (`(x as any)()`), so there the
|
|
8983
|
+
// generic slot is an assumption, and an assumption is not denylist evidence.
|
|
8984
|
+
// ⟨R241⟩ lib.dom interfaces whose every member touches a persistent browser store (see the es-lib call arm).
|
|
8985
|
+
const BROWSER_STORE_IFACES = new Set(["IDBFactory", "IDBDatabase", "IDBObjectStore", "IDBIndex", "IDBCursor",
|
|
8986
|
+
"IDBCursorWithValue", "IDBTransaction", "CacheStorage", "Cache", "StorageManager", "FileSystemHandle",
|
|
8987
|
+
"FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemWritableFileStream", "FileSystemSyncAccessHandle", "CookieStore"]);
|
|
8988
|
+
const HOF_REMOVAL_NAMES = new Set(["off", "removeListener", "removeEventListener", "removeAllListeners"]);
|
|
8989
|
+
// NOT non-invokers — they invoke their first argument — but the reflective arm below answers them with the
|
|
8990
|
+
// invoked function's OWN arguments (R947's locator), and this arm, which cannot see those, would add a second
|
|
8991
|
+
// locator-less charge beside it (measured: `Reflect.apply(fs.readFileSync, undefined, ["/tmp/ok/b"])` lost its
|
|
8992
|
+
// `allow Fs /tmp/ok` certification). Owned elsewhere, so excluded here.
|
|
8993
|
+
const HOF_OWNED_ELSEWHERE = new Set(["Reflect.apply", "Reflect.construct"]);
|
|
8994
|
+
const hofOwnerMember = (decl) => {
|
|
8995
|
+
if (!decl) return null;
|
|
8996
|
+
const member = ts.isConstructSignatureDeclaration(decl) ? "new" : decl.name?.getText?.();
|
|
8997
|
+
const holder = decl.parent && ts.isModuleBlock(decl.parent) ? decl.parent.parent : decl.parent; // `namespace Reflect {…}`
|
|
8998
|
+
const owner = holder?.name?.getText?.() ?? null;
|
|
8999
|
+
return owner && member ? `${owner}.${member}` : null;
|
|
9000
|
+
};
|
|
9001
|
+
const declIsReviewedLib = (decl) => !!decl && (declModule(decl) === "<es-lib>" || declIsNodeTypes(decl));
|
|
9002
|
+
const hofCalleeNeverInvokes = (decl, name) => {
|
|
9003
|
+
if ((name && HOF_REMOVAL_NAMES.has(name)) || HOF_REMOVAL_NAMES.has(decl?.name?.getText?.() ?? "")) return true;
|
|
9004
|
+
return declIsReviewedLib(decl) && HOF_OWNED_ELSEWHERE.has(hofOwnerMember(decl) ?? "");
|
|
9005
|
+
};
|
|
9006
|
+
const calleeParamIsGenericSlot = (node, i) => {
|
|
9007
|
+
const sd = checker.getResolvedSignature?.(node)?.declaration;
|
|
9008
|
+
const ps = sd?.parameters;
|
|
9009
|
+
if (!ps?.length || i >= ps.length || !ps[i].type || ps[i].dotDotDotToken) return false;
|
|
9010
|
+
const t = checker.getTypeFromTypeNode(ps[i].type);
|
|
9011
|
+
const isFreeTP = (u) => !!(u.flags & ts.TypeFlags.TypeParameter)
|
|
9012
|
+
&& !(checker.getBaseConstraintOfType(u)?.getCallSignatures?.().length > 0);
|
|
9013
|
+
const parts = t.types ?? [t];
|
|
9014
|
+
return parts.some(isFreeTP) && !parts.some((u) => !isFreeTP(u) && u.getCallSignatures?.().length > 0);
|
|
9015
|
+
};
|
|
9016
|
+
// ⟨R1031⟩ reviewed-library callees whose object-literal argument's callable members are INSTALLED, not
|
|
9017
|
+
// invoked: a property descriptor (`get`/`set`) or a descriptor map. Asked of the resolved declaration.
|
|
9018
|
+
const OBJ_ARG_INSTALLS = new Set(["ObjectConstructor.defineProperty", "ObjectConstructor.defineProperties",
|
|
9019
|
+
"ObjectConstructor.create", "Reflect.defineProperty"]);
|
|
9020
|
+
const HOF_REACH = process.env.CANDOR_HOF_REACH ? (k, n) => console.error(`HOF-REACH ${k} ${n}`) : null;
|
|
9021
|
+
|
|
8360
9022
|
// ---- process.env recognition: the direct dot access (`process.env.KEY`) is the JVM System.getenv twin,
|
|
8361
9023
|
// but the same environment READ is spelled several other ways that all read silent-pure without help:
|
|
8362
9024
|
// bracket access (`process.env[k]`), a local const-alias (`const env = process.env; env.KEY`),
|
|
@@ -8387,14 +9049,25 @@ const declImportsNodeProcess = (decl) => {
|
|
|
8387
9049
|
// downstream consult — the fix in one place, per brief §F1 item 3 ("make the two paths share one
|
|
8388
9050
|
// authority", not patch the losing copy). Populated by the pre-pass immediately below.
|
|
8389
9051
|
const processAliasSymbols = new Set();
|
|
8390
|
-
const identIsGlobalProcess = (
|
|
9052
|
+
const identIsGlobalProcess = (id0) => {
|
|
9053
|
+
// ⟨SOUNDNESS R936⟩ through the ONE transparent-wrapper set (R780): `(process as any).argv`, `(<any>process).argv`
|
|
9054
|
+
// and even `(process).argv` were ABSENT because the argv arm handed this the wrapped node and this matched an
|
|
9055
|
+
// Identifier only — the env arm unwrapped first and the argv arm did not. Unwrapping HERE gives every caller
|
|
9056
|
+
// the same answer instead of each remembering to.
|
|
9057
|
+
const id = unwrapArgExpr(id0);
|
|
8391
9058
|
if (!id) return false;
|
|
9059
|
+
// ⟨SOUNDNESS R936⟩ `require("process")` / `require("node:process")` IS the process object — the module-binding
|
|
9060
|
+
// spelling of the same global (CJS, `--allow-js` trees, and `createRequire`). `require("process").env.HOME` read
|
|
9061
|
+
// nothing (EXECUTED). Keyed on a callee spelled `require` and the literal specifier; a project function that
|
|
9062
|
+
// happens to be called `require` and returns something else is the only false match, and it over-charges.
|
|
9063
|
+
if (ts.isCallExpression(id) && ts.isIdentifier(id.expression) && id.expression.text === "require"
|
|
9064
|
+
&& id.arguments.length === 1 && ts.isStringLiteralLike(id.arguments[0])
|
|
9065
|
+
&& (id.arguments[0].text === "process" || id.arguments[0].text === "node:process")) return true;
|
|
8392
9066
|
// `globalThis.process` / `global.process` — the SAME process object reached off the global (isomorphic code:
|
|
8393
9067
|
// `globalThis.process?.env`, often `(globalThis as any).process.env`). Unwrap parens/`as` casts around the
|
|
8394
9068
|
// root; the `globalThis`/`global` root must be the ambient global, not a project shadow.
|
|
8395
9069
|
if (ts.isPropertyAccessExpression(id) && id.name.text === "process") {
|
|
8396
|
-
|
|
8397
|
-
while (ts.isParenthesizedExpression(root) || ts.isAsExpression(root) || ts.isNonNullExpression(root)) root = root.expression;
|
|
9070
|
+
const root = unwrapArgExpr(id.expression); // R780: `(<any>globalThis).process` is the same object
|
|
8398
9071
|
if (ts.isIdentifier(root) && (root.text === "globalThis" || root.text === "global")) {
|
|
8399
9072
|
const gd = checker.getSymbolAtLocation(root)?.declarations ?? [];
|
|
8400
9073
|
return !gd.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)));
|
|
@@ -8423,9 +9096,7 @@ const identIsGlobalProcess = (id) => {
|
|
|
8423
9096
|
{
|
|
8424
9097
|
const isGlobalProcessInitializer = (expr) => {
|
|
8425
9098
|
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`
|
|
9099
|
+
return identIsGlobalProcess(unwrapArgExpr(expr)); // R780 // handles `globalThis.process` / `global.process` / bare ambient `process`
|
|
8429
9100
|
};
|
|
8430
9101
|
const collectProcessAliases = (node) => {
|
|
8431
9102
|
if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name) && node.initializer) {
|
|
@@ -8434,8 +9105,7 @@ const identIsGlobalProcess = (id) => {
|
|
|
8434
9105
|
if (sym) processAliasSymbols.add(sym);
|
|
8435
9106
|
}
|
|
8436
9107
|
} 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;
|
|
9108
|
+
const root = unwrapArgExpr(node.initializer); // R780
|
|
8439
9109
|
if (ts.isIdentifier(root) && (root.text === "globalThis" || root.text === "global")) {
|
|
8440
9110
|
const gd = checker.getSymbolAtLocation(root)?.declarations ?? [];
|
|
8441
9111
|
if (!gd.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)))) {
|
|
@@ -8476,11 +9146,7 @@ const identIsGlobalProcess = (id) => {
|
|
|
8476
9146
|
// EXPRESSION — that is the model this adopts, with identity widened from a node to a value.
|
|
8477
9147
|
//
|
|
8478
9148
|
// 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
|
-
};
|
|
9149
|
+
const unwrapEnvTransparent = (e) => unwrapArgExpr(e); // R780: one wrapper set, not a fourth copy of it
|
|
8484
9150
|
// `process.env` / `process["env"]` / `globalThis.process?.env` / `(process as any).env` — the global
|
|
8485
9151
|
// process object's `env` member. The `process` side keeps `identIsGlobalProcess`'s shadow guard, so a
|
|
8486
9152
|
// project-local `process` (a parameter, a module `const`) matches nothing.
|
|
@@ -8536,16 +9202,159 @@ const envValueLeaves = (expr, out = []) => {
|
|
|
8536
9202
|
// READ was not even `Unknown` — it was absent).
|
|
8537
9203
|
// A binding is a `VariableDeclaration` with an identifier name (no initializer = a binding with no env
|
|
8538
9204
|
// leaf), a plain or logical (`??=`/`||=`/`&&=`) assignment to an identifier, or `{ env } = process` /
|
|
8539
|
-
// `{ env: e } = process` (wrappers unwrapped).
|
|
8540
|
-
//
|
|
9205
|
+
// `{ env: e } = process` (wrappers unwrapped).
|
|
9206
|
+
//
|
|
9207
|
+
// ⟨R934/R935⟩ THE SAME QUESTION OVER EVERY STORAGE LOCATION WHOSE BINDINGS THIS PASS CAN ENUMERATE, not
|
|
9208
|
+
// over locals only. The identity above was asked of a VARIABLE; the environment object also travels
|
|
9209
|
+
// through a PARAMETER (R934: `function rd(e) { return e.K }` called as `rd(process.env)` — the read runs
|
|
9210
|
+
// in `rd`'s frame and `rd` was absent) and through a heap FIELD (R935: `const cfg = { env: process.env }`
|
|
9211
|
+
// at module level, read later as `cfg.env.K` by a function that never calls the module initializer —
|
|
9212
|
+
// absent; only `<module>` carried Env). Both are one defect: the alias model stopped at the first
|
|
9213
|
+
// location that was not a local, so the value was lost the moment it was stored anywhere else. The fix
|
|
9214
|
+
// is not a second analysis per location kind; it is the SAME binding table and the SAME two fixpoints
|
|
9215
|
+
// with two more kinds of location in them:
|
|
9216
|
+
//
|
|
9217
|
+
// PARAMETER — its bindings are the arguments at every VISIBLE call site (positional, up to a spread;
|
|
9218
|
+
// a site that omits it binds its default, else `undefined`). It joins MUST only — every visible site
|
|
9219
|
+
// hands it the environment — and NEVER MAY. That asymmetry is pass 2b's ruling, transferred: a
|
|
9220
|
+
// parameter fed the environment at one site and `{}` at another is the DIVERGENT shared-HOF shape,
|
|
9221
|
+
// and charging the callee would pool the read onto the caller that passed `{}` (a fabrication). The
|
|
9222
|
+
// caller that passes the environment is already charged by the Env arm (a call argument is not an
|
|
9223
|
+
// inert consumer), so every gate above the callee still fires — the callee's own scoped gate is the
|
|
9224
|
+
// only one that stays 0, exactly as it does for a divergent HOF. Only functions whose every reference
|
|
9225
|
+
// is a CALL are eligible: a function that escapes as a value (a callback, `.call`, `.bind`, stored)
|
|
9226
|
+
// has bindings this pass cannot see. Methods are eligible only on a class with no heritage that no
|
|
9227
|
+
// project class extends — a dispatched call resolves to the base declaration, and an override's
|
|
9228
|
+
// parameter would otherwise look fed by fewer sites than it is.
|
|
9229
|
+
// PROPERTY — its bindings are the initializer of every VALUE-BEARING declaration (an object-literal
|
|
9230
|
+
// property, a class field, a constructor parameter property — which is bound exactly as its
|
|
9231
|
+
// parameter is) and the right side of every assignment to it. An absent initializer is not a binding:
|
|
9232
|
+
// a field that is `undefined` until it is assigned the environment cannot make a reader read a
|
|
9233
|
+
// DIFFERENT object's keys, and that is the only thing MUST has to rule out. MUST charges a reader Env;
|
|
9234
|
+
// MAY (rebound to something else somewhere) discloses `Unknown[env-maybe-read]`, the local-alias
|
|
9235
|
+
// posture, because unlike a parameter there is no caller in the reader's chain that carries it. A
|
|
9236
|
+
// property declared by an interface or a type literal (`PropertySignature`) is NOT a location here:
|
|
9237
|
+
// any conforming object can be its value, so its bindings are an open set — the residual is stated
|
|
9238
|
+
// in SOUNDNESS, not hidden. A field of a class some project class extends is excluded for the
|
|
9239
|
+
// override reason above.
|
|
9240
|
+
//
|
|
9241
|
+
// Writing the environment INTO a property stays charged where it happens (the Env arm's "store" rule);
|
|
9242
|
+
// this adds the READERS and removes nothing.
|
|
8541
9243
|
const envAliasSymbols = new Set();
|
|
8542
9244
|
const envMayAliasSymbols = new Set();
|
|
9245
|
+
// ⟨R935⟩ property locations, kept apart from the local sets so a property name never reaches the
|
|
9246
|
+
// identifier-keyed consumers (2c's `envSourceKind`, the alias-binding inert rule) by accident.
|
|
9247
|
+
const envPropMustSymbols = new Set();
|
|
9248
|
+
const envPropMaySymbols = new Set();
|
|
9249
|
+
// ⟨R935⟩ a property location's identity: its first declaration node (see `memberSymOf` for why not the symbol).
|
|
9250
|
+
const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
|
|
9251
|
+
// ⟨SOUNDNESS R935, the RECEIVER half⟩ THE LOCATION A MEMBER READ NAMES IS THE OBJECT THE RECEIVER HOLDS, not the
|
|
9252
|
+
// member its DECLARED TYPE declares. The first cut keyed a read on the checker's property symbol, which is the
|
|
9253
|
+
// literal's own property only when the binding's type was INFERRED from the literal. Give the binding a type —
|
|
9254
|
+
// `const icfg: Cfg = { env: process.env }` — and `icfg.env` resolves to `Cfg`'s `PropertySignature`, an open set
|
|
9255
|
+
// that is no location at all, so `viaIface() { return icfg.env.SECRET }` was ABSENT with `deny Env viaIface` exit
|
|
9256
|
+
// 0 over a read that printed the planted secret (EXECUTED). An ARRAY literal had no location to key at all:
|
|
9257
|
+
// `const arr = [process.env]; arr[0].SECRET`, same measurement. Where the receiver is an identifier bound by a
|
|
9258
|
+
// `const` to an object or array literal, the object IS that literal, so the read is keyed on the literal's own
|
|
9259
|
+
// member — the last property of that name (a later spread makes it unresolvable), or the element at a literal
|
|
9260
|
+
// index with no spread or hole before it. Everything that can REBIND such a member off the books is a binding
|
|
9261
|
+
// the fixpoint must see, or the location is charged over a replaced value:
|
|
9262
|
+
// · an OBJECT literal's member is written through its declared member too (`icfg.env = {}` names `Cfg.env`,
|
|
9263
|
+
// as does any other `Cfg`-typed object), so a read resolved this way is MAY wherever that declared member
|
|
9264
|
+
// is ever written or reflectively written (`envDeclWritten`, filled by the binding walk below);
|
|
9265
|
+
// · an ARRAY's elements move under any method call or escape, so an element is MUST only while every
|
|
9266
|
+
// reference to the array is a literal-index READ; any other reference makes every element MAY.
|
|
9267
|
+
// MAY discloses `Unknown[env-maybe-read]`, as every other location does. A `let` binding, a receiver that is a
|
|
9268
|
+
// parameter or a property, and a read through iteration (`for…of`, `.map`) are not resolved here.
|
|
9269
|
+
const envDeclWritten = new Set();
|
|
9270
|
+
const envLitElems = new Map(); // array-literal element node -> true while every reference is an index read
|
|
9271
|
+
const envConstLiteral = (id) => {
|
|
9272
|
+
if (!id || !ts.isIdentifier(id)) return null;
|
|
9273
|
+
let sym = checker.getSymbolAtLocation(id);
|
|
9274
|
+
if (sym && (sym.flags & ts.SymbolFlags.Alias)) { try { sym = checker.getAliasedSymbol(sym); } catch { return null; } }
|
|
9275
|
+
const d = sym?.valueDeclaration;
|
|
9276
|
+
if (!d || !ts.isVariableDeclaration(d) || !ts.isIdentifier(d.name) || !d.initializer
|
|
9277
|
+
|| !(ts.getCombinedNodeFlags(d) & ts.NodeFlags.Const)
|
|
9278
|
+
|| !projectFiles.has(path.resolve(d.getSourceFile().fileName))) return null;
|
|
9279
|
+
const lit = unwrapArgExpr(d.initializer);
|
|
9280
|
+
return lit && (ts.isObjectLiteralExpression(lit) || ts.isArrayLiteralExpression(lit)) ? lit : null;
|
|
9281
|
+
};
|
|
9282
|
+
const envMemberName = (p) => (p.name && (ts.isIdentifier(p.name) || ts.isStringLiteralLike(p.name)
|
|
9283
|
+
|| ts.isNumericLiteral(p.name) || ts.isPrivateIdentifier(p.name))) ? p.name.text : null;
|
|
9284
|
+
// -> { key, declKey } for a member read whose receiver is a const literal, else null.
|
|
9285
|
+
const envLiteralMember = (n) => {
|
|
9286
|
+
let recv, name = null, idx = null;
|
|
9287
|
+
if (ts.isPropertyAccessExpression(n)) { recv = n.expression; name = n.name.text; }
|
|
9288
|
+
else if (ts.isElementAccessExpression(n) && n.argumentExpression) {
|
|
9289
|
+
recv = n.expression;
|
|
9290
|
+
if (ts.isStringLiteralLike(n.argumentExpression)) name = n.argumentExpression.text;
|
|
9291
|
+
else if (ts.isNumericLiteral(n.argumentExpression)) idx = Number(n.argumentExpression.text);
|
|
9292
|
+
else return null;
|
|
9293
|
+
} else return null;
|
|
9294
|
+
const lit = envConstLiteral(unwrapArgExpr(recv));
|
|
9295
|
+
if (!lit) return null;
|
|
9296
|
+
if (ts.isArrayLiteralExpression(lit)) {
|
|
9297
|
+
if (idx === null || !Number.isInteger(idx) || idx >= lit.elements.length) return null;
|
|
9298
|
+
for (let i = 0; i <= idx; i++) {
|
|
9299
|
+
const el = lit.elements[i];
|
|
9300
|
+
if (ts.isSpreadElement(el) || ts.isOmittedExpression(el)) return null;
|
|
9301
|
+
}
|
|
9302
|
+
return { key: lit.elements[idx], declKey: null };
|
|
9303
|
+
}
|
|
9304
|
+
const want = name ?? (idx !== null ? String(idx) : null);
|
|
9305
|
+
if (want === null) return null;
|
|
9306
|
+
let hit = null;
|
|
9307
|
+
for (const p of lit.properties) {
|
|
9308
|
+
if (ts.isSpreadAssignment(p)) hit = p;
|
|
9309
|
+
else if (envMemberName(p) === want) hit = p;
|
|
9310
|
+
}
|
|
9311
|
+
if (!hit || !(ts.isPropertyAssignment(hit) || ts.isShorthandPropertyAssignment(hit))) return null;
|
|
9312
|
+
const key = envLocKey(checker.getSymbolAtLocation(hit.name));
|
|
9313
|
+
if (!key) return null;
|
|
9314
|
+
let raw = null;
|
|
9315
|
+
try { raw = ts.isPropertyAccessExpression(n) ? checker.getSymbolAtLocation(n.name)
|
|
9316
|
+
: (n.argumentExpression ? checker.getSymbolAtLocation(n.argumentExpression) : null); } catch { raw = null; }
|
|
9317
|
+
return { key, declKey: envLocKey(raw) };
|
|
9318
|
+
};
|
|
8543
9319
|
{
|
|
8544
9320
|
const ENV_DESTRUCTURED = Symbol("env-destructured");
|
|
8545
|
-
const bindings = new Map(); // symbol -> [value expr | null | ENV_DESTRUCTURED]
|
|
9321
|
+
const bindings = new Map(); // symbol -> [value expr | null | ENV_DESTRUCTURED | { same: symbol }]
|
|
8546
9322
|
const note = (sym, v) => { if (sym) (bindings.get(sym) ?? bindings.set(sym, []).get(sym)).push(v); };
|
|
8547
9323
|
const LOGICAL_ASSIGN = new Set([ts.SyntaxKind.EqualsToken, ts.SyntaxKind.QuestionQuestionEqualsToken,
|
|
8548
9324
|
ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.AmpersandAmpersandEqualsToken]);
|
|
9325
|
+
const paramSyms = new Set(); // ⟨R934⟩ parameters with at least one recorded binding
|
|
9326
|
+
const propSyms = new Set(); // ⟨R935⟩ eligible property locations with at least one binding
|
|
9327
|
+
const propIneligible = new Set();
|
|
9328
|
+
// The LOCATION KEY of a member reference: `o.p` / `o["p"]` → the property's first declaration node.
|
|
9329
|
+
// Keyed by DECLARATION, not symbol: an object literal's type is WIDENED when it is bound, and the
|
|
9330
|
+
// widened type carries fresh transient property symbols — `cfg.env` at a read site and `env:` in the
|
|
9331
|
+
// literal are two symbol objects for one declaration (measured: the read never matched the binding).
|
|
9332
|
+
const memberRawSym = (n) => (ts.isPropertyAccessExpression(n) ? checker.getSymbolAtLocation(n.name)
|
|
9333
|
+
: (ts.isElementAccessExpression(n) && n.argumentExpression && ts.isStringLiteralLike(n.argumentExpression))
|
|
9334
|
+
? checker.getSymbolAtLocation(n.argumentExpression) : null) ?? null;
|
|
9335
|
+
const memberSymOf = (n) => envLiteralMember(n)?.key ?? envLocKey(memberRawSym(n));
|
|
9336
|
+
// Is this property symbol a location whose bindings we can enumerate? Every declaration must be
|
|
9337
|
+
// value-bearing, in a project file, and not a member of an extended class.
|
|
9338
|
+
const propEligible = (sym) => {
|
|
9339
|
+
if (!sym || propIneligible.has(sym)) return false;
|
|
9340
|
+
const decls = sym.declarations ?? [];
|
|
9341
|
+
const ok = decls.length > 0 && decls.every((d) => {
|
|
9342
|
+
if (!projectFiles.has(path.resolve(d.getSourceFile().fileName))) return false;
|
|
9343
|
+
if (ts.isPropertyAssignment(d) || ts.isShorthandPropertyAssignment(d)) return ts.isObjectLiteralExpression(d.parent);
|
|
9344
|
+
if (ts.isPropertyDeclaration(d)) return !d.modifiers?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword)
|
|
9345
|
+
&& !extendedClassSyms.has(classSymOf(d.parent));
|
|
9346
|
+
if (ts.isParameter(d)) return ts.isConstructorDeclaration(d.parent) && !extendedClassSyms.has(classSymOf(d.parent.parent));
|
|
9347
|
+
return false;
|
|
9348
|
+
});
|
|
9349
|
+
if (!ok) propIneligible.add(sym);
|
|
9350
|
+
return ok;
|
|
9351
|
+
};
|
|
9352
|
+
const noteProp = (sym, v) => {
|
|
9353
|
+
if (propEligible(sym)) { const k = envLocKey(sym); propSyms.add(k); note(k, v); }
|
|
9354
|
+
else if (sym && v !== undefined) envDeclWritten.add(envLocKey(sym)); // ⟨R935⟩ see `envLiteralMember`
|
|
9355
|
+
};
|
|
9356
|
+
// ⟨R935⟩ a WRITE through a member that is not itself a location still rebinds whatever object it lands on.
|
|
9357
|
+
const noteWrite = (sym, v) => { if (sym) { envDeclWritten.add(envLocKey(sym)); noteProp(sym, v); } };
|
|
8549
9358
|
const collect = (node) => {
|
|
8550
9359
|
if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
|
|
8551
9360
|
note(checker.getSymbolAtLocation(node.name), node.initializer ?? null);
|
|
@@ -8558,28 +9367,166 @@ const envMayAliasSymbols = new Set();
|
|
|
8558
9367
|
}
|
|
8559
9368
|
} else if (ts.isBinaryExpression(node) && LOGICAL_ASSIGN.has(node.operatorToken.kind) && ts.isIdentifier(node.left)) {
|
|
8560
9369
|
note(checker.getSymbolAtLocation(node.left), node.right);
|
|
9370
|
+
} else if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment
|
|
9371
|
+
&& node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
|
9372
|
+
&& (ts.isPropertyAccessExpression(node.left) || ts.isElementAccessExpression(node.left))) {
|
|
9373
|
+
// ⟨R935⟩ an assignment to a property location; a compound operator (`+=`) binds a non-environment.
|
|
9374
|
+
const s = memberRawSym(node.left);
|
|
9375
|
+
if (s) noteWrite(s, LOGICAL_ASSIGN.has(node.operatorToken.kind) ? node.right : null);
|
|
9376
|
+
// ⟨R935⟩ …and the const literal's own member, when the receiver resolves to one.
|
|
9377
|
+
const lm = envLiteralMember(node.left);
|
|
9378
|
+
if (lm && !ts.isArrayLiteralExpression(lm.key.parent)) { propSyms.add(lm.key); note(lm.key, LOGICAL_ASSIGN.has(node.operatorToken.kind) ? node.right : null); }
|
|
9379
|
+
} else if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {
|
|
9380
|
+
const s = checker.getSymbolAtLocation(node.name);
|
|
9381
|
+
if (s && (s.flags & ts.SymbolFlags.Property)) noteProp(s, node.initializer);
|
|
9382
|
+
} else if (ts.isShorthandPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {
|
|
9383
|
+
const s = checker.getSymbolAtLocation(node.name);
|
|
9384
|
+
const v = checker.getShorthandAssignmentValueSymbol(node);
|
|
9385
|
+
if (s && (s.flags & ts.SymbolFlags.Property)) noteProp(s, v ? { same: v } : null);
|
|
9386
|
+
} else if (ts.isPropertyDeclaration(node) && node.initializer && node.name) {
|
|
9387
|
+
noteProp(checker.getSymbolAtLocation(node.name), node.initializer);
|
|
9388
|
+
} else if (ts.isParameter(node) && ts.isIdentifier(node.name) && ts.isConstructorDeclaration(node.parent)
|
|
9389
|
+
&& ts.getModifiers?.(node)?.some((m) => m.kind === ts.SyntaxKind.PublicKeyword
|
|
9390
|
+
|| m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword
|
|
9391
|
+
|| m.kind === ts.SyntaxKind.ReadonlyKeyword)) {
|
|
9392
|
+
// a constructor parameter property is bound exactly as its parameter is
|
|
9393
|
+
const [ps, fs] = checker.getSymbolsOfParameterPropertyDeclaration(node, node.name.text);
|
|
9394
|
+
if (ps && fs) noteProp(fs, { same: ps });
|
|
9395
|
+
}
|
|
9396
|
+
// ⟨R935⟩ WRITES THE TABLE ABOVE CANNOT NAME: a reflective write into the object (`Object.assign(o, …)`,
|
|
9397
|
+
// `Object.defineProperty`, `Reflect.set`) or a computed-key store `o[k] = v` can rebind ANY of the
|
|
9398
|
+
// object's properties. Every eligible property of the target's type gets a non-environment binding,
|
|
9399
|
+
// so a location written that way can be MAY at best — a reader discloses `Unknown` rather than being
|
|
9400
|
+
// charged `Env` over a value that was replaced off the books (executed: an `Object.assign` rebind read
|
|
9401
|
+
// the replacement, and the first cut of this charged it `Env`).
|
|
9402
|
+
const reflectTarget = ts.isCallExpression(node)
|
|
9403
|
+
&& /^(Object\.(assign|defineProperty|defineProperties)|Reflect\.(set|defineProperty))$/.test(
|
|
9404
|
+
(ts.isPropertyAccessExpression(node.expression) ? node.expression.getText() : "").replace(/\s+/g, ""))
|
|
9405
|
+
? node.arguments[0]
|
|
9406
|
+
: (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
9407
|
+
&& ts.isElementAccessExpression(node.left) && node.left.argumentExpression
|
|
9408
|
+
&& !ts.isStringLiteralLike(node.left.argumentExpression) && !ts.isNumericLiteral(node.left.argumentExpression))
|
|
9409
|
+
? node.left.expression : null;
|
|
9410
|
+
if (reflectTarget) {
|
|
9411
|
+
let props = [];
|
|
9412
|
+
try { props = checker.getTypeAtLocation(reflectTarget)?.getProperties?.() ?? []; } catch { props = []; }
|
|
9413
|
+
for (const ps of props) { envDeclWritten.add(envLocKey(ps)); if (propEligible(ps)) noteProp(ps, null); }
|
|
9414
|
+
// ⟨R935⟩ a const object literal written reflectively: every member of the literal itself is rebound.
|
|
9415
|
+
const tl = envConstLiteral(unwrapArgExpr(reflectTarget));
|
|
9416
|
+
if (tl && ts.isObjectLiteralExpression(tl)) for (const p of tl.properties) {
|
|
9417
|
+
const k = (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) ? envLocKey(checker.getSymbolAtLocation(p.name)) : null;
|
|
9418
|
+
if (k) { propSyms.add(k); note(k, null); }
|
|
9419
|
+
}
|
|
9420
|
+
}
|
|
9421
|
+
// ⟨R935⟩ an ARRAY literal bound by a `const`: each element is a location holding its own value.
|
|
9422
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer
|
|
9423
|
+
&& (ts.getCombinedNodeFlags(node) & ts.NodeFlags.Const)) {
|
|
9424
|
+
const al = unwrapArgExpr(node.initializer);
|
|
9425
|
+
if (al && ts.isArrayLiteralExpression(al)) {
|
|
9426
|
+
for (const el of al.elements) if (!ts.isSpreadElement(el) && !ts.isOmittedExpression(el)) {
|
|
9427
|
+
propSyms.add(el); note(el, el); envLitElems.set(el, al);
|
|
9428
|
+
}
|
|
9429
|
+
}
|
|
8561
9430
|
}
|
|
8562
9431
|
ts.forEachChild(node, collect);
|
|
8563
9432
|
};
|
|
8564
9433
|
for (const sf of sources) collect(sf);
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
9434
|
+
// ⟨R935⟩ an array literal's elements are MUST only while every reference to the array is a literal-index READ.
|
|
9435
|
+
if (envLitElems.size) {
|
|
9436
|
+
const arrs = new Set(envLitElems.values());
|
|
9437
|
+
const names = new Set([...arrs].map((a) => a.parent?.name?.text).filter(Boolean));
|
|
9438
|
+
const opened = new Set();
|
|
9439
|
+
const visitRefs = (node) => {
|
|
9440
|
+
if (ts.isIdentifier(node) && names.has(node.text)) {
|
|
9441
|
+
const lit = envConstLiteral(node);
|
|
9442
|
+
if (lit && arrs.has(lit) && !opened.has(lit) && !(lit.parent?.name === node)) {
|
|
9443
|
+
let cur = node;
|
|
9444
|
+
while (cur.parent && (ts.isParenthesizedExpression(cur.parent) || ts.isNonNullExpression(cur.parent)
|
|
9445
|
+
|| ts.isAsExpression(cur.parent) || ts.isSatisfiesExpression?.(cur.parent))) cur = cur.parent;
|
|
9446
|
+
const p = cur.parent;
|
|
9447
|
+
const indexRead = p && ts.isElementAccessExpression(p) && p.expression === cur && p.argumentExpression
|
|
9448
|
+
&& ts.isNumericLiteral(p.argumentExpression)
|
|
9449
|
+
&& !(p.parent && ts.isBinaryExpression(p.parent) && p.parent.left === p && p.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && p.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
|
|
9450
|
+
&& !(p.parent && (ts.isDeleteExpression(p.parent) || ts.isPrefixUnaryExpression(p.parent) || ts.isPostfixUnaryExpression(p.parent)))
|
|
9451
|
+
&& !isDestructuringAssignTarget(p);
|
|
9452
|
+
const typeOnly = p && (ts.isTypeQueryNode(p) || ts.isExportSpecifier(p));
|
|
9453
|
+
if (!indexRead && !typeOnly) opened.add(lit);
|
|
9454
|
+
}
|
|
9455
|
+
}
|
|
9456
|
+
ts.forEachChild(node, visitRefs);
|
|
9457
|
+
};
|
|
9458
|
+
for (const sf of sources) visitRefs(sf);
|
|
9459
|
+
for (const [el, al] of envLitElems) if (opened.has(al)) note(el, null);
|
|
9460
|
+
}
|
|
9461
|
+
// ⟨R934⟩ the parameter bindings come from the shared PARAMETER PROVENANCE index above. What `collect` noted for
|
|
9462
|
+
// a parameter BEFORE this line is its in-body REASSIGNMENTS (`e = {}`); kept apart, because the two kinds of
|
|
9463
|
+
// binding answer different questions below.
|
|
9464
|
+
const paramAssigns = new Map();
|
|
9465
|
+
for (const [ps, vals] of paramArgs) {
|
|
9466
|
+
paramSyms.add(ps); paramAssigns.set(ps, [...(bindings.get(ps) ?? [])]);
|
|
9467
|
+
for (const v of vals) note(ps, v);
|
|
9468
|
+
}
|
|
9469
|
+
const leafIn = (leaf, set, propSet) => isProcessEnvExpr(leaf)
|
|
9470
|
+
|| (ts.isIdentifier(leaf) && set.has(checker.getSymbolAtLocation(leaf)))
|
|
9471
|
+
|| (!!propSet && propSet.has(memberSymOf(leaf)) && !(propSet === envPropMustSymbols && envLitDowngraded(leaf)));
|
|
9472
|
+
// ⟨R935⟩ a literal member read through a receiver whose DECLARED member is written elsewhere is MAY at best.
|
|
9473
|
+
function envLitDowngraded(n) {
|
|
9474
|
+
const lm = (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n)) ? envLiteralMember(n) : null;
|
|
9475
|
+
return !!lm && !!lm.declKey && lm.declKey !== lm.key && envDeclWritten.has(lm.declKey);
|
|
9476
|
+
}
|
|
9477
|
+
const hasEnvLeaf = (v, set, propSet) => v === ENV_DESTRUCTURED
|
|
9478
|
+
|| (!!v && typeof v === "object" && "same" in v ? set.has(v.same) || propSet?.has(v.same) === true
|
|
9479
|
+
: (!!v && typeof v === "object" && envValueLeaves(v).some((l) => leafIn(l, set, propSet))));
|
|
9480
|
+
|
|
9481
|
+
|
|
9482
|
+
// MAY: least fixpoint of "some binding has a leaf that is process.env or already MAY" — LOCALS, PROPERTIES
|
|
9483
|
+
// and (⟨R934⟩, see MUST below) PARAMETERS: a property's MAY reads a local's MAY and vice versa.
|
|
8569
9484
|
for (let changed = true, n = 0; changed && n < 64; n++) {
|
|
8570
9485
|
changed = false;
|
|
8571
9486
|
for (const [sym, vals] of bindings) {
|
|
8572
|
-
|
|
9487
|
+
const isProp = propSyms.has(sym);
|
|
9488
|
+
const set = isProp ? envPropMaySymbols : envMayAliasSymbols;
|
|
9489
|
+
if (!set.has(sym) && vals.some((v) => hasEnvLeaf(v, envMayAliasSymbols, envPropMaySymbols))) { set.add(sym); changed = true; }
|
|
8573
9490
|
}
|
|
8574
9491
|
}
|
|
8575
9492
|
// 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
|
-
|
|
9493
|
+
// least, so a cycle that never touches process.env directly stays MAY (the disclosing side). Locals and
|
|
9494
|
+
// properties start from their MAY sets.
|
|
9495
|
+
//
|
|
9496
|
+
// ⟨SOUNDNESS R934, the DIVERGENT half⟩ A PARAMETER IS THE ENVIRONMENT IN ITS READER'S FRAME WHEN *SOME* VISIBLE
|
|
9497
|
+
// CALL SITE HANDS IT THE ENVIRONMENT — not every one. The first cut of R934 required every site, citing pass
|
|
9498
|
+
// 2b's divergent-HOF ruling, and left `readDiv(e) { return e.SECRET }` called as `readDiv(process.env)` and
|
|
9499
|
+
// `readDiv({…})` ABSENT with `deny Env readDiv` exit 0 over a read that printed the planted secret (EXECUTED).
|
|
9500
|
+
// 2b's ruling does not transfer: it is about a CALLBACK, whose effect is another unit's body, so the HOF
|
|
9501
|
+
// performs nothing itself and pooling the callbacks onto it would invent an effect no execution of the HOF's
|
|
9502
|
+
// own code performs. Here the read is in the callee's OWN body, and an execution in which it reads the
|
|
9503
|
+
// environment exists — that is the all-paths merge `envValueLeaves` already charges for `(o ?? process.env).X`.
|
|
9504
|
+
// Pass 2c, in this same file, already answers the WRITE side this way (`envFed` is set by ANY site, and the
|
|
9505
|
+
// writer is charged `Env`), so the read side requiring every site was the same question answered twice.
|
|
9506
|
+
// The price is the one every summary pays: a caller handing the callee `{}` inherits `Env` through the
|
|
9507
|
+
// ordinary edge, exactly as a caller of 2c's `populate` and of an `??`-merging reader already does.
|
|
9508
|
+
// Because SOME suffices, whether the function escapes no longer matters — hidden sites can only add bindings.
|
|
9509
|
+
// An in-body REASSIGNMENT (`e = {}`) is the flow-insensitive rebind the local-alias rule exists for: the
|
|
9510
|
+
// parameter is then MUST only if every reassignment is also the environment, and otherwise MAY, which
|
|
9511
|
+
// discloses `Unknown[env-maybe-read]` on the reader rather than charging over a value that was replaced.
|
|
9512
|
+
const mustTry = () => {
|
|
9513
|
+
let changed = false;
|
|
8579
9514
|
for (const sym of envMayAliasSymbols) {
|
|
8580
|
-
if (
|
|
9515
|
+
if (paramSyms.has(sym)) continue;
|
|
9516
|
+
if (!envAliasSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) { envAliasSymbols.add(sym); changed = true; }
|
|
8581
9517
|
}
|
|
8582
|
-
|
|
9518
|
+
for (const sym of envPropMaySymbols) {
|
|
9519
|
+
if (!envPropMustSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) { envPropMustSymbols.add(sym); changed = true; }
|
|
9520
|
+
}
|
|
9521
|
+
for (const sym of paramSyms) {
|
|
9522
|
+
if (envAliasSymbols.has(sym)) continue;
|
|
9523
|
+
const must = (v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols);
|
|
9524
|
+
if (!(paramArgs.get(sym) ?? []).some(must) || !(paramAssigns.get(sym) ?? []).every(must)) continue;
|
|
9525
|
+
envAliasSymbols.add(sym); changed = true;
|
|
9526
|
+
}
|
|
9527
|
+
return changed;
|
|
9528
|
+
};
|
|
9529
|
+
for (let n = 0; mustTry() && n < 64; n++) { /* to fixpoint */ }
|
|
8583
9530
|
}
|
|
8584
9531
|
// True when `id` is an identifier resolving to a confirmed process.env alias local.
|
|
8585
9532
|
const identIsEnvAlias = (id) => {
|
|
@@ -8595,11 +9542,32 @@ const identIsEnvMayAlias = (id) => {
|
|
|
8595
9542
|
};
|
|
8596
9543
|
// The env kind of an expression's VALUE: "env" if some leaf is process.env or a MUST-alias, "may" if
|
|
8597
9544
|
// some leaf is only a MAY-alias, else null. The one answer the Env arm, 2c and the shared helpers ask.
|
|
9545
|
+
// ⟨R935⟩ a member reference `o.p` / `o["p"]` whose property is an environment location.
|
|
9546
|
+
const envMemberSym = (n) => {
|
|
9547
|
+
if (!(envPropMaySymbols.size > 0)) return null;
|
|
9548
|
+
const lm = envLiteralMember(n); // ⟨R935⟩ the object the receiver holds, before the member its type declares
|
|
9549
|
+
if (lm && envPropMaySymbols.has(lm.key)) return lm.key;
|
|
9550
|
+
if (ts.isPropertyAccessExpression(n)) return envLocKey(checker.getSymbolAtLocation(n.name));
|
|
9551
|
+
if (ts.isElementAccessExpression(n) && n.argumentExpression && ts.isStringLiteralLike(n.argumentExpression))
|
|
9552
|
+
return envLocKey(checker.getSymbolAtLocation(n.argumentExpression));
|
|
9553
|
+
return null;
|
|
9554
|
+
};
|
|
9555
|
+
const envPropKind = (n) => {
|
|
9556
|
+
const s = envMemberSym(n);
|
|
9557
|
+
if (!s || !envPropMaySymbols.has(s)) return null;
|
|
9558
|
+
if (!envPropMustSymbols.has(s)) return "may";
|
|
9559
|
+
// ⟨R935⟩ resolved through the receiver: MAY wherever the declared member is written off the literal.
|
|
9560
|
+
const lm = envLiteralMember(n);
|
|
9561
|
+
if (lm && lm.key === s && lm.declKey && lm.declKey !== s && envDeclWritten.has(lm.declKey)) return "may";
|
|
9562
|
+
return "env";
|
|
9563
|
+
};
|
|
8598
9564
|
const envValueKind = (expr) => {
|
|
8599
9565
|
let kind = null;
|
|
8600
9566
|
for (const l of envValueLeaves(expr)) {
|
|
8601
9567
|
if (isProcessEnvExpr(l) || identIsEnvAlias(l)) return "env";
|
|
8602
|
-
|
|
9568
|
+
const pk = envPropKind(l);
|
|
9569
|
+
if (pk === "env") return "env";
|
|
9570
|
+
if (identIsEnvMayAlias(l) || pk === "may") kind = "may";
|
|
8603
9571
|
}
|
|
8604
9572
|
return kind;
|
|
8605
9573
|
};
|
|
@@ -8612,8 +9580,21 @@ const envIsAssignOp = (k) => k >= ts.SyntaxKind.FirstAssignment && k <= ts.Synta
|
|
|
8612
9580
|
let envAliasNames = null;
|
|
8613
9581
|
const envRefKind = (node) => {
|
|
8614
9582
|
if (isProcessEnvExpr(node)) return "env";
|
|
9583
|
+
// ⟨R935⟩ a READ of an environment property location. Not an assignment target, a `delete` operand, a
|
|
9584
|
+
// destructuring target or a `++`/`--` operand: those write the location, they do not read the object.
|
|
9585
|
+
if (envPropMaySymbols.size > 0 && (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node))) {
|
|
9586
|
+
const pk = envPropKind(node);
|
|
9587
|
+
if (!pk) return null;
|
|
9588
|
+
const p = node.parent;
|
|
9589
|
+
if (p && ts.isBinaryExpression(p) && p.left === node && envIsAssignOp(p.operatorToken.kind)) return null;
|
|
9590
|
+
if (p && (ts.isDeleteExpression(p) || ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p))
|
|
9591
|
+
&& (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)))) return null;
|
|
9592
|
+
if (isDestructuringAssignTarget(node)) return null;
|
|
9593
|
+
return pk;
|
|
9594
|
+
}
|
|
8615
9595
|
if (!ts.isIdentifier(node)) return null;
|
|
8616
|
-
|
|
9596
|
+
// ⟨R934⟩ MUST-parameters are in `envAliasSymbols` but never in the MAY set, so the name filter reads both.
|
|
9597
|
+
envAliasNames ??= new Set([...envMayAliasSymbols, ...envAliasSymbols].map((s) => s.name));
|
|
8617
9598
|
if (!envAliasNames.has(node.text)) return null;
|
|
8618
9599
|
const p = node.parent;
|
|
8619
9600
|
if (!p) return null;
|
|
@@ -8628,8 +9609,9 @@ const envRefKind = (node) => {
|
|
|
8628
9609
|
&& (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)) return null;
|
|
8629
9610
|
const sym = ts.isShorthandPropertyAssignment(p) && p.name === node
|
|
8630
9611
|
? checker.getShorthandAssignmentValueSymbol(p) : checker.getSymbolAtLocation(node);
|
|
8631
|
-
if (!sym
|
|
8632
|
-
|
|
9612
|
+
if (!sym) return null;
|
|
9613
|
+
if (envAliasSymbols.has(sym)) return "env";
|
|
9614
|
+
return envMayAliasSymbols.has(sym) ? "may" : null;
|
|
8633
9615
|
};
|
|
8634
9616
|
// ⟨R928/R804⟩ Does the CONSUMER of this env value provably touch no key? Climb through what passes the
|
|
8635
9617
|
// value on unchanged (the transparent wrappers, both arms of `?:`, either side of `??`/`||`, the right of
|
|
@@ -8761,14 +9743,10 @@ const globalBuiltinCallee = (callee) => {
|
|
|
8761
9743
|
if (!callee || !ts.isPropertyAccessExpression(callee)) return null;
|
|
8762
9744
|
const member = callee.name?.text;
|
|
8763
9745
|
if (!member) return null;
|
|
8764
|
-
|
|
8765
|
-
while (owner && (ts.isParenthesizedExpression(owner) || ts.isAsExpression(owner)
|
|
8766
|
-
|| ts.isNonNullExpression(owner))) owner = owner.expression;
|
|
9746
|
+
const owner = unwrapArgExpr(callee.expression); // R780
|
|
8767
9747
|
if (ts.isIdentifier(owner)) return identIsGlobal(owner) ? `${owner.text}.${member}` : null;
|
|
8768
9748
|
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;
|
|
9749
|
+
const root = unwrapArgExpr(owner.expression); // R780
|
|
8772
9750
|
if (!ts.isIdentifier(root) || !GLOBAL_ROOTS.has(root.text) || !identIsGlobal(root)) return null;
|
|
8773
9751
|
return `${owner.name.text}.${member}`;
|
|
8774
9752
|
};
|
|
@@ -8805,9 +9783,7 @@ const globalBareCallee = (callee) => {
|
|
|
8805
9783
|
if (!callee) return null;
|
|
8806
9784
|
if (ts.isIdentifier(callee)) return identIsGlobal(callee) ? callee.text : null;
|
|
8807
9785
|
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;
|
|
9786
|
+
const root = unwrapArgExpr(callee.expression); // R780
|
|
8811
9787
|
// ONLY a global ROOT, never an arbitrary owner: `Object.assign` must not read as the bare global
|
|
8812
9788
|
// `assign`, and `globalThis.Object.assign` must not read as the bare global `Object` — both are the
|
|
8813
9789
|
// member helper's business, and answering them here would be the second copy §G exists to prevent.
|
|
@@ -9021,6 +9997,12 @@ function visitCalls(node) {
|
|
|
9021
9997
|
}
|
|
9022
9998
|
const sig = checker.getResolvedSignature(node);
|
|
9023
9999
|
let decl = sig && sig.declaration;
|
|
10000
|
+
// ⟨R1016⟩ a construction of a class expression runs its constructor unit, whatever the signature names.
|
|
10001
|
+
const classExprCtor = rec ? classExprCtorOfNew(node) : null;
|
|
10002
|
+
if (classExprCtor && classExprCtor !== owner) {
|
|
10003
|
+
rec.edges.add(classExprCtor);
|
|
10004
|
+
if (process.env.CANDOR_R1016_REACH) console.error(`R1016-REACH new ${owner} -> ${classExprCtor}`);
|
|
10005
|
+
}
|
|
9024
10006
|
// ⟨R103⟩ A WRITABLE SLOT IS AN INCOMPLETE CANDIDATE SET — see `openCallSlot`, and see the class-
|
|
9025
10007
|
// override fan-out below, which is the authority this converges on rather than a second rule: it
|
|
9026
10008
|
// edges to every candidate it CAN name and adds `Unknown` when the set it enumerated is not
|
|
@@ -9040,8 +10022,8 @@ function visitCalls(node) {
|
|
|
9040
10022
|
if (!decl) {
|
|
9041
10023
|
// `new C()` on a class with an IMPLICIT constructor resolves to no declaration — edge to
|
|
9042
10024
|
// the class's (synthesized) ctor unit via the class identifier before concluding Unknown.
|
|
9043
|
-
let edged =
|
|
9044
|
-
if (ts.isNewExpression(node) && node.expression && ts.isIdentifier(node.expression)) {
|
|
10025
|
+
let edged = !!classExprCtor, externalClass = false; // ⟨R1016⟩
|
|
10026
|
+
if (!edged && ts.isNewExpression(node) && node.expression && ts.isIdentifier(node.expression)) {
|
|
9045
10027
|
const cd = realDecl(checker.getSymbolAtLocation(node.expression));
|
|
9046
10028
|
const t = cd && nodeName.get(cd);
|
|
9047
10029
|
if (t) { rec.edges.add(t); edged = true; }
|
|
@@ -9143,8 +10125,95 @@ function visitCalls(node) {
|
|
|
9143
10125
|
// never a fabrication. (A LOCAL callee keeps its precise callback-flow below.)
|
|
9144
10126
|
const calleeName = ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text
|
|
9145
10127
|
: ts.isIdentifier(node.expression) ? node.expression.text : null;
|
|
9146
|
-
|
|
9147
|
-
|
|
10128
|
+
// ⟨SOUNDNESS R803⟩ a LISTED name keeps its position map; an UNLISTED non-local callee is admitted at the
|
|
10129
|
+
// positions its signature POSITIVELY declares callable, unless denylisted. See `HOF_REMOVAL_NAMES`.
|
|
10130
|
+
const hofListed = !!calleeName && HOF_INVOKERS.has(calleeName);
|
|
10131
|
+
const hofName = calleeName ?? decl.name?.getText?.() ?? null;
|
|
10132
|
+
const hofOpenAt = (!hofListed && mod !== "<local>" && node.arguments?.length
|
|
10133
|
+
&& !hofCalleeNeverInvokes(decl, hofName))
|
|
10134
|
+
? (i) => calleeParamIsCallable(node, i) === true
|
|
10135
|
+
&& !(declIsReviewedLib(decl) && calleeParamIsGenericSlot(node, i)) : null;
|
|
10136
|
+
// ⟨SOUNDNESS R1031⟩ THE SAME RULE ONE LEVEL DOWN: a function reference handed inside an OBJECT
|
|
10137
|
+
// LITERAL. rxjs `of(1).subscribe({ next: h })`, `{ complete: h }`, `{ error: h }`, `{ next }` and
|
|
10138
|
+
// `{ next: o.m.bind(o) }` were ABSENT with `deny Fs <caller>` exit 0 while the bare `subscribe(h)`
|
|
10139
|
+
// read `['Fs']` (EXECUTED: every handler that the observable fires wrote its file). R803 admits a
|
|
10140
|
+
// POSITION its signature declares callable; the observer object's MEMBERS are positions too, and
|
|
10141
|
+
// the signature declares them just as positively — `Partial<Observer<T>>`'s `next` is
|
|
10142
|
+
// `((value: T) => void) | undefined`. So each property whose value is a reference, at a member the
|
|
10143
|
+
// parameter's (non-callable) type arms declare callable, is handed to the SAME per-argument arm as
|
|
10144
|
+
// a bare reference — one implementation of "a reference the callee may invoke", not a second copy.
|
|
10145
|
+
// Inline arrows and methods in the literal were never dropped (they are structural units, charged
|
|
10146
|
+
// lexically). Same admission as R803's open path: an unlisted non-local callee off the denylist; an
|
|
10147
|
+
// `any`/`unknown`-typed parameter or member admits nothing (the R803 residual, unchanged).
|
|
10148
|
+
const objArgRefs = (argIdx, lit, depth = 0) => {
|
|
10149
|
+
if (depth > 2) return [];
|
|
10150
|
+
// The parameter's DECLARED type, not the call's instantiation of it: `Object.assign(p, { unsubscribe })`
|
|
10151
|
+
// instantiates `U` as `{ unsubscribe: () => void }`, which reads callable only because the CALL made it
|
|
10152
|
+
// so — the signature declares nothing about invoking it (measured: trpc's `Unpromise.then` charged
|
|
10153
|
+
// `callback:unsubscribe` until this read the declaration). A generic slot names no members, so it
|
|
10154
|
+
// admits nothing; `Partial<Observer<T>>` declares `next` callable whatever `T` is.
|
|
10155
|
+
let sd; try { sd = checker.getResolvedSignature?.(node)?.declaration; } catch { sd = null; }
|
|
10156
|
+
const dps = sd?.parameters;
|
|
10157
|
+
if (!dps?.length) return [];
|
|
10158
|
+
const dp = dps[Math.min(argIdx, dps.length - 1)];
|
|
10159
|
+
if (argIdx >= dps.length && !dp.dotDotDotToken) return [];
|
|
10160
|
+
if (!dp.type) return [];
|
|
10161
|
+
let pt; try { pt = checker.getTypeFromTypeNode(dp.type); } catch { pt = null; }
|
|
10162
|
+
if (dp.dotDotDotToken && pt) pt = checker.getIndexTypeOfType?.(pt, ts.IndexKind.Number) ?? null; // `...xs: T[]` → T
|
|
10163
|
+
return objLitRefsFor(pt, lit, depth);
|
|
10164
|
+
};
|
|
10165
|
+
const callableT = (t) => !!t && (t.getCallSignatures?.().length > 0 || (t.types ?? []).some((u) => u.getCallSignatures?.().length > 0));
|
|
10166
|
+
const objLitRefsFor = (pt, lit, depth) => {
|
|
10167
|
+
if (!pt || (pt.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))) return [];
|
|
10168
|
+
const arms = (pt.types ?? [pt]).filter((u) => !(u.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Any | ts.TypeFlags.Unknown))
|
|
10169
|
+
&& !(u.getCallSignatures?.().length > 0));
|
|
10170
|
+
const out = [];
|
|
10171
|
+
for (const pr of lit.properties) {
|
|
10172
|
+
const nm = pr.name && (ts.isIdentifier(pr.name) || ts.isStringLiteralLike(pr.name)) ? pr.name.text : null;
|
|
10173
|
+
if (!nm) continue;
|
|
10174
|
+
const memberTypes = arms.map((u) => { const ms = u.getProperty?.(nm); return ms ? checker.getTypeOfSymbolAtLocation(ms, lit) : null; }).filter(Boolean);
|
|
10175
|
+
if (ts.isShorthandPropertyAssignment(pr)) {
|
|
10176
|
+
if (memberTypes.some(callableT)) out.push({ shorthand: pr });
|
|
10177
|
+
continue;
|
|
10178
|
+
}
|
|
10179
|
+
if (!ts.isPropertyAssignment(pr)) continue; // a method / accessor is a structural unit already
|
|
10180
|
+
const v = unwrapArgExpr(pr.initializer);
|
|
10181
|
+
if (ts.isObjectLiteralExpression(v)) { for (const mt of memberTypes) out.push(...objLitRefsFor(mt, v, depth + 1)); continue; }
|
|
10182
|
+
if (memberTypes.some(callableT)) out.push({ ref: pr.initializer });
|
|
10183
|
+
}
|
|
10184
|
+
return out;
|
|
10185
|
+
};
|
|
10186
|
+
if (mod !== "<local>" && (hofListed || hofOpenAt)) {
|
|
10187
|
+
const hofArgArm = (a0, argIdx) => {
|
|
10188
|
+
// ⟨R1031⟩ an object literal at an admitted callee: its referenced members are the arguments.
|
|
10189
|
+
if (!hofListed) {
|
|
10190
|
+
const lit = unwrapArgExpr(a0);
|
|
10191
|
+
if (lit && ts.isObjectLiteralExpression(lit)) {
|
|
10192
|
+
// A property DESCRIPTOR installs its `get`/`set` and invokes neither; the access sites are
|
|
10193
|
+
// edged to the installed accessor by `definePropAccessors`. Charging the definer would be the
|
|
10194
|
+
// over-charge R803's generic-slot denylist exists for, one level down.
|
|
10195
|
+
if (declIsReviewedLib(decl) && OBJ_ARG_INSTALLS.has(hofOwnerMember(decl) ?? "")) return;
|
|
10196
|
+
for (const r of objArgRefs(argIdx, lit)) {
|
|
10197
|
+
if (r.ref) { HOF_REACH?.("objref", `${hofName}`); hofArgArm(r.ref, -1 - argIdx); continue; }
|
|
10198
|
+
// `{ next }` — the shorthand's VALUE symbol, followed one alias hop to what it holds.
|
|
10199
|
+
const vs = checker.getShorthandAssignmentValueSymbol(r.shorthand);
|
|
10200
|
+
const vd = vs?.valueDeclaration;
|
|
10201
|
+
HOF_REACH?.("objref", `${hofName}`);
|
|
10202
|
+
if (vd && nodeName.get(vd)) rec.edges.add(nodeName.get(vd));
|
|
10203
|
+
else if (vd && ts.isVariableDeclaration(vd) && vd.initializer && !ts.isArrowFunction(vd.initializer)
|
|
10204
|
+
&& !ts.isFunctionExpression(vd.initializer)) hofArgArm(vd.initializer, -1 - argIdx);
|
|
10205
|
+
else if (vd && ts.isVariableDeclaration(vd) && vd.initializer && nodeName.get(vd.initializer)) rec.edges.add(nodeName.get(vd.initializer));
|
|
10206
|
+
else if (!vd || ts.isParameter(vd) || ts.isVariableDeclaration(vd) || ts.isBindingElement(vd)) {
|
|
10207
|
+
rec.direct.add("Unknown"); rec.why.add(`callback:${r.shorthand.name.text.slice(0, 40)}`);
|
|
10208
|
+
}
|
|
10209
|
+
}
|
|
10210
|
+
return;
|
|
10211
|
+
}
|
|
10212
|
+
}
|
|
10213
|
+
// ⟨R1031⟩ a member reference re-entering the arm carries a NEGATIVE index: it is admitted by its
|
|
10214
|
+
// member's declared type above, and no position map or signature lookup applies to it.
|
|
10215
|
+
const memberRef = argIdx < 0;
|
|
10216
|
+
if (!memberRef && !hofListed && !hofOpenAt(argIdx)) return;
|
|
9148
10217
|
// R947 — a TYPE-ONLY wrapper is the same reference: `xs.forEach(fs.unlinkSync as any)`
|
|
9149
10218
|
// was dropped by the id/property-access gate below (no edge, no κ, no disclosure) while the bare
|
|
9150
10219
|
// spelling was not. `as`/`!`/`satisfies`/parentheses change no runtime value.
|
|
@@ -9159,7 +10228,7 @@ function visitCalls(node) {
|
|
|
9159
10228
|
// the same over-charge the by-reference arm below was fixed for and this arm was not.
|
|
9160
10229
|
// …but it must be the NEGATIVE test, not `!hofInvokesArg`: an unresolved or loosely-typed
|
|
9161
10230
|
// callee signature is not a licence to drop a bound callback. See `hofArgIsNeverCallback`.
|
|
9162
|
-
if (hofArgIsNeverCallback(calleeName, argIdx, node)) return;
|
|
10231
|
+
if (hofListed && hofArgIsNeverCallback(calleeName, argIdx, node)) return;
|
|
9163
10232
|
const bound = unwrapBind(a);
|
|
9164
10233
|
if (bound) {
|
|
9165
10234
|
const bref = bound.ref;
|
|
@@ -9212,7 +10281,8 @@ function visitCalls(node) {
|
|
|
9212
10281
|
if (!ts.isIdentifier(a) && !ts.isPropertyAccessExpression(a)) return;
|
|
9213
10282
|
const d2 = realDecl(checker.getSymbolAtLocation(a));
|
|
9214
10283
|
const t = (d2 && nodeName.get(d2)) || resolveFnRefUnit(a); // pin direct fn OR a local alias chain
|
|
9215
|
-
if (t) {
|
|
10284
|
+
if (t) { if (!hofListed) HOF_REACH?.("edge", `${hofName}->${t}`);
|
|
10285
|
+
rec.edges.add(t); return; } // resolvable named/local callback — keep its analyzed effect
|
|
9216
10286
|
// A DEPENDENCY function passed BY REFERENCE — `xs.forEach(depWrite)`, `setTimeout(dep.tick, 0)`.
|
|
9217
10287
|
// The invoking HOF calls it, so its effects are reachable here, and the dependency's report
|
|
9218
10288
|
// holds them under `<pkg>#depWrite`. Guard (2) below reasons that a ref resolving to a concrete
|
|
@@ -9238,7 +10308,7 @@ function visitCalls(node) {
|
|
|
9238
10308
|
// through the κ/invisible channel, so blanket-Unknown here would over-disclose them.
|
|
9239
10309
|
// (3) CALLABILITY — `argIsCallable` (has a call signature, or `any`/`unknown`/unconstrained
|
|
9240
10310
|
// generic that COULD hold a function).
|
|
9241
|
-
if (!hofInvokesArg(calleeName, argIdx, node)) return;
|
|
10311
|
+
if (hofListed && !hofInvokesArg(calleeName, argIdx, node)) return;
|
|
9242
10312
|
// ⟨SOUNDNESS R558⟩ …and BEFORE the by-reference dependency charge below, without returning:
|
|
9243
10313
|
// a FOREIGN interface member named as a value needs BOTH the visible-implementor join and
|
|
9244
10314
|
// the `invisible`/ledger disclosure `chargeExternalDecl` already gives it, and the two
|
|
@@ -9253,6 +10323,7 @@ function visitCalls(node) {
|
|
|
9253
10323
|
// dep VALUE with a call signature, passes. Guard (1) exists for exactly this shape and its own
|
|
9254
10324
|
// comment names the case (`path.reduce(fn, obj)`); the new arm simply ran before it.
|
|
9255
10325
|
if (d2 && !declIsLocal(d2) && argIsCallable(a)) {
|
|
10326
|
+
if (!hofListed) HOF_REACH?.("dep", `${hofName}->${a.getText().slice(0, 40)}`);
|
|
9256
10327
|
// R947 — A κ-CLASSIFIED BUILTIN PASSED BY REFERENCE. `chargeExternalDecl` is the
|
|
9257
10328
|
// dependency funnel and never asks κ, and `disclosureTail` treats a κ-KNOWN member as covered, so
|
|
9258
10329
|
// `xs.forEach(fs.unlinkSync)`, `setTimeout(fs.unlinkSync, 0, p)`, `.then(cp.execSync)` and
|
|
@@ -9275,11 +10346,24 @@ function visitCalls(node) {
|
|
|
9275
10346
|
const holderIsProjectValue = d2 && (ts.isParameter(d2) || ts.isVariableDeclaration(d2) || ts.isBindingElement(d2))
|
|
9276
10347
|
&& projectFiles.has(path.resolve(d2.getSourceFile().fileName));
|
|
9277
10348
|
if (!(holderIsProjectValue || !d2)) return;
|
|
10349
|
+
// ⟨SOUNDNESS R803⟩ a newly admitted callee handed its OWN caller's parameter (`reg(e, h) { e.on("y", h) }`)
|
|
10350
|
+
// is the callback flow's question, not an opaque value: the parameter is INVOKED, so pass 2b resolves it
|
|
10351
|
+
// to what every visible call site passed (an edge — `deny Fs` on the caller of `reg` fires) and keeps the
|
|
10352
|
+
// honest `callback:param#i` where a call site is opaque or none is visible. The listed names keep their
|
|
10353
|
+
// older `callback:<name>` disclosure — unchanged here so this fix removes nothing it did not add.
|
|
10354
|
+
if (!hofListed && d2 && ts.isParameter(d2) && d2.parent && nodeName.get(d2.parent) && argIsCallable(a)) {
|
|
10355
|
+
const ownerUnit = nodeName.get(d2.parent);
|
|
10356
|
+
HOF_REACH?.("param", `${hofName}->${a.getText().slice(0, 40)}`);
|
|
10357
|
+
(paramInvokes.get(ownerUnit) ?? paramInvokes.set(ownerUnit, new Set()).get(ownerUnit)).add(d2.parent.parameters.indexOf(d2));
|
|
10358
|
+
return;
|
|
10359
|
+
}
|
|
9278
10360
|
if (argIsCallable(a)) {
|
|
10361
|
+
if (!hofListed) HOF_REACH?.("opaque", `${hofName}->${a.getText().slice(0, 40)}`);
|
|
9279
10362
|
rec.direct.add("Unknown");
|
|
9280
10363
|
rec.why.add(`callback:${a.getText().replace(/\s+/g, "").slice(0, 40)}`); // opaque callable invoked by a sync HOF — canonical `callback:`
|
|
9281
10364
|
}
|
|
9282
|
-
}
|
|
10365
|
+
};
|
|
10366
|
+
node.arguments?.forEach(hofArgArm);
|
|
9283
10367
|
}
|
|
9284
10368
|
// `fn.call(thisArg, …)` / `fn.apply(thisArg, args)` INVOKE the receiver function reference, and
|
|
9285
10369
|
// `Reflect.apply(fn, …)` / `Reflect.construct(Ctor, …)` invoke their FIRST ARGUMENT. The resolved
|
|
@@ -9317,6 +10401,11 @@ function visitCalls(node) {
|
|
|
9317
10401
|
if ((m === "call" || m === "apply") && recvText !== "Reflect") invokedRef = recv;
|
|
9318
10402
|
else if (recvText === "Reflect" && (m === "apply" || m === "construct"))
|
|
9319
10403
|
invokedRef = (node.arguments ?? [])[0] ?? null;
|
|
10404
|
+
// ⟨SOUNDNESS R780⟩ `(k.m as F).call(k)`, `(f satisfies F).call(t)` and `Reflect.apply(f as F, …)` invoke what
|
|
10405
|
+
// the bare spelling invokes. A single wrapped reference is not a CHOICE, so R955's leaves (which do unwrap)
|
|
10406
|
+
// returned null and the id/property gate below saw a ParenthesizedExpression: no edge, no `Unknown`, the
|
|
10407
|
+
// caller ABSENT (EXECUTED, `fx/r780`).
|
|
10408
|
+
invokedRef = unwrapArgExpr(invokedRef);
|
|
9320
10409
|
// ⟨SOUNDNESS R587⟩ …and the ELEMENT-ACCESS spelling of the invoked reference, on the same
|
|
9321
10410
|
// grounds as the HOF-ref arm above: `i["roll"].call(null, n)` invokes exactly what
|
|
9322
10411
|
// `i.roll.call(null, n)` invokes. Measured ABSENT at `6a639e6` in every cell
|
|
@@ -9430,8 +10519,7 @@ function visitCalls(node) {
|
|
|
9430
10519
|
const recvExpr = (ts.isPropertyAccessExpression(node.expression)
|
|
9431
10520
|
|| ts.isElementAccessExpression(node.expression)) ? node.expression.expression : null;
|
|
9432
10521
|
const rootClass = localReceiverClass(recvExpr);
|
|
9433
|
-
const direct = rootClass ? allOverrides.filter((om) =>
|
|
9434
|
-
ts.isClassDeclaration(om.parent) && classInSubtree(om.parent, rootClass)) : allOverrides;
|
|
10522
|
+
const direct = rootClass ? allOverrides.filter((om) => classInSubtree(overrideOwner(om), rootClass)) : allOverrides;
|
|
9435
10523
|
const overrides = isSuperReceiver(recvExpr) ? [] : memberDispatchBodies(decl, rootClass);
|
|
9436
10524
|
if (isSuperReceiver(recvExpr)) probeR871("super-call", decl, direct.length);
|
|
9437
10525
|
else if (direct.length <= CHA_FANOUT_LIMIT) probeR871("class", decl, overrides.length - direct.length);
|
|
@@ -9760,6 +10848,22 @@ function visitCalls(node) {
|
|
|
9760
10848
|
rec.direct.add("Unknown");
|
|
9761
10849
|
rec.why.add(`native:Storage.${name}`);
|
|
9762
10850
|
}
|
|
10851
|
+
// SOUNDNESS R241 — THE REST OF THE BROWSER'S PERSISTENT STORES AND NAVIGATION, which the comment
|
|
10852
|
+
// above named as "untouched and still pure" (the documented-limitation shape: writing it down is
|
|
10853
|
+
// what stopped it being measured). IndexedDB, the Cache API and the Origin Private File System
|
|
10854
|
+
// persist across sessions exactly as `Storage` does, and `location.assign`/`replace`/`reload`
|
|
10855
|
+
// navigate (a request leaves the page). All six of the row's spellings read ABSENT under
|
|
10856
|
+
// `lib: ["ES2022","DOM"]`, while `localStorage.setItem` on the same tree read `Unknown[native:…]`.
|
|
10857
|
+
// Same answer as `Storage`, for the same reason: the WHOLE interface (the denylist direction — an
|
|
10858
|
+
// unlisted member would be silent), `Unknown` because the backing store / navigation target is not
|
|
10859
|
+
// modelled, keyed on the RESOLVED DECLARATION'S PARENT so a project's own `class Cache` resolves
|
|
10860
|
+
// `<local>` and never reaches this arm. Reach on the (Node/server) roster is zero by construction;
|
|
10861
|
+
// the price is paid only by code that already touches these hosts.
|
|
10862
|
+
if (BROWSER_STORE_IFACES.has(parent)
|
|
10863
|
+
|| (parent === "Location" && (name === "assign" || name === "replace" || name === "reload"))) {
|
|
10864
|
+
rec.direct.add("Unknown");
|
|
10865
|
+
rec.why.add(`native:${parent}.${name}`);
|
|
10866
|
+
}
|
|
9763
10867
|
// `new EventSource(url)` / `new WebSocket(url)`: the constructor is declared on an anonymous
|
|
9764
10868
|
// `declare var` object type (symbol `__type`, no usable parent name), but reaching the es-lib
|
|
9765
10869
|
// branch already proves the ctor resolved to lib.dom (not a project class shadowing the name),
|
|
@@ -9997,6 +11101,22 @@ function visitCalls(node) {
|
|
|
9997
11101
|
&& (ts.isPropertyAccessExpression(node.expression) || ts.isElementAccessExpression(node.expression))
|
|
9998
11102
|
&& receiverIsProvenNonNetworkStream(node.expression.expression))
|
|
9999
11103
|
{ eff = null; effSuppressed = true; }
|
|
11104
|
+
// ⟨SOUNDNESS R966⟩ A FRAMEWORK'S OWN `listen` IS AN ACCEPT. `NET_ACCEPTING` is asked only of a call κ
|
|
11105
|
+
// classified `Net`, and κ classifies node's servers — so express `app.listen(p)` (declared in
|
|
11106
|
+
// express-serve-static-core), koa `app.listen(p)` and fastify `f.listen({port})` reached neither the effect
|
|
11107
|
+
// nor the accept mark: beside `net.connect(80, "ok.example")`, `allow Net in <fn> ok.example` exited 0
|
|
11108
|
+
// over a server answering anyone (EXECUTED: a client got the handler's reply). An inclusion list decided a
|
|
11109
|
+
// fail-closed question (R781's lesson), so this is the fail-closed reading: a DEPENDENCY member named
|
|
11110
|
+
// `listen` that κ did not answer is a network accept — `Net`, and `netAccepting` marks it `incomplete`.
|
|
11111
|
+
// The failure direction is an over-charge on a non-network `listen`; the one family known to be that is
|
|
11112
|
+
// denylisted by package (a JSON-RPC `connection.listen()` starts reading a transport whose OWN
|
|
11113
|
+
// construction decides whether it is a socket). Measured reach on the pinned roster: 1 unclassified
|
|
11114
|
+
// dependency `listen` (nest's FastifyAdapter), 0 non-network. Not seen: hapi `server.start()`
|
|
11115
|
+
// (`start` is p-queue's and repl's too), Bun.serve/Deno.serve (no types in any roster tree).
|
|
11116
|
+
if (!eff && !effSuppressed && member === "listen" && dependencyListenIsAccept(mod, decl)) {
|
|
11117
|
+
eff = "Net";
|
|
11118
|
+
if (process.env.CANDOR_R966_REACH) console.error(`R966-REACH ${mod}.listen`);
|
|
11119
|
+
}
|
|
10000
11120
|
if (eff) {
|
|
10001
11121
|
rec.direct.add(eff);
|
|
10002
11122
|
// SPEC §2 `fs` — refine an Fs we just PROVED with the direction its verb implies. DIRECT only
|
|
@@ -10327,7 +11447,16 @@ function visitCalls(node) {
|
|
|
10327
11447
|
// `const [,,x] = process.argv`, or handing it to a parser all reach the same process-startup state.
|
|
10328
11448
|
// Marked on the expression itself so every idiom counts without enumerating them.
|
|
10329
11449
|
if (isProcessArgvExpr(node)) markEnv();
|
|
11450
|
+
// ⟨R934⟩ a DESTRUCTURED parameter that every visible call site hands the environment reads its keys
|
|
11451
|
+
// as the frame is entered — the destructuring IS the read, in this function's frame.
|
|
11452
|
+
if (ts.isParameter(node) && ts.isObjectBindingPattern(node.name) && envAliasSymbols.has(node)) { markEnv(); ENVLOC_REACH?.("param-pattern env"); }
|
|
10330
11453
|
const kind = envRefKind(node);
|
|
11454
|
+
if (kind && ENVLOC_REACH && !envConsumerInert(node)) {
|
|
11455
|
+
const vd = ts.isIdentifier(node) ? checker.getSymbolAtLocation(node)?.valueDeclaration : null;
|
|
11456
|
+
const where = `${path.relative(process.cwd(), node.getSourceFile().fileName)}:${node.getStart() >= 0 ? node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).line + 1 : 0}`;
|
|
11457
|
+
if (vd && ts.isParameter(vd)) ENVLOC_REACH(`param ${kind} ${where}`);
|
|
11458
|
+
else if (!ts.isIdentifier(node) && !isProcessEnvExpr(node)) ENVLOC_REACH(`prop ${kind} ${where}`);
|
|
11459
|
+
}
|
|
10331
11460
|
if (kind && !envConsumerInert(node)) {
|
|
10332
11461
|
if (kind === "env") markEnv();
|
|
10333
11462
|
else {
|
|
@@ -10368,6 +11497,24 @@ function visitCalls(node) {
|
|
|
10368
11497
|
&& isWebStorageExpr(node.expression)) {
|
|
10369
11498
|
markStore(memberLabel(node));
|
|
10370
11499
|
}
|
|
11500
|
+
// SOUNDNESS R241 — the two PROPERTY spellings of the same host boundary, which no call arm sees: the
|
|
11501
|
+
// cookie jar (`document.cookie`, read OR written — it is a persistent store, `Storage`'s posture) and a
|
|
11502
|
+
// WRITE to any `Location` property (`location.href = u` navigates; so do `pathname`/`search`/`hash`).
|
|
11503
|
+
// lib.dom declares both as DATA properties, so the accessor arm finds nothing either. Keyed on the
|
|
11504
|
+
// resolved declaration living in the TypeScript lib and on its declaring interface, never on text.
|
|
11505
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
11506
|
+
const hsym = checker.getSymbolAtLocation(node.name);
|
|
11507
|
+
const hdecl = (hsym?.declarations ?? []).find((d) => /typescript\/lib\/lib\..*\.d\.ts$/.test(path.resolve(d.getSourceFile().fileName).replace(/\\/g, "/")));
|
|
11508
|
+
const iface = hdecl?.parent && ts.isInterfaceDeclaration(hdecl.parent) ? hdecl.parent.name.text : null;
|
|
11509
|
+
const pn = node.name.text;
|
|
11510
|
+
const hp = node.parent;
|
|
11511
|
+
const written = (hp && ts.isBinaryExpression(hp) && hp.left === node && envIsAssignOp(hp.operatorToken.kind))
|
|
11512
|
+
|| isDestructuringAssignTarget(node);
|
|
11513
|
+
if ((iface === "Document" && pn === "cookie") || (iface === "Location" && written)) {
|
|
11514
|
+
const owner = enclosing(node);
|
|
11515
|
+
if (owner) { const r = fns.get(owner); r.direct.add("Unknown"); r.why.add(`native:${iface}.${pn}`); }
|
|
11516
|
+
}
|
|
11517
|
+
}
|
|
10371
11518
|
else if (ts.isVariableDeclaration(node) && node.name && ts.isObjectBindingPattern(node.name)
|
|
10372
11519
|
&& node.initializer && isWebStorageExpr(node.initializer)) {
|
|
10373
11520
|
markStore("<destructure>");
|
|
@@ -10788,6 +11935,36 @@ function visitCalls(node) {
|
|
|
10788
11935
|
&& p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && p.operatorToken.kind <= ts.SyntaxKind.LastAssignment;
|
|
10789
11936
|
const recordKind = (kind) => {
|
|
10790
11937
|
const hits = accessorsAt(node, kind);
|
|
11938
|
+
// ⟨R246/R1039⟩ THE RESIDUE: a WRITE (R246) or READ (R1039) through a receiver whose static type is
|
|
11939
|
+
// `any`/`unknown` and whose value neither the assertion look-through nor the parameter provenance could
|
|
11940
|
+
// name. Recorded here; whether it is disclosed is decided after the fixpoint, when it is known which
|
|
11941
|
+
// project accessors of that kind have effects.
|
|
11942
|
+
if (!(hits && hits.length)) {
|
|
11943
|
+
let rt1; try { rt1 = checker.getTypeAtLocation(node.expression); } catch { rt1 = null; }
|
|
11944
|
+
if (anyishType(rt1) && !receiverValueTypes(node.expression).length) {
|
|
11945
|
+
const owner1 = enclosing(node);
|
|
11946
|
+
const nm1 = ts.isPropertyAccessExpression(node) ? node.name.text
|
|
11947
|
+
: (node.argumentExpression && ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null);
|
|
11948
|
+
if (owner1) anyRecvSetResidue.push({ owner: owner1, name: nm1, kind });
|
|
11949
|
+
}
|
|
11950
|
+
}
|
|
11951
|
+
if (R246_REACH) {
|
|
11952
|
+
let rt0; try { rt0 = checker.getTypeAtLocation(node.expression); } catch { rt0 = null; }
|
|
11953
|
+
if (anyishType(rt0) || peelAssertions(node.expression) !== node.expression) {
|
|
11954
|
+
const vts = receiverValueTypes(node.expression);
|
|
11955
|
+
const nm = ts.isPropertyAccessExpression(node) ? node.name.text
|
|
11956
|
+
: (node.argumentExpression && ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null);
|
|
11957
|
+
if (hits && hits.length && vts.length) R246_REACH(`resolved ${kind}`);
|
|
11958
|
+
else if (anyishType(rt0) && !vts.length) {
|
|
11959
|
+
projAccessorNames ??= (() => { const m = { get: new Set(), set: new Set() };
|
|
11960
|
+
const v = (n) => { if ((ts.isGetAccessorDeclaration(n) || ts.isSetAccessorDeclaration(n)) && n.name && !ts.isComputedPropertyName(n.name))
|
|
11961
|
+
(ts.isGetAccessorDeclaration(n) ? m.get : m.set).add(n.name.getText()); ts.forEachChild(n, v); };
|
|
11962
|
+
for (const sf of sources) v(sf); return m; })();
|
|
11963
|
+
const could = nm == null ? projAccessorNames[kind].size > 0 : projAccessorNames[kind].has(nm);
|
|
11964
|
+
R246_REACH(`residue ${kind} ${nm == null ? "dyn" : "named"} ${could ? "could-hit-project-accessor" : "no-project-accessor"}`);
|
|
11965
|
+
}
|
|
11966
|
+
}
|
|
11967
|
+
}
|
|
10791
11968
|
if (hits && hits.length) {
|
|
10792
11969
|
const owner = enclosing(node);
|
|
10793
11970
|
if (!owner) return;
|
|
@@ -10864,7 +12041,10 @@ function visitCalls(node) {
|
|
|
10864
12041
|
// the four symbol rows below); what changed is that the other spelling now asks the same code.
|
|
10865
12042
|
const keyT = ts.isElementAccessExpression(node) && node.argumentExpression
|
|
10866
12043
|
? checker.getTypeAtLocation(node.argumentExpression) : null;
|
|
10867
|
-
|
|
12044
|
+
// R246 — the VALUE's types too: `(s as any)[k] = v`, or `o[k] = v` with `o: any` fed an `S`.
|
|
12045
|
+
const rprops = [...(rt?.getProperties?.() ?? [])];
|
|
12046
|
+
for (const vt of receiverValueTypes(node.expression)) rprops.push(...(vt.getProperties?.() ?? []));
|
|
12047
|
+
for (const prop of rprops) {
|
|
10868
12048
|
if (!accessorsFromSym(prop, kind).length) continue;
|
|
10869
12049
|
if (!keyCouldNameAccessor(prop, keyT)) continue;
|
|
10870
12050
|
const owner = enclosing(node);
|
|
@@ -11282,17 +12462,39 @@ for (const sf of sources) visitCalls(sf);
|
|
|
11282
12462
|
})(cls);
|
|
11283
12463
|
for (const dec of decorators) {
|
|
11284
12464
|
add(localBodiedUnit(dec), "deco"); // the application
|
|
11285
|
-
|
|
11286
|
-
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
|
12465
|
+
const e = unwrapArgExpr(dec.expression); // R780
|
|
11287
12466
|
if (ts.isCallExpression(e)) {
|
|
11288
12467
|
add(localBodiedUnit(e), "deco"); // the factory call
|
|
11289
12468
|
add(`${moduleOf(e.getSourceFile())}.${decoratorArgLocal(e)}`, "arg"); // ⟨R944⟩ the ONE spelling
|
|
11290
12469
|
}
|
|
11291
12470
|
}
|
|
11292
12471
|
for (const m of cls.members ?? [])
|
|
11293
|
-
if (ts.isClassStaticBlockDeclaration(m)
|
|
12472
|
+
if (ts.isClassStaticBlockDeclaration(m)
|
|
12473
|
+
|| (ts.isPropertyDeclaration(m) && m.initializer && (ts.getCombinedModifierFlags(m) & ts.ModifierFlags.Static)))
|
|
12474
|
+
add(staticBlockQual(m), "static"); // R815: a static field initialiser shares the block's unit
|
|
12475
|
+
// R815: the `extends <expr>` unit — run by the definer (here) and reached by every construction (`super`).
|
|
12476
|
+
const ctorQ = nodeName.get(cls);
|
|
12477
|
+
const herQ = ctorQ && ctorQ.endsWith(".constructor") ? ctorQ.replace(/\.constructor$/, ".<heritage>") : null;
|
|
12478
|
+
// Wired only when the expression attributed something: a bare `extends Base` mints the unit (a reference was
|
|
12479
|
+
// attributed) and carries nothing, and an edge to an empty unit is a `calls` entry on every subclass for no answer.
|
|
12480
|
+
const herR = herQ && fns.get(herQ);
|
|
12481
|
+
if (herR && ["direct", "edges", "why", "blind", "hosts", "paths", "cmds", "incomplete", "dispatch", "tables"]
|
|
12482
|
+
.some((k) => herR[k]?.size)) { add(herQ, "heritage"); fns.get(ctorQ)?.edges.add(herQ); }
|
|
12483
|
+
// ⟨R1016⟩ the containment edge to a class expression's constructor, unless its value cannot escape a `new`.
|
|
12484
|
+
// Asked from the class itself rather than its parent, so a `static X = class {…}` lands on `<static-init>`.
|
|
12485
|
+
const ceq = ts.isClassExpression(cls) ? classExprCtorQual.get(cls) : null;
|
|
12486
|
+
if (ceq && classExprEscapes(cls)) {
|
|
12487
|
+
const cfrom = enclosing(cls);
|
|
12488
|
+
const crec = cfrom && cfrom !== ceq ? fns.get(cfrom) : null;
|
|
12489
|
+
if (crec && !crec.edges.has(ceq)) { crec.edges.add(ceq); hit("classexpr"); }
|
|
12490
|
+
} else if (ceq && process.env.CANDOR_R1016_REACH) console.error(`R1016-REACH contained-dropped ${ceq}`);
|
|
11294
12491
|
if (!targets.length) continue;
|
|
11295
|
-
|
|
12492
|
+
// ⟨SOUNDNESS R1060⟩ a class EXPRESSION is asked from itself, as the R1016 edge above is: climbing from its PARENT
|
|
12493
|
+
// starts at a `static X = class {…}` property with no child in hand, so R815's static-initialiser rule never
|
|
12494
|
+
// matched and the inner class's definition-time work was wired from the OUTER class's constructor — `new HB()`
|
|
12495
|
+
// charged for a `static {}` block that runs once, at import (EXECUTED), and the module that runs it silent.
|
|
12496
|
+
// A declaration is still asked from its parent: from itself, `nodeName` would answer its own constructor.
|
|
12497
|
+
const from = ts.isClassExpression(cls) ? enclosing(cls) : enclosing(cls.parent);
|
|
11296
12498
|
const rec = from && fns.get(from);
|
|
11297
12499
|
if (!rec) continue;
|
|
11298
12500
|
for (const [q, k] of targets) if (q !== from && !rec.edges.has(q)) { rec.edges.add(q); hit(k); }
|
|
@@ -11797,6 +12999,52 @@ const inferred = new Map([...fns.keys()].map((k) => [k, new Set(fns.get(k).direc
|
|
|
11797
12999
|
if (!queued.has(c)) { queued.add(c); queue.push(c); }
|
|
11798
13000
|
}
|
|
11799
13001
|
}
|
|
13002
|
+
// ⟨SOUNDNESS R246⟩ THE HEDGE FOR WHAT RESOLUTION CANNOT REACH. A write `o.name = v` / `o[k] = v` through a
|
|
13003
|
+
// receiver typed `any`/`unknown` whose value neither an assertion nor a visible call site names — an escaped
|
|
13004
|
+
// function's parameter, an exported one with no visible caller, an `any` field or return — can still land
|
|
13005
|
+
// on a project SETTER (EXECUTED: `[new Sx()].forEach(w)` with `w(o: any) { o.token = v }` ran Sx's setter, and
|
|
13006
|
+
// `w` was ABSENT, `deny Fs`/`deny Unknown` both exit 0). Which object it is cannot be known, so this
|
|
13007
|
+
// DISCLOSES rather than charges: `Unknown[reflect:accessor:any-receiver]` on the writer, and only when the
|
|
13008
|
+
// project declares a setter that could be the one — the SAME name (or any string-named setter for an
|
|
13009
|
+
// unpinnable key) — AND that setter's body has an effect. A setter-free or pure-setter project pays nothing.
|
|
13010
|
+
// Priced on the 28-entry roster (see the commit for the measured count — a handful of units, <0.1%).
|
|
13011
|
+
// SOUNDNESS R1039 — THE READ SIDE, SAME RULE: `o.name` / `o[k]` through such a receiver can run a project
|
|
13012
|
+
// GETTER (EXECUTED: an escaped `r(o: any) { return o.token }` handed a class whose `get token()` writes a
|
|
13013
|
+
// file — `r` ABSENT, `deny Unknown`/`deny Fs Unknown` exit 0 on v0.40.0). Same admission: a getter WITH A
|
|
13014
|
+
// BODY and an EFFECT that the read could name (the same name, or any string-named one for an unpinnable key).
|
|
13015
|
+
if (anyRecvSetResidue.length) {
|
|
13016
|
+
const accUnits = { get: new Map(), set: new Map() }; // kind -> name -> [unit]
|
|
13017
|
+
const visitS = (n) => {
|
|
13018
|
+
// An accessor WITH A BODY: a body-less declaration (a project typings stub) is already its own `native:`
|
|
13019
|
+
// disclosure, and arming every `any` write on it measured as pure noise (typeorm's bson `ObjectId.set id`).
|
|
13020
|
+
if ((ts.isSetAccessorDeclaration(n) || ts.isGetAccessorDeclaration(n)) && n.body && n.name && !ts.isComputedPropertyName(n.name)) {
|
|
13021
|
+
const u = nodeName.get(n);
|
|
13022
|
+
const m = ts.isSetAccessorDeclaration(n) ? accUnits.set : accUnits.get;
|
|
13023
|
+
if (u) { const k = n.name.getText(); (m.get(k) ?? m.set(k, []).get(k)).push(u); }
|
|
13024
|
+
}
|
|
13025
|
+
ts.forEachChild(n, visitS);
|
|
13026
|
+
};
|
|
13027
|
+
for (const sf of sources) visitS(sf);
|
|
13028
|
+
const effectful = (u) => (inferred.get(u)?.size ?? 0) > 0;
|
|
13029
|
+
const anyEffectful = { get: [...accUnits.get.values()].flat().some(effectful), set: [...accUnits.set.values()].flat().some(effectful) };
|
|
13030
|
+
const hedged = new Set();
|
|
13031
|
+
for (const { owner, name, kind } of anyRecvSetResidue) {
|
|
13032
|
+
const hit = name == null ? anyEffectful[kind] : (accUnits[kind].get(name) ?? []).some(effectful);
|
|
13033
|
+
if (!hit || !fns.has(owner)) continue;
|
|
13034
|
+
const rec = fns.get(owner);
|
|
13035
|
+
rec.direct.add("Unknown"); rec.why.add("reflect:accessor:any-receiver");
|
|
13036
|
+
hedged.add(owner);
|
|
13037
|
+
}
|
|
13038
|
+
// propagate the added `Unknown` to every transitive caller (the fixpoint above has already run)
|
|
13039
|
+
const queue = [...hedged];
|
|
13040
|
+
const seenU = new Set(queue);
|
|
13041
|
+
for (let head = 0; head < queue.length; head++) {
|
|
13042
|
+
const u = queue[head];
|
|
13043
|
+
inferred.get(u)?.add("Unknown");
|
|
13044
|
+
for (const c of callersOf.get(u) ?? [])
|
|
13045
|
+
if (!seenU.has(c) && !inferred.get(c)?.has("Unknown")) { seenU.add(c); queue.push(c); }
|
|
13046
|
+
}
|
|
13047
|
+
}
|
|
11800
13048
|
// `fsKinds` joins the propagated surfaces: kinds TRAVEL the call graph (a caller that transitively only
|
|
11801
13049
|
// writes IS a writer), and the "?" poison travels with them so a caller of an undetermined-kind function
|
|
11802
13050
|
// inherits the SUPPRESSION rather than a half-answer. Pinned by conformance PART 31.
|