candor-ts 0.40.1 → 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/scan.mjs +421 -46
package/README.md
CHANGED
|
@@ -185,6 +185,20 @@ functions analyzed): its DI-style fs injection means many functions read `Unknow
|
|
|
185
185
|
that's the contract working, not noise. The report says "can reach", never "does"; an absent
|
|
186
186
|
literal is never a claim of absence.
|
|
187
187
|
|
|
188
|
+
**Framework dependency injection (spec §2's bounded-CHA clause).** rimraf's injection is a function-valued
|
|
189
|
+
parameter, which reads `Unknown`. A DI *container* is measured separately, on NestJS 10 with an executed fixture:
|
|
190
|
+
|
|
191
|
+
- a provider entry `{ provide: Store, useClass | useValue | useFactory | useExisting: … }` whose token is a project
|
|
192
|
+
CLASS is read as a conversion: the substitute joins `Store`'s candidates, so `this.store.save()` is charged the
|
|
193
|
+
union of `Store.save` and the substitute's `save` (SOUNDNESS R1061 — silent before, all three forms executed);
|
|
194
|
+
- a parameter typed by an INTERFACE (`@Inject("SINK") sink: Sink`) dispatches over the project's visible
|
|
195
|
+
implementors of `Sink`, so a `useClass` that `implements Sink` is charged; a `useValue` object literal that
|
|
196
|
+
implements nothing reads `Unknown` (`dispatch:`).
|
|
197
|
+
|
|
198
|
+
**Named miss:** a provider list the scan cannot see (built at runtime, or imported from an unscanned package), a
|
|
199
|
+
token that is a variable rather than the class itself, and a substitute supplied by a dependency are not
|
|
200
|
+
substitutions to this engine. Gate a DI-heavy service with `deny <E> Unknown` as well as `deny <E>`.
|
|
201
|
+
|
|
188
202
|
## Cross-engine consistency — machine-checked
|
|
189
203
|
|
|
190
204
|
candor-ts is one of the **four code engines** (with the reference engine candor-java, the Rust
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "candor-ts",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.2",
|
|
4
4
|
"mcpName": "io.github.tombaldwin/candor",
|
|
5
5
|
"description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.40)",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@types/node": "
|
|
9
|
-
"typescript": "
|
|
8
|
+
"@types/node": "25.9.2",
|
|
9
|
+
"typescript": "6.0.3"
|
|
10
10
|
},
|
|
11
11
|
"bin": {
|
|
12
12
|
"candor-ts": "./scan.mjs",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"lint": "eslint *.mjs",
|
|
24
|
+
"prepack": "node -e \"require('fs').copyFileSync('package-lock.json','npm-shrinkwrap.json')\"",
|
|
25
|
+
"postpack": "node -e \"require('fs').rmSync('npm-shrinkwrap.json',{force:true})\"",
|
|
24
26
|
"test": "npm run lint && node --test test-unit.mjs && node test.mjs --parallel && node test-mcp.mjs && node test-lsp.mjs && node test-watch.mjs && npm run test:probe && npm run test:fuzz",
|
|
25
27
|
"test:unit": "node --test test-unit.mjs",
|
|
26
28
|
"test:probe": "node fabrication_probe.mjs",
|
|
@@ -45,6 +47,7 @@
|
|
|
45
47
|
"node": ">=20"
|
|
46
48
|
},
|
|
47
49
|
"files": [
|
|
50
|
+
"npm-shrinkwrap.json",
|
|
48
51
|
"Cases.ts",
|
|
49
52
|
"scan.mjs",
|
|
50
53
|
"query.mjs",
|
package/scan.mjs
CHANGED
|
@@ -1653,11 +1653,60 @@ if (NO_SOURCES) {
|
|
|
1653
1653
|
// types). Resolved via the module system, NOT a fixed relative path — npm HOISTS dependencies, so
|
|
1654
1654
|
// in an npx/install tree @types/node sits BESIDE candor-ts, not inside it (the second probe's
|
|
1655
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
|
+
}
|
|
1656
1687
|
if (!compilerOptions.typeRoots) {
|
|
1657
1688
|
const roots = [path.join(rootDir, "node_modules", "@types")];
|
|
1658
1689
|
try {
|
|
1659
1690
|
const req = createRequire(path.join(ENGINE_DIR, "scan.mjs"));
|
|
1660
|
-
|
|
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
|
+
}
|
|
1661
1710
|
} catch {}
|
|
1662
1711
|
compilerOptions.typeRoots = roots;
|
|
1663
1712
|
}
|
|
@@ -4023,6 +4072,7 @@ const fns = new Map(); // qualified name -> { direct, edges, hosts, ta
|
|
|
4023
4072
|
const bodylessDecls = new Map(); // qual -> {node, mod, abstract, owner, member}
|
|
4024
4073
|
const unlistedSeen = new Map(); // the κ-coverage ledger: unlisted npm package -> call-site count
|
|
4025
4074
|
const nodeName = new WeakMap(); // declaration node -> qualified name
|
|
4075
|
+
const classExprCtorQual = new Map(); // ⟨R1016⟩ ClassExpression -> its constructor unit (see `mintClassExprCtor`)
|
|
4026
4076
|
// ⟨scan-boundary, export-alias⟩ `module.exports = { thing: _internalImplName }` / `exports.thing =
|
|
4027
4077
|
// _internalImplName` — a RENAMED re-export whose RHS NAMES an EXISTING declaration rather than being an
|
|
4028
4078
|
// inline function. `localName` mints a CJS unit for `module.exports = function(){}` and
|
|
@@ -4563,6 +4613,7 @@ const STABLE_KEY_SHAPES = {
|
|
|
4563
4613
|
callable: new Set([ts.SyntaxKind.ArrowFunction, ts.SyntaxKind.FunctionExpression]),
|
|
4564
4614
|
decorator: new Set([ts.SyntaxKind.ArrowFunction, ts.SyntaxKind.FunctionExpression]),
|
|
4565
4615
|
"decorator-arg": new Set([ts.SyntaxKind.CallExpression]),
|
|
4616
|
+
classexpr: new Set([ts.SyntaxKind.ClassExpression]), // ⟨R1016⟩ a class expression's constructor unit
|
|
4566
4617
|
computed: null, // a descriptor member of any shape — counted over EVERY node under the anchor
|
|
4567
4618
|
};
|
|
4568
4619
|
const stableKeySanitise = (s) => s.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
@@ -5134,6 +5185,7 @@ for (const sf of sources) {
|
|
|
5134
5185
|
// (a wholly-unresolvable target leaves only the minted unit — nothing to join a forcing site to.)
|
|
5135
5186
|
}
|
|
5136
5187
|
}
|
|
5188
|
+
if (ts.isClassExpression(node)) mintClassExprCtor(node); // ⟨R1016⟩
|
|
5137
5189
|
ts.forEachChild(node, collect);
|
|
5138
5190
|
})(sf);
|
|
5139
5191
|
}
|
|
@@ -5265,6 +5317,93 @@ for (const { mod, name, ident } of exportAliasCandidates) {
|
|
|
5265
5317
|
}
|
|
5266
5318
|
}
|
|
5267
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
|
+
}
|
|
5268
5407
|
// ⟨SOUNDNESS R956⟩ A class EXPRESSION's member has no unit (`localName` names class DECLARATION members only), so
|
|
5269
5408
|
// its body folded into whatever evaluates the expression and no dispatch could land on it. An override needs an
|
|
5270
5409
|
// addressable body: mint it the way a structural implementor's member is minted — keyed by `stableUnitTag`, with
|
|
@@ -6119,12 +6258,13 @@ function recordConformer(c, t) {
|
|
|
6119
6258
|
}
|
|
6120
6259
|
if (ts.isClassDeclaration(t)) {
|
|
6121
6260
|
if (c.kind === "class" && classInSubtree(c.node, t)) return false; // nominal: the subtree already answers
|
|
6122
|
-
// R958
|
|
6123
|
-
//
|
|
6124
|
-
//
|
|
6125
|
-
// `
|
|
6126
|
-
//
|
|
6127
|
-
|
|
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.
|
|
6128
6268
|
let added = false;
|
|
6129
6269
|
for (const anc of localAncestorsAndSelf(t)) added = pushUnique(classConformers, anc, c.node) || added;
|
|
6130
6270
|
if (c.kind === "lit") mintStructuralMembers(c.node, true);
|
|
@@ -6197,7 +6337,6 @@ function isConversionPosition(n) {
|
|
|
6197
6337
|
return false;
|
|
6198
6338
|
}
|
|
6199
6339
|
// R958 — the value under an assertion chain: parentheses, `as`, `<T>`, `!` and `satisfies` change no runtime value.
|
|
6200
|
-
let conversionViaAssertion = false; // set while `convert` records a source read THROUGH an assertion
|
|
6201
6340
|
const peelAssertions = (e) => unwrapArgExpr(e); // R780: one wrapper set, not a third copy of it
|
|
6202
6341
|
// R82 / PART 87: is the conversion's SOURCE type a fact about the VALUE, or only about a generic signature? A
|
|
6203
6342
|
// call whose declared return type mentions one of its own type parameters (`wrap<T>(x: T): T`) hands back
|
|
@@ -6395,21 +6534,19 @@ for (const sf of sources) {
|
|
|
6395
6534
|
// called as `launder(new LocalW())` ran LocalW's `m` (EXECUTED) with `deny Fs` exit 0 on the caller. The
|
|
6396
6535
|
// parameter's visible arguments say what it holds — the SAME provenance index R246's accessor arm asks
|
|
6397
6536
|
// (`receiverValueTypes`) — so each argument type is recorded as a conversion source. Upcasts only, as
|
|
6398
|
-
// ever (`recordConversion` keeps a target the type is assignable to).
|
|
6399
|
-
//
|
|
6400
|
-
|
|
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)) {
|
|
6401
6541
|
try { recordConversion(vt, tt); } catch { /* best effort, as the main record below */ }
|
|
6402
6542
|
CONFORMER_REACH?.("any-param", `${path.relative(rootDir, sf.fileName)}:${at.getStart()}`);
|
|
6403
6543
|
}
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
recordConversion(st, tt);
|
|
6411
|
-
if (count() > before) CONFORMER_REACH(kind, `${path.relative(rootDir, sf.fileName)}:${at.getStart()}`);
|
|
6412
|
-
} finally { conversionViaAssertion = false; }
|
|
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()}`);
|
|
6413
6550
|
};
|
|
6414
6551
|
(function walkConversions(node) {
|
|
6415
6552
|
if (ts.isExpression(node) && !ts.isSpreadElement(node) && isConversionPosition(node)) {
|
|
@@ -6429,9 +6566,53 @@ for (const sf of sources) {
|
|
|
6429
6566
|
let tt; try { tt = checker.getTypeFromTypeNode(node.type); } catch { tt = undefined; }
|
|
6430
6567
|
convert(peelAssertions(node.expression), tt, node, "assertion");
|
|
6431
6568
|
}
|
|
6569
|
+
if (ts.isObjectLiteralExpression(node)) providerSubstitution(node); // ⟨R1061⟩
|
|
6432
6570
|
ts.forEachChild(node, walkConversions);
|
|
6433
6571
|
})(sf);
|
|
6434
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
|
+
}
|
|
6435
6616
|
// The interface-flow fixpoint: a `J`-typed value converted to `X` carries every conformer of `J` with it.
|
|
6436
6617
|
for (let changed = true, g = 0; changed && g++ < 32; ) {
|
|
6437
6618
|
changed = false;
|
|
@@ -6590,6 +6771,19 @@ function keyLiteralNames(t, depth = 0, seen = new Set()) {
|
|
|
6590
6771
|
// the accessor edges the value's type declares — and never hedges; a receiver neither rule answers is
|
|
6591
6772
|
// exactly as it was (the residual is stated in SOUNDNESS R246, not hidden here).
|
|
6592
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
|
+
}
|
|
6593
6787
|
function receiverValueTypes(expr, depth = 0, seen = new Set()) {
|
|
6594
6788
|
if (!expr || depth > 6) return [];
|
|
6595
6789
|
let t; try { t = checker.getTypeAtLocation(expr); } catch { t = null; }
|
|
@@ -6599,10 +6793,18 @@ function receiverValueTypes(expr, depth = 0, seen = new Set()) {
|
|
|
6599
6793
|
let ti; try { ti = checker.getTypeAtLocation(inner); } catch { ti = null; }
|
|
6600
6794
|
return [...(ti && !anyishType(ti) ? [ti] : []), ...below];
|
|
6601
6795
|
}
|
|
6602
|
-
if (!
|
|
6796
|
+
if (!uninformativeType(t) || !ts.isIdentifier(expr)) return [];
|
|
6603
6797
|
const sym = checker.getSymbolAtLocation(expr);
|
|
6604
6798
|
const d = sym?.valueDeclaration;
|
|
6605
|
-
if (!sym || !d ||
|
|
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 [];
|
|
6606
6808
|
seen.add(sym);
|
|
6607
6809
|
const args = paramArgs.get(sym);
|
|
6608
6810
|
if (!args || paramFnEscaped(paramOwnerOf.get(sym))) return [];
|
|
@@ -6610,7 +6812,7 @@ function receiverValueTypes(expr, depth = 0, seen = new Set()) {
|
|
|
6610
6812
|
for (const a of args) {
|
|
6611
6813
|
if (!a) continue;
|
|
6612
6814
|
let ta; try { ta = checker.getTypeAtLocation(a); } catch { ta = null; }
|
|
6613
|
-
if (ta && !anyishType(ta)) out.push(ta);
|
|
6815
|
+
if (ta && !anyishType(ta)) out.push(ta); // a VALUE's own type, even `{}`, is a fact about it
|
|
6614
6816
|
out.push(...receiverValueTypes(a, depth + 1, seen));
|
|
6615
6817
|
}
|
|
6616
6818
|
return out;
|
|
@@ -7327,6 +7529,12 @@ function enclosing(node) {
|
|
|
7327
7529
|
&& (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Static)
|
|
7328
7530
|
&& p.parent && (ts.isClassDeclaration(p.parent) || ts.isClassExpression(p.parent)))
|
|
7329
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);
|
|
7330
7538
|
if (ts.isHeritageClause(p) && p.token === ts.SyntaxKind.ExtendsKeyword && p.parent
|
|
7331
7539
|
&& (ts.isClassDeclaration(p.parent) || ts.isClassExpression(p.parent))) {
|
|
7332
7540
|
const ctor = nodeName.get(p.parent);
|
|
@@ -9040,6 +9248,74 @@ const envPropMustSymbols = new Set();
|
|
|
9040
9248
|
const envPropMaySymbols = new Set();
|
|
9041
9249
|
// ⟨R935⟩ a property location's identity: its first declaration node (see `memberSymOf` for why not the symbol).
|
|
9042
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
|
+
};
|
|
9043
9319
|
{
|
|
9044
9320
|
const ENV_DESTRUCTURED = Symbol("env-destructured");
|
|
9045
9321
|
const bindings = new Map(); // symbol -> [value expr | null | ENV_DESTRUCTURED | { same: symbol }]
|
|
@@ -9056,7 +9332,7 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
|
|
|
9056
9332
|
const memberRawSym = (n) => (ts.isPropertyAccessExpression(n) ? checker.getSymbolAtLocation(n.name)
|
|
9057
9333
|
: (ts.isElementAccessExpression(n) && n.argumentExpression && ts.isStringLiteralLike(n.argumentExpression))
|
|
9058
9334
|
? checker.getSymbolAtLocation(n.argumentExpression) : null) ?? null;
|
|
9059
|
-
const memberSymOf = (n) => envLocKey(memberRawSym(n));
|
|
9335
|
+
const memberSymOf = (n) => envLiteralMember(n)?.key ?? envLocKey(memberRawSym(n));
|
|
9060
9336
|
// Is this property symbol a location whose bindings we can enumerate? Every declaration must be
|
|
9061
9337
|
// value-bearing, in a project file, and not a member of an extended class.
|
|
9062
9338
|
const propEligible = (sym) => {
|
|
@@ -9073,7 +9349,12 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
|
|
|
9073
9349
|
if (!ok) propIneligible.add(sym);
|
|
9074
9350
|
return ok;
|
|
9075
9351
|
};
|
|
9076
|
-
const noteProp = (sym, v) => {
|
|
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); } };
|
|
9077
9358
|
const collect = (node) => {
|
|
9078
9359
|
if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
|
|
9079
9360
|
note(checker.getSymbolAtLocation(node.name), node.initializer ?? null);
|
|
@@ -9091,7 +9372,10 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
|
|
|
9091
9372
|
&& (ts.isPropertyAccessExpression(node.left) || ts.isElementAccessExpression(node.left))) {
|
|
9092
9373
|
// ⟨R935⟩ an assignment to a property location; a compound operator (`+=`) binds a non-environment.
|
|
9093
9374
|
const s = memberRawSym(node.left);
|
|
9094
|
-
if (s)
|
|
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); }
|
|
9095
9379
|
} else if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {
|
|
9096
9380
|
const s = checker.getSymbolAtLocation(node.name);
|
|
9097
9381
|
if (s && (s.flags & ts.SymbolFlags.Property)) noteProp(s, node.initializer);
|
|
@@ -9126,27 +9410,80 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
|
|
|
9126
9410
|
if (reflectTarget) {
|
|
9127
9411
|
let props = [];
|
|
9128
9412
|
try { props = checker.getTypeAtLocation(reflectTarget)?.getProperties?.() ?? []; } catch { props = []; }
|
|
9129
|
-
for (const ps of props) if (propEligible(ps)) noteProp(ps, null);
|
|
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
|
+
}
|
|
9130
9430
|
}
|
|
9131
9431
|
ts.forEachChild(node, collect);
|
|
9132
9432
|
};
|
|
9133
9433
|
for (const sf of sources) collect(sf);
|
|
9134
|
-
// ⟨
|
|
9135
|
-
|
|
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
|
+
}
|
|
9136
9469
|
const leafIn = (leaf, set, propSet) => isProcessEnvExpr(leaf)
|
|
9137
9470
|
|| (ts.isIdentifier(leaf) && set.has(checker.getSymbolAtLocation(leaf)))
|
|
9138
|
-
|| (!!propSet && propSet.has(memberSymOf(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
|
+
}
|
|
9139
9477
|
const hasEnvLeaf = (v, set, propSet) => v === ENV_DESTRUCTURED
|
|
9140
9478
|
|| (!!v && typeof v === "object" && "same" in v ? set.has(v.same) || propSet?.has(v.same) === true
|
|
9141
9479
|
: (!!v && typeof v === "object" && envValueLeaves(v).some((l) => leafIn(l, set, propSet))));
|
|
9142
9480
|
|
|
9143
9481
|
|
|
9144
|
-
// MAY: least fixpoint of "some binding has a leaf that is process.env or already MAY" — LOCALS
|
|
9145
|
-
//
|
|
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.
|
|
9146
9484
|
for (let changed = true, n = 0; changed && n < 64; n++) {
|
|
9147
9485
|
changed = false;
|
|
9148
9486
|
for (const [sym, vals] of bindings) {
|
|
9149
|
-
if (paramSyms.has(sym)) continue;
|
|
9150
9487
|
const isProp = propSyms.has(sym);
|
|
9151
9488
|
const set = isProp ? envPropMaySymbols : envMayAliasSymbols;
|
|
9152
9489
|
if (!set.has(sym) && vals.some((v) => hasEnvLeaf(v, envMayAliasSymbols, envPropMaySymbols))) { set.add(sym); changed = true; }
|
|
@@ -9154,24 +9491,37 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
|
|
|
9154
9491
|
}
|
|
9155
9492
|
// MUST: least fixpoint (from empty) of "every binding has a leaf that is process.env or already MUST" —
|
|
9156
9493
|
// least, so a cycle that never touches process.env directly stays MAY (the disclosing side). Locals and
|
|
9157
|
-
// properties start from their MAY sets
|
|
9158
|
-
//
|
|
9159
|
-
|
|
9160
|
-
|
|
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.
|
|
9161
9512
|
const mustTry = () => {
|
|
9162
9513
|
let changed = false;
|
|
9163
9514
|
for (const sym of envMayAliasSymbols) {
|
|
9515
|
+
if (paramSyms.has(sym)) continue;
|
|
9164
9516
|
if (!envAliasSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) { envAliasSymbols.add(sym); changed = true; }
|
|
9165
9517
|
}
|
|
9166
9518
|
for (const sym of envPropMaySymbols) {
|
|
9167
9519
|
if (!envPropMustSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) { envPropMustSymbols.add(sym); changed = true; }
|
|
9168
9520
|
}
|
|
9169
|
-
for (const sym of
|
|
9521
|
+
for (const sym of paramSyms) {
|
|
9170
9522
|
if (envAliasSymbols.has(sym)) continue;
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
if (!batched) { batched = true; paramFnEscaped.batch(paramCands.map((p) => paramOwnerOf.get(p)).filter(Boolean)); }
|
|
9174
|
-
if (paramFnEscaped(fsym)) continue;
|
|
9523
|
+
const must = (v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols);
|
|
9524
|
+
if (!(paramArgs.get(sym) ?? []).some(must) || !(paramAssigns.get(sym) ?? []).every(must)) continue;
|
|
9175
9525
|
envAliasSymbols.add(sym); changed = true;
|
|
9176
9526
|
}
|
|
9177
9527
|
return changed;
|
|
@@ -9195,6 +9545,8 @@ const identIsEnvMayAlias = (id) => {
|
|
|
9195
9545
|
// ⟨R935⟩ a member reference `o.p` / `o["p"]` whose property is an environment location.
|
|
9196
9546
|
const envMemberSym = (n) => {
|
|
9197
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;
|
|
9198
9550
|
if (ts.isPropertyAccessExpression(n)) return envLocKey(checker.getSymbolAtLocation(n.name));
|
|
9199
9551
|
if (ts.isElementAccessExpression(n) && n.argumentExpression && ts.isStringLiteralLike(n.argumentExpression))
|
|
9200
9552
|
return envLocKey(checker.getSymbolAtLocation(n.argumentExpression));
|
|
@@ -9203,7 +9555,11 @@ const envMemberSym = (n) => {
|
|
|
9203
9555
|
const envPropKind = (n) => {
|
|
9204
9556
|
const s = envMemberSym(n);
|
|
9205
9557
|
if (!s || !envPropMaySymbols.has(s)) return null;
|
|
9206
|
-
|
|
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";
|
|
9207
9563
|
};
|
|
9208
9564
|
const envValueKind = (expr) => {
|
|
9209
9565
|
let kind = null;
|
|
@@ -9641,6 +9997,12 @@ function visitCalls(node) {
|
|
|
9641
9997
|
}
|
|
9642
9998
|
const sig = checker.getResolvedSignature(node);
|
|
9643
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
|
+
}
|
|
9644
10006
|
// ⟨R103⟩ A WRITABLE SLOT IS AN INCOMPLETE CANDIDATE SET — see `openCallSlot`, and see the class-
|
|
9645
10007
|
// override fan-out below, which is the authority this converges on rather than a second rule: it
|
|
9646
10008
|
// edges to every candidate it CAN name and adds `Unknown` when the set it enumerated is not
|
|
@@ -9660,8 +10022,8 @@ function visitCalls(node) {
|
|
|
9660
10022
|
if (!decl) {
|
|
9661
10023
|
// `new C()` on a class with an IMPLICIT constructor resolves to no declaration — edge to
|
|
9662
10024
|
// the class's (synthesized) ctor unit via the class identifier before concluding Unknown.
|
|
9663
|
-
let edged =
|
|
9664
|
-
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)) {
|
|
9665
10027
|
const cd = realDecl(checker.getSymbolAtLocation(node.expression));
|
|
9666
10028
|
const t = cd && nodeName.get(cd);
|
|
9667
10029
|
if (t) { rec.edges.add(t); edged = true; }
|
|
@@ -12118,8 +12480,21 @@ for (const sf of sources) visitCalls(sf);
|
|
|
12118
12480
|
const herR = herQ && fns.get(herQ);
|
|
12119
12481
|
if (herR && ["direct", "edges", "why", "blind", "hosts", "paths", "cmds", "incomplete", "dispatch", "tables"]
|
|
12120
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}`);
|
|
12121
12491
|
if (!targets.length) continue;
|
|
12122
|
-
|
|
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);
|
|
12123
12498
|
const rec = from && fns.get(from);
|
|
12124
12499
|
if (!rec) continue;
|
|
12125
12500
|
for (const [q, k] of targets) if (q !== from && !rec.edges.has(q)) { rec.edges.add(q); hit(k); }
|