candor-ts 0.40.1 → 0.40.3
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 +20 -0
- package/package.json +6 -3
- package/scan.mjs +1096 -73
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
|
}
|
|
@@ -2595,6 +2644,29 @@ const unaliasGlobal = (expr) => {
|
|
|
2595
2644
|
};
|
|
2596
2645
|
const projectFiles = new Set(fileNames.map((f) => path.resolve(f)));
|
|
2597
2646
|
const sources = program.getSourceFiles().filter((f) => projectFiles.has(path.resolve(f.fileName)));
|
|
2647
|
+
// ⟨SOUNDNESS R1074⟩ DOES A PROJECT DECLARATION SHADOW AN AMBIENT GLOBAL? Eight checks asked "is any declaration of
|
|
2648
|
+
// this symbol in a project file?", and that is not the question: in a JavaScript SCRIPT the checker records every
|
|
2649
|
+
// expando ASSIGNMENT as a declaration of the global it writes through. One `process.env['K'] = v` in a project
|
|
2650
|
+
// `.cjs`/`.mjs`/`.js` script gives the global `process` a declaration in that file, and every `process.env` read
|
|
2651
|
+
// in the WHOLE PROJECT then failed the "ambient global" test. MEASURED (EXECUTED, `fx74`): `readSecret() { return
|
|
2652
|
+
// process.env.SECRET }` printed the planted value and was ABSENT, `deny Env readSecret` exit 0, beside a one-line
|
|
2653
|
+
// script that writes a different key; the same file without the script is `['Env']`. On the pinned corpus, every
|
|
2654
|
+
// `process.env` read across drizzle-orm (two driver-test scripts assign `process.env['POSTGRES_URL']`).
|
|
2655
|
+
// A shadow is a declaration that CREATES a different runtime binding: a non-ambient variable, function, class,
|
|
2656
|
+
// parameter, binding element or import. An expando (an Identifier/PropertyAccess/ElementAccess/BinaryExpression
|
|
2657
|
+
// declaration node, or `Object.defineProperty` on the global), a `declare` statement, a `.d.ts` file and a
|
|
2658
|
+
// `declare global {}` block all name the global itself and shadow nothing.
|
|
2659
|
+
const declShadowsGlobal = (d) => {
|
|
2660
|
+
if (!projectFiles.has(path.resolve(d.getSourceFile().fileName)) || d.getSourceFile().isDeclarationFile) return false;
|
|
2661
|
+
if (!(ts.isVariableDeclaration(d) || ts.isFunctionDeclaration(d) || ts.isClassDeclaration(d) || ts.isParameter(d)
|
|
2662
|
+
|| ts.isBindingElement(d) || ts.isImportSpecifier(d) || ts.isImportClause(d) || ts.isNamespaceImport(d)
|
|
2663
|
+
|| ts.isImportEqualsDeclaration(d) || ts.isEnumDeclaration(d) || ts.isModuleDeclaration(d))) return false;
|
|
2664
|
+
if (ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Ambient) return false;
|
|
2665
|
+
for (let p = d.parent; p; p = p.parent)
|
|
2666
|
+
if (ts.isModuleDeclaration(p) && (p.flags & ts.NodeFlags.GlobalAugmentation)) return false;
|
|
2667
|
+
return true;
|
|
2668
|
+
};
|
|
2669
|
+
const projectShadows = (decls) => (decls ?? []).some(declShadowsGlobal);
|
|
2598
2670
|
|
|
2599
2671
|
// ⟨0.21⟩ COMPLETENESS MANIFEST (Gap 2): the TARGET's own source candor could NOT analyze — a .ts that
|
|
2600
2672
|
// FAILED TO PARSE (a syntax error). getSyntacticDiagnostics() reports lexer/parser failures; the source
|
|
@@ -3483,7 +3555,7 @@ function determinedUrlObject(expr, call, depth = 0) {
|
|
|
3483
3555
|
&& (expr.expression.text === "URL" || expr.expression.text === "Request")) {
|
|
3484
3556
|
const decls = checker.getSymbolAtLocation(expr.expression)?.declarations ?? [];
|
|
3485
3557
|
if (decls.length === 0
|
|
3486
|
-
|| decls
|
|
3558
|
+
|| projectShadows(decls)) return null; // ⟨R1074⟩
|
|
3487
3559
|
const args = expr.arguments ?? [];
|
|
3488
3560
|
const max = expr.expression.text === "URL" ? 2 : 1;
|
|
3489
3561
|
if (args.length === 0 || args.length > max) return null;
|
|
@@ -4023,6 +4095,7 @@ const fns = new Map(); // qualified name -> { direct, edges, hosts, ta
|
|
|
4023
4095
|
const bodylessDecls = new Map(); // qual -> {node, mod, abstract, owner, member}
|
|
4024
4096
|
const unlistedSeen = new Map(); // the κ-coverage ledger: unlisted npm package -> call-site count
|
|
4025
4097
|
const nodeName = new WeakMap(); // declaration node -> qualified name
|
|
4098
|
+
const classExprCtorQual = new Map(); // ⟨R1016⟩ ClassExpression -> its constructor unit (see `mintClassExprCtor`)
|
|
4026
4099
|
// ⟨scan-boundary, export-alias⟩ `module.exports = { thing: _internalImplName }` / `exports.thing =
|
|
4027
4100
|
// _internalImplName` — a RENAMED re-export whose RHS NAMES an EXISTING declaration rather than being an
|
|
4028
4101
|
// inline function. `localName` mints a CJS unit for `module.exports = function(){}` and
|
|
@@ -4187,6 +4260,10 @@ const classDescendants = new Map();// base ClassDeclaration -> transitive LOCAL
|
|
|
4187
4260
|
// R954 — the conformer registries (filled by the conversion pass after `walkStructural`).
|
|
4188
4261
|
const classConformers = new Map(); // local ClassDeclaration -> conformer nodes (local class, literal, dep class)
|
|
4189
4262
|
const depConformers = new Map(); // InterfaceDeclaration (local or foreign) -> dependency ClassDeclarations
|
|
4263
|
+
// ⟨R1061⟩ local classes a DI provider substitutes with a value this scan cannot name: a dispatch on one discloses.
|
|
4264
|
+
const openSubstitutedClasses = new Set();
|
|
4265
|
+
const tokenProviders = new Map(); // ⟨R1061⟩ non-class provider token key -> [{ lit, prop }] (see `injectTokenKey`)
|
|
4266
|
+
const tokenDeclIds = new Map();
|
|
4190
4267
|
const ifaceFlows = new Map(); // local InterfaceDeclaration J -> Set of targets a J-typed value is converted to
|
|
4191
4268
|
const isProjectNode = (d) => !!d && projectFiles.has(path.resolve(d.getSourceFile().fileName));
|
|
4192
4269
|
const conversionAddedImpls = new Map(); // InterfaceDeclaration -> Set of implementors the conversion pass added
|
|
@@ -4563,6 +4640,7 @@ const STABLE_KEY_SHAPES = {
|
|
|
4563
4640
|
callable: new Set([ts.SyntaxKind.ArrowFunction, ts.SyntaxKind.FunctionExpression]),
|
|
4564
4641
|
decorator: new Set([ts.SyntaxKind.ArrowFunction, ts.SyntaxKind.FunctionExpression]),
|
|
4565
4642
|
"decorator-arg": new Set([ts.SyntaxKind.CallExpression]),
|
|
4643
|
+
classexpr: new Set([ts.SyntaxKind.ClassExpression]), // ⟨R1016⟩ a class expression's constructor unit
|
|
4566
4644
|
computed: null, // a descriptor member of any shape — counted over EVERY node under the anchor
|
|
4567
4645
|
};
|
|
4568
4646
|
const stableKeySanitise = (s) => s.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
@@ -4890,6 +4968,42 @@ function descriptorAliasRef(descProp) {
|
|
|
4890
4968
|
// True when `node` is lexically inside a FUNCTION body — so its bare local name can collide with a
|
|
4891
4969
|
// module-level or sibling-scope unit of the same name (see the qual disambiguation below). A namespace/
|
|
4892
4970
|
// module block and the source file are NOT function scopes (their members are top-level units).
|
|
4971
|
+
// ⟨SOUNDNESS R1067/R1074⟩ THE KEYS OF A CLASS'S SYNTHESIZED UNITS, spelled once. A named function's, method's and
|
|
4972
|
+
// explicit constructor's qual carries `#line:col` when FUNCTION-SCOPED (above), because two scopes may declare the
|
|
4973
|
+
// same name. The units a CLASS mints for itself — its constructor when implicit (instance field initialisers), its
|
|
4974
|
+
// `<static-init>` and its `<heritage>` — were keyed on the bare class name, so two function-scoped `class K` in one
|
|
4975
|
+
// module shared one unit, and every anonymous class EXPRESSION's static block shared `<anonymous>.<static-init>`.
|
|
4976
|
+
// MEASURED (EXECUTED, `fx67`): six pure definers charged `Fs` from a sibling's block or field (`deny Fs` exit 1 over
|
|
4977
|
+
// code that writes nothing), and — the silent half — a function-scoped class with an EXPLICIT constructor kept its
|
|
4978
|
+
// field initialisers on the bare `K.constructor` unit while `new K()` resolved to the positioned explicit one, so
|
|
4979
|
+
// the constructing function was ABSENT and `deny Fs` exited 0 over a write that ran.
|
|
4980
|
+
// The class's own `.constructor` unit IS its explicit implementation's when there is one (the top-level spelling
|
|
4981
|
+
// already coincided); otherwise it is positioned at the class like any function-scoped unit.
|
|
4982
|
+
function classCtorQual(cls) {
|
|
4983
|
+
const sf = cls.getSourceFile();
|
|
4984
|
+
const base = `${moduleOf(sf)}.${namespacePrefixOf(cls)}${cls.name.text}.constructor`;
|
|
4985
|
+
if (!isFunctionScoped(cls)) return base;
|
|
4986
|
+
const impl = (cls.members ?? []).find((m) => ts.isConstructorDeclaration(m) && m.body);
|
|
4987
|
+
const { line, character } = sf.getLineAndCharacterOfPosition((impl ?? cls).getStart());
|
|
4988
|
+
return `${base}#${line + 1}:${character + 1}`;
|
|
4989
|
+
}
|
|
4990
|
+
// A class EXPRESSION is keyed by its stable tag (`<class>@…`, R944/R1016), never by its own optional name, which is
|
|
4991
|
+
// scoped to the class body: `return class Q {…}` twice in one module is two classes.
|
|
4992
|
+
function classUnitQual(cls, member) {
|
|
4993
|
+
const sf = cls.getSourceFile();
|
|
4994
|
+
const mod = moduleOf(sf);
|
|
4995
|
+
if (ts.isClassExpression(cls)) return `${mod}.<class>@${stableUnitTag("classexpr", cls)}.${member}`;
|
|
4996
|
+
const name = cls.name ? cls.name.text : "<anonymous>"; // only `export default class {}`: one per module
|
|
4997
|
+
const base = `${mod}.${namespacePrefixOf(cls)}${name}.${member}`;
|
|
4998
|
+
if (!cls.name || !isFunctionScoped(cls)) return base;
|
|
4999
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(cls.getStart());
|
|
5000
|
+
return `${base}#${line + 1}:${character + 1}`;
|
|
5001
|
+
}
|
|
5002
|
+
// The `<heritage>` unit beside a class's constructor unit — one derivation for its minting and its wiring.
|
|
5003
|
+
function heritageQualOf(ctorQual) {
|
|
5004
|
+
return ctorQual && /\.constructor(#\d+:\d+)?$/.test(ctorQual)
|
|
5005
|
+
? ctorQual.replace(/\.constructor(#\d+:\d+)?$/, ".<heritage>$1") : null;
|
|
5006
|
+
}
|
|
4893
5007
|
function isFunctionScoped(node) {
|
|
4894
5008
|
for (let p = node.parent; p; p = p.parent) {
|
|
4895
5009
|
if (ts.isSourceFile(p) || ts.isModuleBlock(p)) return false;
|
|
@@ -5006,7 +5120,7 @@ for (const sf of sources) {
|
|
|
5006
5120
|
for (const t of h.types) for (const idecl of localInterfaceDecls(t.expression)) climb(idecl);
|
|
5007
5121
|
for (const t of h.types) for (const idecl of foreignInterfaceDecls(t.expression)) fClimb(idecl);
|
|
5008
5122
|
}
|
|
5009
|
-
const ctorQual =
|
|
5123
|
+
const ctorQual = classCtorQual(node);
|
|
5010
5124
|
if (!fns.has(ctorQual)) {
|
|
5011
5125
|
const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart());
|
|
5012
5126
|
fns.set(ctorQual, { local: `${node.name.text}.constructor`, direct: new Set(), fsKinds: new Set(), edges: new Set(),
|
|
@@ -5134,6 +5248,7 @@ for (const sf of sources) {
|
|
|
5134
5248
|
// (a wholly-unresolvable target leaves only the minted unit — nothing to join a forcing site to.)
|
|
5135
5249
|
}
|
|
5136
5250
|
}
|
|
5251
|
+
if (ts.isClassExpression(node)) mintClassExprCtor(node); // ⟨R1016⟩
|
|
5137
5252
|
ts.forEachChild(node, collect);
|
|
5138
5253
|
})(sf);
|
|
5139
5254
|
}
|
|
@@ -5265,6 +5380,93 @@ for (const { mod, name, ident } of exportAliasCandidates) {
|
|
|
5265
5380
|
}
|
|
5266
5381
|
}
|
|
5267
5382
|
|
|
5383
|
+
// ⟨SOUNDNESS R1016⟩ A CLASS EXPRESSION'S CONSTRUCTOR IS A UNIT, as a class declaration's always was. Without one,
|
|
5384
|
+
// `const L = class { constructor() { fs.writeFileSync(…) } }; new L()` and `new Holder.Inner()` over a
|
|
5385
|
+
// `static Inner = class {…}` had nothing to edge to: the constructing function read `['Unknown']` (`deny Fs` exit 0
|
|
5386
|
+
// on a write that ran — EXECUTED), and the body's `Fs` fell through `enclosing` to whatever EVALUATES the class
|
|
5387
|
+
// (`<module>`, `Holder.<static-init>`), which never constructs it (EXECUTED: importing the module writes nothing).
|
|
5388
|
+
// The unit holds what runs at construction — the explicit constructor's body and parameters, and every INSTANCE
|
|
5389
|
+
// field initialiser that is not itself a function value (that one runs when called, and stays where it was) —
|
|
5390
|
+
// so `new` resolves to it through the checker like any constructor, and an implicit constructor is found through
|
|
5391
|
+
// the constructed value's TYPE (`classExprCtorOfNew`). Minted only where there is something to hold.
|
|
5392
|
+
//
|
|
5393
|
+
// WHETHER THE EVALUATING UNIT KEEPS THE CHARGE is ⟨R519⟩'s question, and its answer stands: minting is an
|
|
5394
|
+
// ADDITION unless every construction is provably one this scan edges. A class VALUE that escapes — exported,
|
|
5395
|
+
// returned, passed, stored, `extends`-ed, read as a static — can be constructed where no edge reaches it (another
|
|
5396
|
+
// package, which cannot name an anonymous class's constructor; an `any`-typed `new`), so pass 2a′ keeps the
|
|
5397
|
+
// evaluator's containment edge for it. Only a `const`-bound, unexported class expression whose every reference
|
|
5398
|
+
// is a `new` callee (or a type position, or an `instanceof` operand) drops it — there, every run of the body is
|
|
5399
|
+
// a `new` the scan resolved, and the evaluator's charge was the fabrication R1016 measured.
|
|
5400
|
+
function mintClassExprCtor(cls) {
|
|
5401
|
+
const ctor = (cls.members ?? []).find((m) => ts.isConstructorDeclaration(m) && m.body);
|
|
5402
|
+
const fieldInit = (cls.members ?? []).some((m) => ts.isPropertyDeclaration(m) && m.initializer
|
|
5403
|
+
&& !(ts.getCombinedModifierFlags(m) & ts.ModifierFlags.Static)
|
|
5404
|
+
&& !ts.isArrowFunction(m.initializer) && !ts.isFunctionExpression(m.initializer));
|
|
5405
|
+
if (!ctor && !fieldInit) return null;
|
|
5406
|
+
const sf = cls.getSourceFile();
|
|
5407
|
+
const local = `<class>@${stableUnitTag("classexpr", cls)}.constructor`;
|
|
5408
|
+
const qual = `${moduleOf(sf)}.${local}`;
|
|
5409
|
+
if (!fns.has(qual)) {
|
|
5410
|
+
const at = ctor ?? cls;
|
|
5411
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(at.getStart());
|
|
5412
|
+
fns.set(qual, { local, direct: new Set(), fsKinds: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
|
|
5413
|
+
cmds: new Set(), paths: new Set(), blind: new Set(), incomplete: new Set(), dispatch: new Set(),
|
|
5414
|
+
why: new Set(), entry: false,
|
|
5415
|
+
loc: `${path.relative(rootDir, sf.fileName)}:${line + 1}:${character + 1}`,
|
|
5416
|
+
endLine: sf.getLineAndCharacterOfPosition(at.getEnd()).line + 1 });
|
|
5417
|
+
}
|
|
5418
|
+
if (ctor) nodeName.set(ctor, qual);
|
|
5419
|
+
classExprCtorQual.set(cls, qual);
|
|
5420
|
+
if (process.env.CANDOR_R1016_REACH) console.error(`R1016-REACH mint ${qual}`);
|
|
5421
|
+
return qual;
|
|
5422
|
+
}
|
|
5423
|
+
// The class-expression constructor unit a `new X(…)` runs, read off the constructed value's TYPE — the route an
|
|
5424
|
+
// IMPLICIT constructor needs (the signature then has no declaration, or names the BASE's), and harmless beside an
|
|
5425
|
+
// explicit one, which the signature already names.
|
|
5426
|
+
function classExprCtorOfNew(node) {
|
|
5427
|
+
if (!classExprCtorQual.size || !ts.isNewExpression(node) || !node.expression) return null;
|
|
5428
|
+
let vd;
|
|
5429
|
+
try { vd = checker.getTypeAtLocation(node.expression)?.getSymbol?.()?.valueDeclaration; } catch { vd = undefined; }
|
|
5430
|
+
return vd && ts.isClassExpression(vd) ? (classExprCtorQual.get(vd) ?? null) : null;
|
|
5431
|
+
}
|
|
5432
|
+
// Does this class expression's VALUE reach anything but a `new` this scan resolves? (see `mintClassExprCtor`)
|
|
5433
|
+
function classExprEscapes(cls) {
|
|
5434
|
+
let top = cls;
|
|
5435
|
+
while (top.parent && (ts.isParenthesizedExpression(top.parent) || ts.isAsExpression(top.parent)
|
|
5436
|
+
|| ts.isNonNullExpression(top.parent) || (ts.isSatisfiesExpression?.(top.parent) ?? false))) top = top.parent;
|
|
5437
|
+
const vd = top.parent;
|
|
5438
|
+
if (!vd || !ts.isVariableDeclaration(vd) || vd.initializer !== top || !ts.isIdentifier(vd.name)
|
|
5439
|
+
|| !(ts.getCombinedNodeFlags(vd) & ts.NodeFlags.Const)
|
|
5440
|
+
|| (ts.getCombinedModifierFlags(vd) & ts.ModifierFlags.Export)) return true;
|
|
5441
|
+
const syms = new Set();
|
|
5442
|
+
const outer = checker.getSymbolAtLocation(vd.name);
|
|
5443
|
+
if (!outer) return true;
|
|
5444
|
+
syms.add(outer);
|
|
5445
|
+
if (cls.name) { const inner = checker.getSymbolAtLocation(cls.name); if (inner) syms.add(inner); }
|
|
5446
|
+
const names = new Set([vd.name.text, cls.name?.text].filter(Boolean));
|
|
5447
|
+
let escapes = false;
|
|
5448
|
+
(function visit(n) {
|
|
5449
|
+
if (escapes) return;
|
|
5450
|
+
if (ts.isIdentifier(n) && names.has(n.text) && n !== vd.name && n !== cls.name) {
|
|
5451
|
+
let sym; try { sym = checker.getSymbolAtLocation(n); } catch { sym = undefined; }
|
|
5452
|
+
if (sym && (sym.flags & ts.SymbolFlags.Alias)) { try { sym = checker.getAliasedSymbol(sym); } catch { /* keep */ } }
|
|
5453
|
+
const p0 = n.parent;
|
|
5454
|
+
if (p0 && ts.isShorthandPropertyAssignment(p0) && p0.name === n) { escapes = true; return; }
|
|
5455
|
+
if (sym && syms.has(sym)) {
|
|
5456
|
+
let cur = n;
|
|
5457
|
+
while (cur.parent && (ts.isParenthesizedExpression(cur.parent) || ts.isNonNullExpression(cur.parent)
|
|
5458
|
+
|| ts.isAsExpression(cur.parent) || (ts.isSatisfiesExpression?.(cur.parent) ?? false))) cur = cur.parent;
|
|
5459
|
+
const p = cur.parent;
|
|
5460
|
+
const safe = !!p && ((ts.isNewExpression(p) && p.expression === cur)
|
|
5461
|
+
|| ts.isTypeQueryNode(p)
|
|
5462
|
+
|| (ts.isBinaryExpression(p) && p.right === cur && p.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword));
|
|
5463
|
+
if (!safe) { escapes = true; return; }
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
ts.forEachChild(n, visit);
|
|
5467
|
+
})(vd.getSourceFile());
|
|
5468
|
+
return escapes;
|
|
5469
|
+
}
|
|
5268
5470
|
// ⟨SOUNDNESS R956⟩ A class EXPRESSION's member has no unit (`localName` names class DECLARATION members only), so
|
|
5269
5471
|
// its body folded into whatever evaluates the expression and no dispatch could land on it. An override needs an
|
|
5270
5472
|
// addressable body: mint it the way a structural implementor's member is minted — keyed by `stableUnitTag`, with
|
|
@@ -6119,12 +6321,13 @@ function recordConformer(c, t) {
|
|
|
6119
6321
|
}
|
|
6120
6322
|
if (ts.isClassDeclaration(t)) {
|
|
6121
6323
|
if (c.kind === "class" && classInSubtree(c.node, t)) return false; // nominal: the subtree already answers
|
|
6122
|
-
// R958
|
|
6123
|
-
//
|
|
6124
|
-
//
|
|
6125
|
-
// `
|
|
6126
|
-
//
|
|
6127
|
-
|
|
6324
|
+
// ⟨SOUNDNESS R958, the DOWNCAST half⟩ An asserted downcast (`base as Sub`) IS a new conformer when the value is a
|
|
6325
|
+
// `Base`, and nothing here can tell that apart from a truthful one. A refusal stood here, scoped to assertions,
|
|
6326
|
+
// on the claim that "the receiver's class arm answers `Sub`'s subtree" — but the value's class is `Base`, which
|
|
6327
|
+
// is NOT in `Sub`'s subtree, so `Base.m` was the body that ran and the one no arm named. EXECUTED: `dO(bO as
|
|
6328
|
+
// SubO)` wrote a file through `BaseO.m` with `dO`/`rO` ABSENT and `pure a.dO` exit 0, while `dP(bP)` — the same
|
|
6329
|
+
// value without the `as` — was `Fs`, exit 1. An assertion must not delete a charge the unasserted conversion
|
|
6330
|
+
// makes. On a TRUTHFUL downcast the registration over-charges a `Sub`-typed receiver with `Base`'s override.
|
|
6128
6331
|
let added = false;
|
|
6129
6332
|
for (const anc of localAncestorsAndSelf(t)) added = pushUnique(classConformers, anc, c.node) || added;
|
|
6130
6333
|
if (c.kind === "lit") mintStructuralMembers(c.node, true);
|
|
@@ -6197,7 +6400,6 @@ function isConversionPosition(n) {
|
|
|
6197
6400
|
return false;
|
|
6198
6401
|
}
|
|
6199
6402
|
// 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
6403
|
const peelAssertions = (e) => unwrapArgExpr(e); // R780: one wrapper set, not a third copy of it
|
|
6202
6404
|
// R82 / PART 87: is the conversion's SOURCE type a fact about the VALUE, or only about a generic signature? A
|
|
6203
6405
|
// call whose declared return type mentions one of its own type parameters (`wrap<T>(x: T): T`) hands back
|
|
@@ -6395,21 +6597,19 @@ for (const sf of sources) {
|
|
|
6395
6597
|
// called as `launder(new LocalW())` ran LocalW's `m` (EXECUTED) with `deny Fs` exit 0 on the caller. The
|
|
6396
6598
|
// parameter's visible arguments say what it holds — the SAME provenance index R246's accessor arm asks
|
|
6397
6599
|
// (`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
|
-
|
|
6600
|
+
// ever (`recordConversion` keeps a target the type is assignable to). ⟨R958 widened-local half⟩ and the
|
|
6601
|
+
// same for a source typed `object`/`{}`, and a `const` declared wider than its initializer
|
|
6602
|
+
// (`uninformativeType`). A dependency-sourced value is R958's disclosed residue (`invisible`).
|
|
6603
|
+
if (uninformativeType(st)) for (const vt of receiverValueTypes(src)) {
|
|
6401
6604
|
try { recordConversion(vt, tt); } catch { /* best effort, as the main record below */ }
|
|
6402
6605
|
CONFORMER_REACH?.("any-param", `${path.relative(rootDir, sf.fileName)}:${at.getStart()}`);
|
|
6403
6606
|
}
|
|
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; }
|
|
6607
|
+
if (!CONFORMER_REACH) { recordConversion(st, tt); return; }
|
|
6608
|
+
const count = () => [...interfaceImpls.values(), ...foreignInterfaceImpls.values(), ...classConformers.values(),
|
|
6609
|
+
...depConformers.values()].reduce((a, x) => a + x.length, 0);
|
|
6610
|
+
const before = count();
|
|
6611
|
+
recordConversion(st, tt);
|
|
6612
|
+
if (count() > before) CONFORMER_REACH(kind, `${path.relative(rootDir, sf.fileName)}:${at.getStart()}`);
|
|
6413
6613
|
};
|
|
6414
6614
|
(function walkConversions(node) {
|
|
6415
6615
|
if (ts.isExpression(node) && !ts.isSpreadElement(node) && isConversionPosition(node)) {
|
|
@@ -6429,9 +6629,248 @@ for (const sf of sources) {
|
|
|
6429
6629
|
let tt; try { tt = checker.getTypeFromTypeNode(node.type); } catch { tt = undefined; }
|
|
6430
6630
|
convert(peelAssertions(node.expression), tt, node, "assertion");
|
|
6431
6631
|
}
|
|
6632
|
+
if (ts.isObjectLiteralExpression(node)) providerSubstitution(node); // ⟨R1061⟩
|
|
6432
6633
|
ts.forEachChild(node, walkConversions);
|
|
6433
6634
|
})(sf);
|
|
6434
6635
|
}
|
|
6636
|
+
// ⟨SOUNDNESS R1061⟩ A DEPENDENCY-INJECTION PROVIDER IS A CONVERSION THE CHECKER NEVER SEES. NestJS (and Angular,
|
|
6637
|
+
// which shares the shape) wires `{ provide: Store, useClass: FileStore }` — or `useValue: <obj>`, `useFactory: () =>
|
|
6638
|
+
// <obj>`, `useExisting: Other` — and the container then hands the substitute to every `store: Store` parameter. No
|
|
6639
|
+
// expression converts the substitute to `Store`, so a `this.store.save()` resolved to `Store.save` alone: EXECUTED on
|
|
6640
|
+
// NestJS 10, `FileStore.save` wrote a file while the calling method was ABSENT and `deny Fs`/`deny Fs Unknown` exited 0
|
|
6641
|
+
// for all three provider forms. The provider entry IS the conversion, so the substitute is registered as a conformer
|
|
6642
|
+
// of the token class — the union of the declared class and the visible substitute, through the R874/R954 conformer
|
|
6643
|
+
// arm every class dispatch already asks. Registered directly, without the checker's assignability test, because the
|
|
6644
|
+
// container does not apply one (a token class with a private member is not structurally assignable from its
|
|
6645
|
+
// substitute, and is still what gets injected). Only a token that names a LOCAL class registers; a string or symbol
|
|
6646
|
+
// token types its parameter by an interface or `any`, which the interface arm already answers or discloses.
|
|
6647
|
+
function providerSubstitution(lit) {
|
|
6648
|
+
const prop = (name) => lit.properties.find((p) => ts.isPropertyAssignment(p) && p.name && !ts.isComputedPropertyName(p.name)
|
|
6649
|
+
&& (p.name.text ?? p.name.getText()) === name);
|
|
6650
|
+
const provide = prop("provide");
|
|
6651
|
+
if (!provide) return;
|
|
6652
|
+
const targets = providerTokenClasses(provide.initializer);
|
|
6653
|
+
if (targets === null) { substituteForOpenToken(lit, prop); return; }
|
|
6654
|
+
if (!targets.length) { // ⟨R1061⟩ a string/symbol token: joined to its `@Inject(token)` parameters after the walk
|
|
6655
|
+
const key = injectTokenKey(provide.initializer);
|
|
6656
|
+
if (key) (tokenProviders.get(key) ?? tokenProviders.set(key, []).get(key)).push({ lit, prop });
|
|
6657
|
+
return;
|
|
6658
|
+
}
|
|
6659
|
+
for (const target of targets) substituteFor(lit, prop, target);
|
|
6660
|
+
}
|
|
6661
|
+
// ⟨SOUNDNESS R1061⟩ A TOKEN THE CHECKER CANNOT NAME (`provide: someType` typed `Type<any>`, a parameter whose callers
|
|
6662
|
+
// escape the scan). The container injects the substitute wherever that token is asked for, which may be ANY class
|
|
6663
|
+
// token, so the substitute joins the candidates of every project class whose instance type it is ASSIGNABLE to — the
|
|
6664
|
+
// classes it could stand in for without breaking its callers. A union, never a narrowing: a class it cannot
|
|
6665
|
+
// conform to keeps its own answer. A token typed `any` (its import unresolved) needs nothing here: a parameter
|
|
6666
|
+
// typed by that token is `any` too, and a dispatch on it already discloses.
|
|
6667
|
+
function substituteForOpenToken(lit, prop) {
|
|
6668
|
+
const projectClassDecls = substituteForOpenToken.classes ??= (() => {
|
|
6669
|
+
const out = [];
|
|
6670
|
+
const visit = (n) => { if (ts.isClassDeclaration(n) && n.name) out.push(n); ts.forEachChild(n, visit); };
|
|
6671
|
+
for (const sf of sources) visit(sf);
|
|
6672
|
+
return out;
|
|
6673
|
+
})();
|
|
6674
|
+
if (typeof checker.isTypeAssignableTo !== "function") return;
|
|
6675
|
+
const subs = [];
|
|
6676
|
+
for (const key of ["useClass", "useExisting"]) {
|
|
6677
|
+
const p = prop(key);
|
|
6678
|
+
if (!p) continue;
|
|
6679
|
+
let t; try { t = checker.getTypeAtLocation(peelAssertions(p.initializer)); } catch { t = undefined; }
|
|
6680
|
+
for (const u of t?.isUnion?.() ? t.types : [t])
|
|
6681
|
+
for (const sig of u?.getConstructSignatures?.() ?? []) { try { subs.push(sig.getReturnType()); } catch { /* none */ } }
|
|
6682
|
+
}
|
|
6683
|
+
const uv = prop("useValue");
|
|
6684
|
+
if (uv) { try { subs.push(checker.getTypeAtLocation(peelAssertions(uv.initializer))); } catch { /* none */ } }
|
|
6685
|
+
const uf = prop("useFactory");
|
|
6686
|
+
if (uf) {
|
|
6687
|
+
let ft; try { ft = checker.getTypeAtLocation(uf.initializer); } catch { ft = undefined; }
|
|
6688
|
+
for (const sig of ft?.getCallSignatures?.() ?? []) {
|
|
6689
|
+
let rt; try { rt = sig.getReturnType(); rt = checker.getAwaitedType?.(rt) ?? rt; } catch { rt = undefined; }
|
|
6690
|
+
if (rt) subs.push(rt);
|
|
6691
|
+
}
|
|
6692
|
+
}
|
|
6693
|
+
for (const st of subs) for (const s of st?.isUnion?.() ? st.types : [st]) {
|
|
6694
|
+
if (!s || (s.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))) continue;
|
|
6695
|
+
const c = conformerOfSourceType(s);
|
|
6696
|
+
if (!c || c.kind === "iface") continue;
|
|
6697
|
+
for (const cd of projectClassDecls) {
|
|
6698
|
+
if (cd === c.node) continue;
|
|
6699
|
+
let it; try { it = checker.getDeclaredTypeOfSymbol(checker.getSymbolAtLocation(cd.name)); } catch { it = undefined; }
|
|
6700
|
+
if (!it || !(it.getProperties?.().some((m) => m.flags & (ts.SymbolFlags.Method | ts.SymbolFlags.Property)))) continue;
|
|
6701
|
+
let ok = false; try { ok = checker.isTypeAssignableTo(s, it); } catch { ok = false; }
|
|
6702
|
+
if (ok && recordConformer(c, cd)) CONFORMER_REACH?.("di-open-token", `${path.relative(rootDir, lit.getSourceFile().fileName)}:${lit.getStart()}`);
|
|
6703
|
+
}
|
|
6704
|
+
}
|
|
6705
|
+
}
|
|
6706
|
+
// ⟨SOUNDNESS R1061, the TOKEN half⟩ WHICH CLASS A `provide:` TOKEN NAMES, asked of the CHECKER'S TYPE rather than of
|
|
6707
|
+
// the token's spelling. The first cut read the token's symbol and required it to BE a class declaration, so
|
|
6708
|
+
// `const TOK = Store6; { provide: TOK, useClass: FileStore6 }` named a variable and registered nothing: EXECUTED on
|
|
6709
|
+
// NestJS 10, `FileStore6.save` wrote its file, `runStore6` was ABSENT and `deny Fs`/`deny Fs Unknown` exited 0. The
|
|
6710
|
+
// value of the token at runtime is a class CONSTRUCTOR, and its type says which: `typeof Store6` through a const, an
|
|
6711
|
+
// import, an object member, a parameter fed at visible sites, or a `.map` over `[Store6]` alike. Returns the project
|
|
6712
|
+
// classes the token may be (a union token yields each), [] when the token is provably not a class constructor (a
|
|
6713
|
+
// string, a symbol, an `InjectionToken` instance — the interface arm's question), or null when the checker cannot say.
|
|
6714
|
+
function providerTokenClasses(expr, depth = 0, deps = false) {
|
|
6715
|
+
const e = peelAssertions(expr);
|
|
6716
|
+
if (!e || depth > 6) return null;
|
|
6717
|
+
let tsym; try { tsym = checker.getSymbolAtLocation(e); } catch { tsym = undefined; }
|
|
6718
|
+
if (tsym && (tsym.flags & ts.SymbolFlags.Alias)) { try { tsym = checker.getAliasedSymbol(tsym); } catch { tsym = undefined; } }
|
|
6719
|
+
const direct = (tsym?.declarations ?? []).find((d) => ts.isClassDeclaration(d) && (isProjectNode(d) || (deps && d.getSourceFile().isDeclarationFile)));
|
|
6720
|
+
if (direct) return [direct];
|
|
6721
|
+
// ⟨R1061⟩ THE VALUE BEFORE THE ANNOTATION. `const T: typeof Store = FileStore` holds FileStore; its type says Store.
|
|
6722
|
+
// A `const` binding answers with its initializer (and a const object literal's member with that member's value);
|
|
6723
|
+
// a reassignable binding's value is not its declaration's, so the type cannot be trusted either — open.
|
|
6724
|
+
const vd = tsym?.valueDeclaration;
|
|
6725
|
+
if (vd && ts.isVariableDeclaration(vd) && isProjectNode(vd)) {
|
|
6726
|
+
if (!(ts.getCombinedNodeFlags(vd) & ts.NodeFlags.Const) || !vd.initializer) return null;
|
|
6727
|
+
if (ts.isIdentifier(vd.name)) return providerTokenClasses(vd.initializer, depth + 1, deps);
|
|
6728
|
+
}
|
|
6729
|
+
if (vd && ts.isPropertyAssignment(vd) && isProjectNode(vd) && ts.isPropertyAccessExpression(e)) {
|
|
6730
|
+
const recv = unwrapArgExpr(e.expression);
|
|
6731
|
+
let rs; try { rs = ts.isIdentifier(recv) ? checker.getSymbolAtLocation(recv) : undefined; } catch { rs = undefined; }
|
|
6732
|
+
const rd = rs?.valueDeclaration;
|
|
6733
|
+
if (rd && ts.isVariableDeclaration(rd) && (ts.getCombinedNodeFlags(rd) & ts.NodeFlags.Const) && rd.initializer
|
|
6734
|
+
&& unwrapArgExpr(rd.initializer) === vd.parent) return providerTokenClasses(vd.initializer, depth + 1, deps);
|
|
6735
|
+
}
|
|
6736
|
+
let t; try { t = checker.getTypeAtLocation(e); } catch { return null; }
|
|
6737
|
+
if (!t) return null;
|
|
6738
|
+
const out = [];
|
|
6739
|
+
let open = false;
|
|
6740
|
+
for (const u of t.isUnion?.() ? t.types : [t]) {
|
|
6741
|
+
if (u.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.BooleanLike
|
|
6742
|
+
| ts.TypeFlags.Undefined | ts.TypeFlags.Null)) continue;
|
|
6743
|
+
if (!u.getConstructSignatures?.().length) {
|
|
6744
|
+
// A type that can HOLD a class without saying which — `Function`, `Object`, `{}`, `object`, `any`, `unknown`.
|
|
6745
|
+
const nm = u.getSymbol?.()?.getName?.();
|
|
6746
|
+
const libRoot = (nm === "Function" || nm === "Object" || nm === "CallableFunction" || nm === "NewableFunction")
|
|
6747
|
+
&& (u.getSymbol().declarations ?? []).every((d) => d.getSourceFile().isDeclarationFile && !isProjectNode(d));
|
|
6748
|
+
const emptyObj = (u.flags & ts.TypeFlags.Object) && !u.getProperties?.().length && !u.getCallSignatures?.().length;
|
|
6749
|
+
if (libRoot || emptyObj || (u.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.NonPrimitive | ts.TypeFlags.TypeParameter))) {
|
|
6750
|
+
open = true; continue;
|
|
6751
|
+
}
|
|
6752
|
+
// an object that is not a constructor (an `InjectionToken`, a plain token object) cannot be a class token
|
|
6753
|
+
if ((u.flags & ts.TypeFlags.Object) && !(u.getCallSignatures?.().length)) continue;
|
|
6754
|
+
open = true; continue;
|
|
6755
|
+
}
|
|
6756
|
+
const cd = (u.getSymbol?.()?.declarations ?? []).find((d) => ts.isClassDeclaration(d));
|
|
6757
|
+
if (cd && (isProjectNode(cd) || (deps && cd.getSourceFile().isDeclarationFile))) { if (!out.includes(cd)) out.push(cd); continue; }
|
|
6758
|
+
if (cd) continue; // a dependency's class as a TOKEN: its own dispatch is the dependency's ledger
|
|
6759
|
+
open = true; // a constructor TYPE with no class behind it (`Type<any>`, `new () => T`, `Function`)
|
|
6760
|
+
}
|
|
6761
|
+
if (open && ts.isIdentifier(e)) {
|
|
6762
|
+
// a PARAMETER whose type does not say (`token: Type<any>`): the classes its visible call sites pass, when the
|
|
6763
|
+
// function does not escape — a helper `provider(Store, FileStore)` is the shape.
|
|
6764
|
+
let ps = checker.getSymbolAtLocation(e);
|
|
6765
|
+
if (ps && (ps.flags & ts.SymbolFlags.Alias)) { try { ps = checker.getAliasedSymbol(ps); } catch { /* keep */ } }
|
|
6766
|
+
const args = ps && paramArgs.get(ps);
|
|
6767
|
+
if (args?.length && !paramFnEscaped(paramOwnerOf.get(ps))) {
|
|
6768
|
+
const viaArgs = [];
|
|
6769
|
+
for (const a of args) {
|
|
6770
|
+
const r = a ? providerTokenClasses(a, depth + 1, deps) : null;
|
|
6771
|
+
if (r === null) return out.length ? out : null;
|
|
6772
|
+
for (const c of r) if (!viaArgs.includes(c)) viaArgs.push(c);
|
|
6773
|
+
}
|
|
6774
|
+
for (const c of viaArgs) if (!out.includes(c)) out.push(c);
|
|
6775
|
+
return out;
|
|
6776
|
+
}
|
|
6777
|
+
}
|
|
6778
|
+
return open ? (out.length ? out : null) : out;
|
|
6779
|
+
}
|
|
6780
|
+
function substituteFor(lit, prop, target) {
|
|
6781
|
+
const sources = [];
|
|
6782
|
+
let open = false; // a substitute this scan cannot name stands in for `target`
|
|
6783
|
+
// ⟨R1061⟩ The INSTANCE types a `useClass`/`useExisting` value constructs, asked as the token is: the class symbol,
|
|
6784
|
+
// else the class(es) the expression's TYPE or its visible call sites name — `const IMPL = FileStore` and a helper
|
|
6785
|
+
// `prov(Store, FileStore)` alike. A constructor value the checker cannot name (`Type<Store>`, `any`) is OPEN.
|
|
6786
|
+
const fromCtorValue = (e) => {
|
|
6787
|
+
const cls = providerTokenClasses(e, 0, true);
|
|
6788
|
+
if (cls === null) { open = true; return; }
|
|
6789
|
+
for (const cd of cls) {
|
|
6790
|
+
let it; try { it = checker.getDeclaredTypeOfSymbol(cd.name ? checker.getSymbolAtLocation(cd.name) : cd.symbol); } catch { it = undefined; }
|
|
6791
|
+
if (it) sources.push(it); else open = true;
|
|
6792
|
+
}
|
|
6793
|
+
};
|
|
6794
|
+
for (const key of ["useClass", "useExisting"]) { const p = prop(key); if (p) fromCtorValue(p.initializer); }
|
|
6795
|
+
const anyish = (t) => !t || !!(t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown));
|
|
6796
|
+
const uv = prop("useValue");
|
|
6797
|
+
if (uv) { let t; try { t = checker.getTypeAtLocation(peelAssertions(uv.initializer)); } catch { t = undefined; } if (anyish(t)) open = true; else sources.push(t); }
|
|
6798
|
+
const uf = prop("useFactory");
|
|
6799
|
+
if (uf) {
|
|
6800
|
+
let ft; try { ft = checker.getTypeAtLocation(uf.initializer); } catch { ft = undefined; }
|
|
6801
|
+
const sigs = ft?.getCallSignatures?.() ?? [];
|
|
6802
|
+
if (!sigs.length) open = true;
|
|
6803
|
+
for (const sig of sigs) {
|
|
6804
|
+
let rt; try { rt = sig.getReturnType(); rt = checker.getAwaitedType?.(rt) ?? rt; } catch { rt = undefined; }
|
|
6805
|
+
if (anyish(rt)) open = true; else sources.push(rt);
|
|
6806
|
+
}
|
|
6807
|
+
}
|
|
6808
|
+
for (const st of sources) for (const s of st?.isUnion?.() ? st.types : [st]) {
|
|
6809
|
+
const c = conformerOfSourceType(s);
|
|
6810
|
+
if (c && recordConformer(c, target)) CONFORMER_REACH?.("di-provider", `${path.relative(rootDir, lit.getSourceFile().fileName)}:${lit.getStart()}`);
|
|
6811
|
+
}
|
|
6812
|
+
if (open && ts.isClassDeclaration(target)) {
|
|
6813
|
+
for (const a of localAncestorsAndSelf(target)) openSubstitutedClasses.add(a);
|
|
6814
|
+
CONFORMER_REACH?.("di-open-substitute", `${path.relative(rootDir, lit.getSourceFile().fileName)}:${lit.getStart()}`);
|
|
6815
|
+
}
|
|
6816
|
+
}
|
|
6817
|
+
// ⟨SOUNDNESS R1061, the STRING-TOKEN half⟩ `{ provide: "STORE", useClass: FileStore }` injected as
|
|
6818
|
+
// `@Inject("STORE") store: Store` — a parameter typed by a project CLASS under a non-class token. Nothing converts
|
|
6819
|
+
// FileStore to Store, so `this.store.save()` resolved to the pure `Store.save`: EXECUTED on NestJS 10, the substitute
|
|
6820
|
+
// wrote its file with the caller ABSENT, `deny Fs`/`deny Fs Unknown` exit 0. The token is the join key — its string
|
|
6821
|
+
// literal value (so `"STORE"` and `const STORE_TOKEN = "STORE"` match), else the declaration of the unique symbol or
|
|
6822
|
+
// const it names. Every provider under the key substitutes the parameter's declared type; a class-typed parameter
|
|
6823
|
+
// whose key NO visible provider registers (the list is built where this scan cannot see — a dependency's
|
|
6824
|
+
// `forRoot`) is an OPEN substitution and its dispatch discloses.
|
|
6825
|
+
function injectTokenKey(expr) {
|
|
6826
|
+
const e = peelAssertions(expr);
|
|
6827
|
+
if (!e) return null;
|
|
6828
|
+
let t; try { t = checker.getTypeAtLocation(e); } catch { t = undefined; }
|
|
6829
|
+
if (t && (t.flags & ts.TypeFlags.StringLiteral)) return `s:${t.value}`;
|
|
6830
|
+
if (t && (t.flags & ts.TypeFlags.NumberLiteral)) return `n:${t.value}`;
|
|
6831
|
+
let sym; try { sym = ts.isPropertyAccessExpression(e) ? checker.getSymbolAtLocation(e.name) : checker.getSymbolAtLocation(e); } catch { sym = undefined; }
|
|
6832
|
+
if (sym && (sym.flags & ts.SymbolFlags.Alias)) { try { sym = checker.getAliasedSymbol(sym); } catch { /* keep */ } }
|
|
6833
|
+
const d = sym?.valueDeclaration;
|
|
6834
|
+
if (!d || !(ts.isVariableDeclaration(d) || ts.isPropertyAssignment(d) || ts.isPropertyDeclaration(d))) return null;
|
|
6835
|
+
if (!tokenDeclIds.has(d)) tokenDeclIds.set(d, tokenDeclIds.size + 1);
|
|
6836
|
+
return `d:${tokenDeclIds.get(d)}`;
|
|
6837
|
+
}
|
|
6838
|
+
{
|
|
6839
|
+
const injectParams = [];
|
|
6840
|
+
const visitInject = (n) => {
|
|
6841
|
+
if (ts.isParameter(n) && n.type) {
|
|
6842
|
+
for (const dec of ts.getDecorators?.(n) ?? []) {
|
|
6843
|
+
const ce = dec.expression;
|
|
6844
|
+
if (ts.isCallExpression(ce) && ts.isIdentifier(ce.expression) && ce.expression.text === "Inject" && ce.arguments.length === 1) {
|
|
6845
|
+
const key = injectTokenKey(ce.arguments[0]);
|
|
6846
|
+
if (key) injectParams.push({ param: n, key });
|
|
6847
|
+
}
|
|
6848
|
+
}
|
|
6849
|
+
}
|
|
6850
|
+
ts.forEachChild(n, visitInject);
|
|
6851
|
+
};
|
|
6852
|
+
for (const sf of sources) visitInject(sf);
|
|
6853
|
+
for (const { param, key } of injectParams) {
|
|
6854
|
+
let tt; try { tt = checker.getTypeFromTypeNode(param.type); } catch { tt = undefined; }
|
|
6855
|
+
const targets = [];
|
|
6856
|
+
for (const u of tt?.isUnion?.() ? tt.types : [tt]) {
|
|
6857
|
+
const d = (u?.getSymbol?.()?.declarations ?? []).find((x) => (ts.isClassDeclaration(x) || ts.isInterfaceDeclaration(x)) && isProjectNode(x));
|
|
6858
|
+
if (d && !targets.includes(d)) targets.push(d);
|
|
6859
|
+
}
|
|
6860
|
+
if (!targets.length) continue;
|
|
6861
|
+
const provs = tokenProviders.get(key) ?? [];
|
|
6862
|
+
for (const target of targets) {
|
|
6863
|
+
if (!provs.length) {
|
|
6864
|
+
if (ts.isClassDeclaration(target)) {
|
|
6865
|
+
for (const a of localAncestorsAndSelf(target)) openSubstitutedClasses.add(a);
|
|
6866
|
+
CONFORMER_REACH?.("di-inject-unprovided", `${path.relative(rootDir, param.getSourceFile().fileName)}:${param.getStart()}`);
|
|
6867
|
+
}
|
|
6868
|
+
continue;
|
|
6869
|
+
}
|
|
6870
|
+
for (const { lit, prop } of provs) substituteFor(lit, prop, target);
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
}
|
|
6435
6874
|
// The interface-flow fixpoint: a `J`-typed value converted to `X` carries every conformer of `J` with it.
|
|
6436
6875
|
for (let changed = true, g = 0; changed && g++ < 32; ) {
|
|
6437
6876
|
changed = false;
|
|
@@ -6590,6 +7029,19 @@ function keyLiteralNames(t, depth = 0, seen = new Set()) {
|
|
|
6590
7029
|
// the accessor edges the value's type declares — and never hedges; a receiver neither rule answers is
|
|
6591
7030
|
// exactly as it was (the residual is stated in SOUNDNESS R246, not hidden here).
|
|
6592
7031
|
function anyishType(t) { return !!t && !!(t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)); } // hoisted: the conversion walk asks it first
|
|
7032
|
+
// ⟨SOUNDNESS R958, the WIDENED-LOCAL half⟩ A type that names no member is as silent about the value as `any`: a local
|
|
7033
|
+
// declared `object` or `{}` holding `new LocalW()` and asserted into an interface (`qDisp(o as Sink)`) ran
|
|
7034
|
+
// `LocalW.m` (EXECUTED) with every unit `[]` and no disclosure — `anyishType` alone let it through. Asked by the
|
|
7035
|
+
// conversion walk and the R246 receiver question alike; it only ever lets MORE value types be recorded.
|
|
7036
|
+
function uninformativeType(t) {
|
|
7037
|
+
if (!t) return false;
|
|
7038
|
+
if (anyishType(t) || (t.flags & ts.TypeFlags.NonPrimitive)) return true;
|
|
7039
|
+
if (!(t.flags & ts.TypeFlags.Object) || t.isUnion?.() || t.isIntersection?.()) return false;
|
|
7040
|
+
try {
|
|
7041
|
+
return checker.getPropertiesOfType(t).length === 0 && t.getCallSignatures().length === 0
|
|
7042
|
+
&& t.getConstructSignatures().length === 0 && !checker.getIndexInfosOfType?.(t)?.length;
|
|
7043
|
+
} catch { return false; }
|
|
7044
|
+
}
|
|
6593
7045
|
function receiverValueTypes(expr, depth = 0, seen = new Set()) {
|
|
6594
7046
|
if (!expr || depth > 6) return [];
|
|
6595
7047
|
let t; try { t = checker.getTypeAtLocation(expr); } catch { t = null; }
|
|
@@ -6599,10 +7051,18 @@ function receiverValueTypes(expr, depth = 0, seen = new Set()) {
|
|
|
6599
7051
|
let ti; try { ti = checker.getTypeAtLocation(inner); } catch { ti = null; }
|
|
6600
7052
|
return [...(ti && !anyishType(ti) ? [ti] : []), ...below];
|
|
6601
7053
|
}
|
|
6602
|
-
if (!
|
|
7054
|
+
if (!uninformativeType(t) || !ts.isIdentifier(expr)) return [];
|
|
6603
7055
|
const sym = checker.getSymbolAtLocation(expr);
|
|
6604
7056
|
const d = sym?.valueDeclaration;
|
|
6605
|
-
if (!sym || !d ||
|
|
7057
|
+
if (!sym || !d || seen.has(sym)) return [];
|
|
7058
|
+
// ⟨R958, widened-local half⟩ a `const` declared wider than its initializer holds the initializer's value.
|
|
7059
|
+
if (ts.isVariableDeclaration(d) && ts.isIdentifier(d.name) && d.initializer
|
|
7060
|
+
&& (ts.getCombinedNodeFlags(d) & ts.NodeFlags.Const)) {
|
|
7061
|
+
seen.add(sym);
|
|
7062
|
+
let ti; try { ti = checker.getTypeAtLocation(d.initializer); } catch { ti = null; }
|
|
7063
|
+
return [...(ti && !anyishType(ti) ? [ti] : []), ...receiverValueTypes(d.initializer, depth + 1, seen)];
|
|
7064
|
+
}
|
|
7065
|
+
if (!ts.isParameter(d)) return [];
|
|
6606
7066
|
seen.add(sym);
|
|
6607
7067
|
const args = paramArgs.get(sym);
|
|
6608
7068
|
if (!args || paramFnEscaped(paramOwnerOf.get(sym))) return [];
|
|
@@ -6610,7 +7070,7 @@ function receiverValueTypes(expr, depth = 0, seen = new Set()) {
|
|
|
6610
7070
|
for (const a of args) {
|
|
6611
7071
|
if (!a) continue;
|
|
6612
7072
|
let ta; try { ta = checker.getTypeAtLocation(a); } catch { ta = null; }
|
|
6613
|
-
if (ta && !anyishType(ta)) out.push(ta);
|
|
7073
|
+
if (ta && !anyishType(ta)) out.push(ta); // a VALUE's own type, even `{}`, is a fact about it
|
|
6614
7074
|
out.push(...receiverValueTypes(a, depth + 1, seen));
|
|
6615
7075
|
}
|
|
6616
7076
|
return out;
|
|
@@ -7197,13 +7657,11 @@ function moduleUnit(sf) {
|
|
|
7197
7657
|
// body's effects otherwise walked up in `enclosing` to the ClassDeclaration, which maps to the
|
|
7198
7658
|
// `C.constructor` unit, so a static-init effect was MISLABELED as the instance ctor (and carried no
|
|
7199
7659
|
// unitKind). Mint it as its own unit, lazily, mirroring `moduleUnit`. (An anonymous class expression's
|
|
7200
|
-
// static block
|
|
7660
|
+
// static block keyed under `<anonymous>` — one unit for every such class in the module, R1067; see `classUnitQual`.)
|
|
7201
7661
|
// The qual alone, WITHOUT minting — R782/R785's wiring pass asks "does this block's unit exist?" and must
|
|
7202
7662
|
// not create an empty one to find out. One derivation for both, so the two cannot spell it differently.
|
|
7203
7663
|
function staticBlockQual(node) {
|
|
7204
|
-
|
|
7205
|
-
const cname = (ts.isClassDeclaration(cls) || ts.isClassExpression(cls)) && cls.name ? cls.name.text : "<anonymous>";
|
|
7206
|
-
return `${moduleOf(node.getSourceFile())}.${cname}.<static-init>`;
|
|
7664
|
+
return classUnitQual(node.parent, "<static-init>"); // ⟨R1067⟩ one unit per CLASS, not per class name
|
|
7207
7665
|
}
|
|
7208
7666
|
function staticBlockUnit(node) {
|
|
7209
7667
|
const sf = node.getSourceFile();
|
|
@@ -7256,7 +7714,8 @@ function decoratorArgUnit(callNode) {
|
|
|
7256
7714
|
// that writes in its generated constructor (EXECUTED) went from `invisible:['mixdep']` to ABSENT, and nest's six
|
|
7257
7715
|
// `extends PartialType(…)` DTO constructors lost their `@nestjs/mapped-types` disclosure the same way.
|
|
7258
7716
|
function heritageUnit(cls, ctorQual) {
|
|
7259
|
-
const qual = ctorQual
|
|
7717
|
+
const qual = heritageQualOf(ctorQual);
|
|
7718
|
+
if (!qual) return ctorQual; // not a constructor unit's key: attribute where the climb already was
|
|
7260
7719
|
if (!fns.has(qual)) {
|
|
7261
7720
|
const sf = cls.getSourceFile();
|
|
7262
7721
|
const h = (cls.heritageClauses ?? []).find((c) => c.token === ts.SyntaxKind.ExtendsKeyword) ?? cls;
|
|
@@ -7327,6 +7786,12 @@ function enclosing(node) {
|
|
|
7327
7786
|
&& (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Static)
|
|
7328
7787
|
&& p.parent && (ts.isClassDeclaration(p.parent) || ts.isClassExpression(p.parent)))
|
|
7329
7788
|
{ R815_REACH?.("static"); return staticBlockUnit(p); }
|
|
7789
|
+
// ⟨R1016⟩ an INSTANCE field initialiser of a class expression runs at construction — its constructor unit.
|
|
7790
|
+
if (prev && ts.isPropertyDeclaration(p) && p.initializer === prev
|
|
7791
|
+
&& !(ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Static)
|
|
7792
|
+
&& !ts.isArrowFunction(prev) && !ts.isFunctionExpression(prev)
|
|
7793
|
+
&& p.parent && ts.isClassExpression(p.parent) && classExprCtorQual.has(p.parent))
|
|
7794
|
+
return classExprCtorQual.get(p.parent);
|
|
7330
7795
|
if (ts.isHeritageClause(p) && p.token === ts.SyntaxKind.ExtendsKeyword && p.parent
|
|
7331
7796
|
&& (ts.isClassDeclaration(p.parent) || ts.isClassExpression(p.parent))) {
|
|
7332
7797
|
const ctor = nodeName.get(p.parent);
|
|
@@ -8342,8 +8807,8 @@ function isStringifyingSink(call) {
|
|
|
8342
8807
|
const member = ex.name?.getText?.();
|
|
8343
8808
|
// (a) `console.*` — every console method runs its arguments through util.format/inspect.
|
|
8344
8809
|
if (ts.isIdentifier(ex.expression) && ex.expression.getText() === "console") {
|
|
8345
|
-
const
|
|
8346
|
-
return !
|
|
8810
|
+
// the GLOBAL console; a local `const console = …` shadow is not a sink (⟨R1074⟩ an expando write is not one)
|
|
8811
|
+
return !projectShadows(checker.getSymbolAtLocation(ex.expression)?.declarations);
|
|
8347
8812
|
}
|
|
8348
8813
|
// (b) a logging LEVEL-method on an EXTERNAL receiver — pino/winston/bunyan/log4js/debug, the direct
|
|
8349
8814
|
// analogue of the SLF4J parameterized call this vein was found on. A LOCAL logger is excluded: its
|
|
@@ -8841,6 +9306,30 @@ const declImportsNodeProcess = (decl) => {
|
|
|
8841
9306
|
// downstream consult — the fix in one place, per brief §F1 item 3 ("make the two paths share one
|
|
8842
9307
|
// authority", not patch the losing copy). Populated by the pre-pass immediately below.
|
|
8843
9308
|
const processAliasSymbols = new Set();
|
|
9309
|
+
// ⟨SOUNDNESS R1075⟩ AN IDENTIFIER IMPORTED FROM NODE'S `process` MODULE. `import { env } from "node:process"` binds the
|
|
9310
|
+
// environment object itself, `import { argv }` the argument vector, and `import p from` / `import * as p from` the
|
|
9311
|
+
// process object — under ANY local name. The identity tests below asked for the spelling `process` (and the default
|
|
9312
|
+
// import only when its local name was `process`), so every one of these was ABSENT: EXECUTED (`fx75`), `env.SECRET`,
|
|
9313
|
+
// `envAlias.SECRET`, `nsp.env.SECRET` and `procDefault.env.SECRET` each printed the planted value, each function was
|
|
9314
|
+
// missing from the report and `deny Env` exited 0; rollup's `src/utils/colors.ts` reads `env.FORCE_COLOR` this way.
|
|
9315
|
+
// Asked of the import's TARGET, not its spelling: the alias resolves to node's own `process` variable or to a member
|
|
9316
|
+
// of its `Process` interface, which also covers a project re-export of either. Returns "process", the member name,
|
|
9317
|
+
// or null. A declaration position (the import specifier itself, an export specifier) is not a reference.
|
|
9318
|
+
const processModuleBinding = (id) => {
|
|
9319
|
+
if (!id || !ts.isIdentifier(id)) return null;
|
|
9320
|
+
const p = id.parent;
|
|
9321
|
+
if (p && (ts.isImportSpecifier(p) || ts.isExportSpecifier(p) || ts.isImportClause(p) || ts.isNamespaceImport(p)
|
|
9322
|
+
|| ts.isImportEqualsDeclaration(p) || ts.isNamespaceExport?.(p))) return null;
|
|
9323
|
+
let sym = checker.getSymbolAtLocation(id);
|
|
9324
|
+
if (!sym || !(sym.flags & ts.SymbolFlags.Alias)) return null;
|
|
9325
|
+
try { sym = checker.getAliasedSymbol(sym); } catch { return null; }
|
|
9326
|
+
const decls = sym?.declarations ?? [];
|
|
9327
|
+
const external = (d) => !projectFiles.has(path.resolve(d.getSourceFile().fileName));
|
|
9328
|
+
if (decls.some((d) => external(d) && ts.isVariableDeclaration(d) && ts.isIdentifier(d.name) && d.name.text === "process")) return "process";
|
|
9329
|
+
const m = decls.find((d) => external(d) && (ts.isPropertySignature(d) || ts.isMethodSignature(d) || ts.isPropertyDeclaration(d))
|
|
9330
|
+
&& d.parent && ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Process");
|
|
9331
|
+
return m && m.name && ts.isIdentifier(m.name) ? m.name.text : null;
|
|
9332
|
+
};
|
|
8844
9333
|
const identIsGlobalProcess = (id0) => {
|
|
8845
9334
|
// ⟨SOUNDNESS R936⟩ through the ONE transparent-wrapper set (R780): `(process as any).argv`, `(<any>process).argv`
|
|
8846
9335
|
// and even `(process).argv` were ABSENT because the argv arm handed this the wrapped node and this matched an
|
|
@@ -8862,14 +9351,15 @@ const identIsGlobalProcess = (id0) => {
|
|
|
8862
9351
|
const root = unwrapArgExpr(id.expression); // R780: `(<any>globalThis).process` is the same object
|
|
8863
9352
|
if (ts.isIdentifier(root) && (root.text === "globalThis" || root.text === "global")) {
|
|
8864
9353
|
const gd = checker.getSymbolAtLocation(root)?.declarations ?? [];
|
|
8865
|
-
return !gd
|
|
9354
|
+
return !projectShadows(gd); // ⟨R1074⟩
|
|
8866
9355
|
}
|
|
8867
9356
|
}
|
|
8868
9357
|
if (!ts.isIdentifier(id)) return false;
|
|
9358
|
+
if (processModuleBinding(id) === "process") return true; // ⟨R1075⟩ `import p from "node:process"`, any name
|
|
8869
9359
|
if (id.text === "process") {
|
|
8870
9360
|
const decls = checker.getSymbolAtLocation(id)?.declarations ?? [];
|
|
8871
9361
|
if (decls.some(declImportsNodeProcess)) return true; // `import process from 'node:process'`
|
|
8872
|
-
if (!decls
|
|
9362
|
+
if (!projectShadows(decls)) return true; // ambient global ⟨R1074⟩: an expando write is not a shadow
|
|
8873
9363
|
}
|
|
8874
9364
|
// A local binding under ANY name (`p`, `process`, whatever) whose sole initializer is confirmed —
|
|
8875
9365
|
// by the pre-pass below — to be the global process object itself.
|
|
@@ -8900,7 +9390,7 @@ const identIsGlobalProcess = (id0) => {
|
|
|
8900
9390
|
const root = unwrapArgExpr(node.initializer); // R780
|
|
8901
9391
|
if (ts.isIdentifier(root) && (root.text === "globalThis" || root.text === "global")) {
|
|
8902
9392
|
const gd = checker.getSymbolAtLocation(root)?.declarations ?? [];
|
|
8903
|
-
if (!gd
|
|
9393
|
+
if (!projectShadows(gd)) { // ⟨R1074⟩
|
|
8904
9394
|
for (const el of node.name.elements) {
|
|
8905
9395
|
const propName = el.propertyName ? (ts.isIdentifier(el.propertyName) ? el.propertyName.text : null)
|
|
8906
9396
|
: (ts.isIdentifier(el.name) ? el.name.text : null);
|
|
@@ -8944,6 +9434,7 @@ const unwrapEnvTransparent = (e) => unwrapArgExpr(e); // R780: one wrapper set
|
|
|
8944
9434
|
// project-local `process` (a parameter, a module `const`) matches nothing.
|
|
8945
9435
|
const isProcessEnvExpr = (expr) => {
|
|
8946
9436
|
if (!expr) return false;
|
|
9437
|
+
if (ts.isIdentifier(expr) && processModuleBinding(expr) === "env") return true; // ⟨R1075⟩ `import { env } from "node:process"`
|
|
8947
9438
|
let obj = null;
|
|
8948
9439
|
if (ts.isPropertyAccessExpression(expr) && expr.name.text === "env") obj = expr.expression;
|
|
8949
9440
|
else if (ts.isElementAccessExpression(expr) && expr.argumentExpression
|
|
@@ -8961,7 +9452,8 @@ const isProcessEnvExpr = (expr) => {
|
|
|
8961
9452
|
// it "flooded a scala-library scan with a spurious 14k Env" — JVM `-D` config read pervasively at
|
|
8962
9453
|
// class-init. `argv` is read once in a main.)
|
|
8963
9454
|
const isProcessArgvExpr = (expr) =>
|
|
8964
|
-
expr && ts.isPropertyAccessExpression(expr) && expr.name.text === "argv" && identIsGlobalProcess(expr.expression)
|
|
9455
|
+
expr && ((ts.isPropertyAccessExpression(expr) && expr.name.text === "argv" && identIsGlobalProcess(expr.expression))
|
|
9456
|
+
|| (ts.isIdentifier(expr) && processModuleBinding(expr) === "argv")); // ⟨R1075⟩ `import { argv } from "node:process"`
|
|
8965
9457
|
|
|
8966
9458
|
// The LEAVES a value can come from: through the transparent wrappers, both arms of `?:`, either side of
|
|
8967
9459
|
// `??`/`||`, the right of `&&`/`,`, and the right of an assignment expression. A merge is a MAY-flow, and
|
|
@@ -9034,12 +9526,90 @@ const envValueLeaves = (expr, out = []) => {
|
|
|
9034
9526
|
// this adds the READERS and removes nothing.
|
|
9035
9527
|
const envAliasSymbols = new Set();
|
|
9036
9528
|
const envMayAliasSymbols = new Set();
|
|
9529
|
+
// ⟨SOUNDNESS R1075⟩ the symbol a REFERENCE names, through an import: `import { myEnv } from "./cfg"` over
|
|
9530
|
+
// `export const myEnv = process.env` is the exported const, whose symbol is in the alias sets — the import's own
|
|
9531
|
+
// alias symbol never is. EXECUTED (`fx75c`): `viaExportedAlias() { return myEnv.SECRET }` printed the planted value
|
|
9532
|
+
// and was ABSENT, `deny Env` exit 0, while the same read in the declaring module is `['Env']`.
|
|
9533
|
+
const envValueSymAt = (id) => {
|
|
9534
|
+
let sym = checker.getSymbolAtLocation(id);
|
|
9535
|
+
if (sym && (sym.flags & ts.SymbolFlags.Alias)) { try { sym = checker.getAliasedSymbol(sym); } catch { /* unresolved */ } }
|
|
9536
|
+
return sym;
|
|
9537
|
+
};
|
|
9037
9538
|
// ⟨R935⟩ property locations, kept apart from the local sets so a property name never reaches the
|
|
9038
9539
|
// identifier-keyed consumers (2c's `envSourceKind`, the alias-binding inert rule) by accident.
|
|
9039
9540
|
const envPropMustSymbols = new Set();
|
|
9040
9541
|
const envPropMaySymbols = new Set();
|
|
9041
9542
|
// ⟨R935⟩ a property location's identity: its first declaration node (see `memberSymOf` for why not the symbol).
|
|
9042
9543
|
const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
|
|
9544
|
+
// ⟨SOUNDNESS R935, the RECEIVER half⟩ THE LOCATION A MEMBER READ NAMES IS THE OBJECT THE RECEIVER HOLDS, not the
|
|
9545
|
+
// member its DECLARED TYPE declares. The first cut keyed a read on the checker's property symbol, which is the
|
|
9546
|
+
// literal's own property only when the binding's type was INFERRED from the literal. Give the binding a type —
|
|
9547
|
+
// `const icfg: Cfg = { env: process.env }` — and `icfg.env` resolves to `Cfg`'s `PropertySignature`, an open set
|
|
9548
|
+
// that is no location at all, so `viaIface() { return icfg.env.SECRET }` was ABSENT with `deny Env viaIface` exit
|
|
9549
|
+
// 0 over a read that printed the planted secret (EXECUTED). An ARRAY literal had no location to key at all:
|
|
9550
|
+
// `const arr = [process.env]; arr[0].SECRET`, same measurement. Where the receiver is an identifier bound by a
|
|
9551
|
+
// `const` to an object or array literal, the object IS that literal, so the read is keyed on the literal's own
|
|
9552
|
+
// member — the last property of that name (a later spread makes it unresolvable), or the element at a literal
|
|
9553
|
+
// index with no spread or hole before it. Everything that can REBIND such a member off the books is a binding
|
|
9554
|
+
// the fixpoint must see, or the location is charged over a replaced value:
|
|
9555
|
+
// · an OBJECT literal's member is written through its declared member too (`icfg.env = {}` names `Cfg.env`,
|
|
9556
|
+
// as does any other `Cfg`-typed object), so a read resolved this way is MAY wherever that declared member
|
|
9557
|
+
// is ever written or reflectively written (`envDeclWritten`, filled by the binding walk below);
|
|
9558
|
+
// · an ARRAY's elements move under any method call or escape, so an element is MUST only while every
|
|
9559
|
+
// reference to the array is a literal-index READ; any other reference makes every element MAY.
|
|
9560
|
+
// MAY discloses `Unknown[env-maybe-read]`, as every other location does. A `let` binding, a receiver that is a
|
|
9561
|
+
// parameter or a property, and a read through iteration (`for…of`, `.map`) are not resolved here.
|
|
9562
|
+
const envDeclWritten = new Set();
|
|
9563
|
+
let envBindingTable = null; // ⟨R935⟩ the binding table, published for the typed floor (`envFloorOpaque`)
|
|
9564
|
+
const envLitElems = new Map(); // array-literal element node -> true while every reference is an index read
|
|
9565
|
+
const envConstLiteral = (id) => {
|
|
9566
|
+
if (!id || !ts.isIdentifier(id)) return null;
|
|
9567
|
+
let sym = checker.getSymbolAtLocation(id);
|
|
9568
|
+
if (sym && (sym.flags & ts.SymbolFlags.Alias)) { try { sym = checker.getAliasedSymbol(sym); } catch { return null; } }
|
|
9569
|
+
const d = sym?.valueDeclaration;
|
|
9570
|
+
if (!d || !ts.isVariableDeclaration(d) || !ts.isIdentifier(d.name) || !d.initializer
|
|
9571
|
+
|| !(ts.getCombinedNodeFlags(d) & ts.NodeFlags.Const)
|
|
9572
|
+
|| !projectFiles.has(path.resolve(d.getSourceFile().fileName))) return null;
|
|
9573
|
+
const lit = unwrapArgExpr(d.initializer);
|
|
9574
|
+
return lit && (ts.isObjectLiteralExpression(lit) || ts.isArrayLiteralExpression(lit)) ? lit : null;
|
|
9575
|
+
};
|
|
9576
|
+
const envMemberName = (p) => (p.name && (ts.isIdentifier(p.name) || ts.isStringLiteralLike(p.name)
|
|
9577
|
+
|| ts.isNumericLiteral(p.name) || ts.isPrivateIdentifier(p.name))) ? p.name.text : null;
|
|
9578
|
+
// -> { key, declKey } for a member read whose receiver is a const literal, else null.
|
|
9579
|
+
const envLiteralMember = (n) => {
|
|
9580
|
+
let recv, name = null, idx = null;
|
|
9581
|
+
if (ts.isPropertyAccessExpression(n)) { recv = n.expression; name = n.name.text; }
|
|
9582
|
+
else if (ts.isElementAccessExpression(n) && n.argumentExpression) {
|
|
9583
|
+
recv = n.expression;
|
|
9584
|
+
if (ts.isStringLiteralLike(n.argumentExpression)) name = n.argumentExpression.text;
|
|
9585
|
+
else if (ts.isNumericLiteral(n.argumentExpression)) idx = Number(n.argumentExpression.text);
|
|
9586
|
+
else return null;
|
|
9587
|
+
} else return null;
|
|
9588
|
+
const lit = envConstLiteral(unwrapArgExpr(recv));
|
|
9589
|
+
if (!lit) return null;
|
|
9590
|
+
if (ts.isArrayLiteralExpression(lit)) {
|
|
9591
|
+
if (idx === null || !Number.isInteger(idx) || idx >= lit.elements.length) return null;
|
|
9592
|
+
for (let i = 0; i <= idx; i++) {
|
|
9593
|
+
const el = lit.elements[i];
|
|
9594
|
+
if (ts.isSpreadElement(el) || ts.isOmittedExpression(el)) return null;
|
|
9595
|
+
}
|
|
9596
|
+
return { key: lit.elements[idx], declKey: null };
|
|
9597
|
+
}
|
|
9598
|
+
const want = name ?? (idx !== null ? String(idx) : null);
|
|
9599
|
+
if (want === null) return null;
|
|
9600
|
+
let hit = null;
|
|
9601
|
+
for (const p of lit.properties) {
|
|
9602
|
+
if (ts.isSpreadAssignment(p)) hit = p;
|
|
9603
|
+
else if (envMemberName(p) === want) hit = p;
|
|
9604
|
+
}
|
|
9605
|
+
if (!hit || !(ts.isPropertyAssignment(hit) || ts.isShorthandPropertyAssignment(hit))) return null;
|
|
9606
|
+
const key = envLocKey(checker.getSymbolAtLocation(hit.name));
|
|
9607
|
+
if (!key) return null;
|
|
9608
|
+
let raw = null;
|
|
9609
|
+
try { raw = ts.isPropertyAccessExpression(n) ? checker.getSymbolAtLocation(n.name)
|
|
9610
|
+
: (n.argumentExpression ? checker.getSymbolAtLocation(n.argumentExpression) : null); } catch { raw = null; }
|
|
9611
|
+
return { key, declKey: envLocKey(raw) };
|
|
9612
|
+
};
|
|
9043
9613
|
{
|
|
9044
9614
|
const ENV_DESTRUCTURED = Symbol("env-destructured");
|
|
9045
9615
|
const bindings = new Map(); // symbol -> [value expr | null | ENV_DESTRUCTURED | { same: symbol }]
|
|
@@ -9056,7 +9626,7 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
|
|
|
9056
9626
|
const memberRawSym = (n) => (ts.isPropertyAccessExpression(n) ? checker.getSymbolAtLocation(n.name)
|
|
9057
9627
|
: (ts.isElementAccessExpression(n) && n.argumentExpression && ts.isStringLiteralLike(n.argumentExpression))
|
|
9058
9628
|
? checker.getSymbolAtLocation(n.argumentExpression) : null) ?? null;
|
|
9059
|
-
const memberSymOf = (n) => envLocKey(memberRawSym(n));
|
|
9629
|
+
const memberSymOf = (n) => envLiteralMember(n)?.key ?? envLocKey(memberRawSym(n));
|
|
9060
9630
|
// Is this property symbol a location whose bindings we can enumerate? Every declaration must be
|
|
9061
9631
|
// value-bearing, in a project file, and not a member of an extended class.
|
|
9062
9632
|
const propEligible = (sym) => {
|
|
@@ -9073,7 +9643,12 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
|
|
|
9073
9643
|
if (!ok) propIneligible.add(sym);
|
|
9074
9644
|
return ok;
|
|
9075
9645
|
};
|
|
9076
|
-
const noteProp = (sym, v) => {
|
|
9646
|
+
const noteProp = (sym, v) => {
|
|
9647
|
+
if (propEligible(sym)) { const k = envLocKey(sym); propSyms.add(k); note(k, v); }
|
|
9648
|
+
else if (sym && v !== undefined) envDeclWritten.add(envLocKey(sym)); // ⟨R935⟩ see `envLiteralMember`
|
|
9649
|
+
};
|
|
9650
|
+
// ⟨R935⟩ a WRITE through a member that is not itself a location still rebinds whatever object it lands on.
|
|
9651
|
+
const noteWrite = (sym, v) => { if (sym) { envDeclWritten.add(envLocKey(sym)); noteProp(sym, v); } };
|
|
9077
9652
|
const collect = (node) => {
|
|
9078
9653
|
if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
|
|
9079
9654
|
note(checker.getSymbolAtLocation(node.name), node.initializer ?? null);
|
|
@@ -9091,7 +9666,10 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
|
|
|
9091
9666
|
&& (ts.isPropertyAccessExpression(node.left) || ts.isElementAccessExpression(node.left))) {
|
|
9092
9667
|
// ⟨R935⟩ an assignment to a property location; a compound operator (`+=`) binds a non-environment.
|
|
9093
9668
|
const s = memberRawSym(node.left);
|
|
9094
|
-
if (s)
|
|
9669
|
+
if (s) noteWrite(s, LOGICAL_ASSIGN.has(node.operatorToken.kind) ? node.right : null);
|
|
9670
|
+
// ⟨R935⟩ …and the const literal's own member, when the receiver resolves to one.
|
|
9671
|
+
const lm = envLiteralMember(node.left);
|
|
9672
|
+
if (lm && !ts.isArrayLiteralExpression(lm.key.parent)) { propSyms.add(lm.key); note(lm.key, LOGICAL_ASSIGN.has(node.operatorToken.kind) ? node.right : null); }
|
|
9095
9673
|
} else if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {
|
|
9096
9674
|
const s = checker.getSymbolAtLocation(node.name);
|
|
9097
9675
|
if (s && (s.flags & ts.SymbolFlags.Property)) noteProp(s, node.initializer);
|
|
@@ -9126,27 +9704,80 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
|
|
|
9126
9704
|
if (reflectTarget) {
|
|
9127
9705
|
let props = [];
|
|
9128
9706
|
try { props = checker.getTypeAtLocation(reflectTarget)?.getProperties?.() ?? []; } catch { props = []; }
|
|
9129
|
-
for (const ps of props) if (propEligible(ps)) noteProp(ps, null);
|
|
9707
|
+
for (const ps of props) { envDeclWritten.add(envLocKey(ps)); if (propEligible(ps)) noteProp(ps, null); }
|
|
9708
|
+
// ⟨R935⟩ a const object literal written reflectively: every member of the literal itself is rebound.
|
|
9709
|
+
const tl = envConstLiteral(unwrapArgExpr(reflectTarget));
|
|
9710
|
+
if (tl && ts.isObjectLiteralExpression(tl)) for (const p of tl.properties) {
|
|
9711
|
+
const k = (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) ? envLocKey(checker.getSymbolAtLocation(p.name)) : null;
|
|
9712
|
+
if (k) { propSyms.add(k); note(k, null); }
|
|
9713
|
+
}
|
|
9714
|
+
}
|
|
9715
|
+
// ⟨R935⟩ an ARRAY literal bound by a `const`: each element is a location holding its own value.
|
|
9716
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer
|
|
9717
|
+
&& (ts.getCombinedNodeFlags(node) & ts.NodeFlags.Const)) {
|
|
9718
|
+
const al = unwrapArgExpr(node.initializer);
|
|
9719
|
+
if (al && ts.isArrayLiteralExpression(al)) {
|
|
9720
|
+
for (const el of al.elements) if (!ts.isSpreadElement(el) && !ts.isOmittedExpression(el)) {
|
|
9721
|
+
propSyms.add(el); note(el, el); envLitElems.set(el, al);
|
|
9722
|
+
}
|
|
9723
|
+
}
|
|
9130
9724
|
}
|
|
9131
9725
|
ts.forEachChild(node, collect);
|
|
9132
9726
|
};
|
|
9133
9727
|
for (const sf of sources) collect(sf);
|
|
9134
|
-
// ⟨
|
|
9135
|
-
|
|
9728
|
+
// ⟨R935⟩ an array literal's elements are MUST only while every reference to the array is a literal-index READ.
|
|
9729
|
+
if (envLitElems.size) {
|
|
9730
|
+
const arrs = new Set(envLitElems.values());
|
|
9731
|
+
const names = new Set([...arrs].map((a) => a.parent?.name?.text).filter(Boolean));
|
|
9732
|
+
const opened = new Set();
|
|
9733
|
+
const visitRefs = (node) => {
|
|
9734
|
+
if (ts.isIdentifier(node) && names.has(node.text)) {
|
|
9735
|
+
const lit = envConstLiteral(node);
|
|
9736
|
+
if (lit && arrs.has(lit) && !opened.has(lit) && !(lit.parent?.name === node)) {
|
|
9737
|
+
let cur = node;
|
|
9738
|
+
while (cur.parent && (ts.isParenthesizedExpression(cur.parent) || ts.isNonNullExpression(cur.parent)
|
|
9739
|
+
|| ts.isAsExpression(cur.parent) || ts.isSatisfiesExpression?.(cur.parent))) cur = cur.parent;
|
|
9740
|
+
const p = cur.parent;
|
|
9741
|
+
const indexRead = p && ts.isElementAccessExpression(p) && p.expression === cur && p.argumentExpression
|
|
9742
|
+
&& ts.isNumericLiteral(p.argumentExpression)
|
|
9743
|
+
&& !(p.parent && ts.isBinaryExpression(p.parent) && p.parent.left === p && p.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && p.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
|
|
9744
|
+
&& !(p.parent && (ts.isDeleteExpression(p.parent) || ts.isPrefixUnaryExpression(p.parent) || ts.isPostfixUnaryExpression(p.parent)))
|
|
9745
|
+
&& !isDestructuringAssignTarget(p);
|
|
9746
|
+
const typeOnly = p && (ts.isTypeQueryNode(p) || ts.isExportSpecifier(p));
|
|
9747
|
+
if (!indexRead && !typeOnly) opened.add(lit);
|
|
9748
|
+
}
|
|
9749
|
+
}
|
|
9750
|
+
ts.forEachChild(node, visitRefs);
|
|
9751
|
+
};
|
|
9752
|
+
for (const sf of sources) visitRefs(sf);
|
|
9753
|
+
for (const [el, al] of envLitElems) if (opened.has(al)) note(el, null);
|
|
9754
|
+
}
|
|
9755
|
+
// ⟨R934⟩ the parameter bindings come from the shared PARAMETER PROVENANCE index above. What `collect` noted for
|
|
9756
|
+
// a parameter BEFORE this line is its in-body REASSIGNMENTS (`e = {}`); kept apart, because the two kinds of
|
|
9757
|
+
// binding answer different questions below.
|
|
9758
|
+
const paramAssigns = new Map();
|
|
9759
|
+
for (const [ps, vals] of paramArgs) {
|
|
9760
|
+
paramSyms.add(ps); paramAssigns.set(ps, [...(bindings.get(ps) ?? [])]);
|
|
9761
|
+
for (const v of vals) note(ps, v);
|
|
9762
|
+
}
|
|
9136
9763
|
const leafIn = (leaf, set, propSet) => isProcessEnvExpr(leaf)
|
|
9137
|
-
|| (ts.isIdentifier(leaf) && set.has(
|
|
9138
|
-
|| (!!propSet && propSet.has(memberSymOf(leaf)));
|
|
9764
|
+
|| (ts.isIdentifier(leaf) && set.has(envValueSymAt(leaf))) // ⟨R1075⟩ through an import
|
|
9765
|
+
|| (!!propSet && propSet.has(memberSymOf(leaf)) && !(propSet === envPropMustSymbols && envLitDowngraded(leaf)));
|
|
9766
|
+
// ⟨R935⟩ a literal member read through a receiver whose DECLARED member is written elsewhere is MAY at best.
|
|
9767
|
+
function envLitDowngraded(n) {
|
|
9768
|
+
const lm = (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n)) ? envLiteralMember(n) : null;
|
|
9769
|
+
return !!lm && !!lm.declKey && lm.declKey !== lm.key && envDeclWritten.has(lm.declKey);
|
|
9770
|
+
}
|
|
9139
9771
|
const hasEnvLeaf = (v, set, propSet) => v === ENV_DESTRUCTURED
|
|
9140
9772
|
|| (!!v && typeof v === "object" && "same" in v ? set.has(v.same) || propSet?.has(v.same) === true
|
|
9141
9773
|
: (!!v && typeof v === "object" && envValueLeaves(v).some((l) => leafIn(l, set, propSet))));
|
|
9142
9774
|
|
|
9143
9775
|
|
|
9144
|
-
// MAY: least fixpoint of "some binding has a leaf that is process.env or already MAY" — LOCALS
|
|
9145
|
-
//
|
|
9776
|
+
// MAY: least fixpoint of "some binding has a leaf that is process.env or already MAY" — LOCALS, PROPERTIES
|
|
9777
|
+
// and (⟨R934⟩, see MUST below) PARAMETERS: a property's MAY reads a local's MAY and vice versa.
|
|
9146
9778
|
for (let changed = true, n = 0; changed && n < 64; n++) {
|
|
9147
9779
|
changed = false;
|
|
9148
9780
|
for (const [sym, vals] of bindings) {
|
|
9149
|
-
if (paramSyms.has(sym)) continue;
|
|
9150
9781
|
const isProp = propSyms.has(sym);
|
|
9151
9782
|
const set = isProp ? envPropMaySymbols : envMayAliasSymbols;
|
|
9152
9783
|
if (!set.has(sym) && vals.some((v) => hasEnvLeaf(v, envMayAliasSymbols, envPropMaySymbols))) { set.add(sym); changed = true; }
|
|
@@ -9154,40 +9785,54 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
|
|
|
9154
9785
|
}
|
|
9155
9786
|
// MUST: least fixpoint (from empty) of "every binding has a leaf that is process.env or already MUST" —
|
|
9156
9787
|
// 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
|
-
|
|
9788
|
+
// properties start from their MAY sets.
|
|
9789
|
+
//
|
|
9790
|
+
// ⟨SOUNDNESS R934, the DIVERGENT half⟩ A PARAMETER IS THE ENVIRONMENT IN ITS READER'S FRAME WHEN *SOME* VISIBLE
|
|
9791
|
+
// CALL SITE HANDS IT THE ENVIRONMENT — not every one. The first cut of R934 required every site, citing pass
|
|
9792
|
+
// 2b's divergent-HOF ruling, and left `readDiv(e) { return e.SECRET }` called as `readDiv(process.env)` and
|
|
9793
|
+
// `readDiv({…})` ABSENT with `deny Env readDiv` exit 0 over a read that printed the planted secret (EXECUTED).
|
|
9794
|
+
// 2b's ruling does not transfer: it is about a CALLBACK, whose effect is another unit's body, so the HOF
|
|
9795
|
+
// performs nothing itself and pooling the callbacks onto it would invent an effect no execution of the HOF's
|
|
9796
|
+
// own code performs. Here the read is in the callee's OWN body, and an execution in which it reads the
|
|
9797
|
+
// environment exists — that is the all-paths merge `envValueLeaves` already charges for `(o ?? process.env).X`.
|
|
9798
|
+
// Pass 2c, in this same file, already answers the WRITE side this way (`envFed` is set by ANY site, and the
|
|
9799
|
+
// writer is charged `Env`), so the read side requiring every site was the same question answered twice.
|
|
9800
|
+
// The price is the one every summary pays: a caller handing the callee `{}` inherits `Env` through the
|
|
9801
|
+
// ordinary edge, exactly as a caller of 2c's `populate` and of an `??`-merging reader already does.
|
|
9802
|
+
// Because SOME suffices, whether the function escapes no longer matters — hidden sites can only add bindings.
|
|
9803
|
+
// An in-body REASSIGNMENT (`e = {}`) is the flow-insensitive rebind the local-alias rule exists for: the
|
|
9804
|
+
// parameter is then MUST only if every reassignment is also the environment, and otherwise MAY, which
|
|
9805
|
+
// discloses `Unknown[env-maybe-read]` on the reader rather than charging over a value that was replaced.
|
|
9161
9806
|
const mustTry = () => {
|
|
9162
9807
|
let changed = false;
|
|
9163
9808
|
for (const sym of envMayAliasSymbols) {
|
|
9809
|
+
if (paramSyms.has(sym)) continue;
|
|
9164
9810
|
if (!envAliasSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) { envAliasSymbols.add(sym); changed = true; }
|
|
9165
9811
|
}
|
|
9166
9812
|
for (const sym of envPropMaySymbols) {
|
|
9167
9813
|
if (!envPropMustSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) { envPropMustSymbols.add(sym); changed = true; }
|
|
9168
9814
|
}
|
|
9169
|
-
for (const sym of
|
|
9815
|
+
for (const sym of paramSyms) {
|
|
9170
9816
|
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;
|
|
9817
|
+
const must = (v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols);
|
|
9818
|
+
if (!(paramArgs.get(sym) ?? []).some(must) || !(paramAssigns.get(sym) ?? []).every(must)) continue;
|
|
9175
9819
|
envAliasSymbols.add(sym); changed = true;
|
|
9176
9820
|
}
|
|
9177
9821
|
return changed;
|
|
9178
9822
|
};
|
|
9179
9823
|
for (let n = 0; mustTry() && n < 64; n++) { /* to fixpoint */ }
|
|
9824
|
+
envBindingTable = { bindings, memberSymOf, paramSyms }; // ⟨R935⟩ the floor below asks what each location holds
|
|
9180
9825
|
}
|
|
9181
9826
|
// True when `id` is an identifier resolving to a confirmed process.env alias local.
|
|
9182
9827
|
const identIsEnvAlias = (id) => {
|
|
9183
9828
|
if (!id || !ts.isIdentifier(id)) return false;
|
|
9184
|
-
const sym =
|
|
9829
|
+
const sym = envValueSymAt(id); // ⟨R1075⟩ through an import
|
|
9185
9830
|
return !!sym && envAliasSymbols.has(sym);
|
|
9186
9831
|
};
|
|
9187
9832
|
// MAY-read: a MAY-alias (ever-bound to process.env but reassignable). A superset of the MUST set.
|
|
9188
9833
|
const identIsEnvMayAlias = (id) => {
|
|
9189
9834
|
if (!id || !ts.isIdentifier(id)) return false;
|
|
9190
|
-
const sym =
|
|
9835
|
+
const sym = envValueSymAt(id); // ⟨R1075⟩ through an import
|
|
9191
9836
|
return !!sym && envMayAliasSymbols.has(sym);
|
|
9192
9837
|
};
|
|
9193
9838
|
// The env kind of an expression's VALUE: "env" if some leaf is process.env or a MUST-alias, "may" if
|
|
@@ -9195,6 +9840,8 @@ const identIsEnvMayAlias = (id) => {
|
|
|
9195
9840
|
// ⟨R935⟩ a member reference `o.p` / `o["p"]` whose property is an environment location.
|
|
9196
9841
|
const envMemberSym = (n) => {
|
|
9197
9842
|
if (!(envPropMaySymbols.size > 0)) return null;
|
|
9843
|
+
const lm = envLiteralMember(n); // ⟨R935⟩ the object the receiver holds, before the member its type declares
|
|
9844
|
+
if (lm && envPropMaySymbols.has(lm.key)) return lm.key;
|
|
9198
9845
|
if (ts.isPropertyAccessExpression(n)) return envLocKey(checker.getSymbolAtLocation(n.name));
|
|
9199
9846
|
if (ts.isElementAccessExpression(n) && n.argumentExpression && ts.isStringLiteralLike(n.argumentExpression))
|
|
9200
9847
|
return envLocKey(checker.getSymbolAtLocation(n.argumentExpression));
|
|
@@ -9203,7 +9850,11 @@ const envMemberSym = (n) => {
|
|
|
9203
9850
|
const envPropKind = (n) => {
|
|
9204
9851
|
const s = envMemberSym(n);
|
|
9205
9852
|
if (!s || !envPropMaySymbols.has(s)) return null;
|
|
9206
|
-
|
|
9853
|
+
if (!envPropMustSymbols.has(s)) return "may";
|
|
9854
|
+
// ⟨R935⟩ resolved through the receiver: MAY wherever the declared member is written off the literal.
|
|
9855
|
+
const lm = envLiteralMember(n);
|
|
9856
|
+
if (lm && lm.key === s && lm.declKey && lm.declKey !== s && envDeclWritten.has(lm.declKey)) return "may";
|
|
9857
|
+
return "env";
|
|
9207
9858
|
};
|
|
9208
9859
|
const envValueKind = (expr) => {
|
|
9209
9860
|
let kind = null;
|
|
@@ -9238,7 +9889,19 @@ const envRefKind = (node) => {
|
|
|
9238
9889
|
}
|
|
9239
9890
|
if (!ts.isIdentifier(node)) return null;
|
|
9240
9891
|
// ⟨R934⟩ MUST-parameters are in `envAliasSymbols` but never in the MAY set, so the name filter reads both.
|
|
9241
|
-
envAliasNames ??=
|
|
9892
|
+
envAliasNames ??= (() => {
|
|
9893
|
+
const names = new Set([...envMayAliasSymbols, ...envAliasSymbols].map((s) => s.name));
|
|
9894
|
+
// ⟨R1075⟩ …and the LOCAL name of every import that resolves to one (`import { myEnv as e }`).
|
|
9895
|
+
const visit = (n) => {
|
|
9896
|
+
if ((ts.isImportSpecifier(n) || ts.isImportClause(n)) && n.name) {
|
|
9897
|
+
const t = envValueSymAt(n.name);
|
|
9898
|
+
if (t && (envAliasSymbols.has(t) || envMayAliasSymbols.has(t))) names.add(n.name.text);
|
|
9899
|
+
}
|
|
9900
|
+
if (!ts.isFunctionLike(n) && !ts.isClassLike(n)) ts.forEachChild(n, visit);
|
|
9901
|
+
};
|
|
9902
|
+
if (names.size) for (const sf of sources) visit(sf);
|
|
9903
|
+
return names;
|
|
9904
|
+
})();
|
|
9242
9905
|
if (!envAliasNames.has(node.text)) return null;
|
|
9243
9906
|
const p = node.parent;
|
|
9244
9907
|
if (!p) return null;
|
|
@@ -9251,17 +9914,351 @@ const envRefKind = (node) => {
|
|
|
9251
9914
|
if (ts.isBinaryExpression(p) && p.left === node && envIsAssignOp(p.operatorToken.kind)) return null;
|
|
9252
9915
|
if ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p))
|
|
9253
9916
|
&& (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)) return null;
|
|
9254
|
-
|
|
9917
|
+
let sym = ts.isShorthandPropertyAssignment(p) && p.name === node
|
|
9255
9918
|
? checker.getShorthandAssignmentValueSymbol(p) : checker.getSymbolAtLocation(node);
|
|
9919
|
+
if (sym && (sym.flags & ts.SymbolFlags.Alias)) { try { sym = checker.getAliasedSymbol(sym); } catch { /* unresolved */ } } // ⟨R1075⟩
|
|
9256
9920
|
if (!sym) return null;
|
|
9257
9921
|
if (envAliasSymbols.has(sym)) return "env";
|
|
9258
9922
|
return envMayAliasSymbols.has(sym) ? "may" : null;
|
|
9259
9923
|
};
|
|
9924
|
+
// ⟨SOUNDNESS R935, the CONTAINER / ITERATION / OPEN-PROPERTY half⟩ A FLOOR UNDER THE LOCATION MODEL, KEYED ON THE
|
|
9925
|
+
// TYPE. Three lanes closed R935 one storage kind at a time — a value-bearing declaration, then a const literal
|
|
9926
|
+
// receiver — and each left the next kind silent: `const m = new Map([["e", process.env]]); m.get("e")!.SECRET`,
|
|
9927
|
+
// a `Set`/`WeakMap`, `for (const x of arr) x.SECRET`, `arr.map((x) => x.SECRET)`, all EXECUTED (`fx35`), each
|
|
9928
|
+
// printed the planted value, each reader ABSENT with `deny Env` and `deny Env Unknown` exit 0. Every one is a value
|
|
9929
|
+
// whose PROVENANCE the binding table cannot enumerate, and the set of such storage kinds is open; a ninth special
|
|
9930
|
+
// case would leave a tenth. What every one of them still carries is node's `NodeJS.ProcessEnv` TYPE, which the
|
|
9931
|
+
// checker propagated from `process.env` through the container's inferred type argument.
|
|
9932
|
+
// So: an expression whose type is `ProcessEnv`, which the location model above did NOT resolve, and whose value
|
|
9933
|
+
// comes from somewhere this pass cannot see into, is POSSIBLY the environment — `Unknown[env-maybe-read]`, the MAY
|
|
9934
|
+
// posture every other location uses. Never `Env`: a `ProcessEnv`-typed object can be a copy or a literal.
|
|
9935
|
+
// "Somewhere this pass cannot see into" is the part that keeps it from re-asking settled questions:
|
|
9936
|
+
// · a PARAMETER of a named function or method is R934's — a caller that hands it the environment is charged at
|
|
9937
|
+
// its own call, and a parameter no visible site feeds is not charged there either. A CALLBACK's parameter
|
|
9938
|
+
// (an arrow or function expression passed as an argument) is not: its caller is the HOF — `Array.map`,
|
|
9939
|
+
// `Set.forEach` — and nothing in the scan ever charges that call.
|
|
9940
|
+
// · a call to a PROJECT function with a body is judged inside the callee, whose `return` is a consumer there.
|
|
9941
|
+
// · a `const`/`let` is opaque only if its initializer is (or it has none); otherwise its initializer is either
|
|
9942
|
+
// resolved or itself floored where it is evaluated.
|
|
9943
|
+
// RESIDUAL, stated: the type is the carrier, so an environment object stored under `any`, `unknown`, `object` or
|
|
9944
|
+
// `Record<…>` (`new Map<string, any>([["e", process.env]])`) is outside the floor.
|
|
9945
|
+
const isProcessEnvType = (t0) => {
|
|
9946
|
+
let t = t0;
|
|
9947
|
+
try { t = checker.getNonNullableType(t0); } catch { /* keep */ }
|
|
9948
|
+
for (const u of t?.isUnion?.() ? t.types : [t]) {
|
|
9949
|
+
const sy = u?.aliasSymbol ?? u?.symbol;
|
|
9950
|
+
if (sy && sy.getName() === "ProcessEnv"
|
|
9951
|
+
&& (sy.declarations ?? []).some((d) => !projectFiles.has(path.resolve(d.getSourceFile().fileName)))) return true;
|
|
9952
|
+
}
|
|
9953
|
+
return false;
|
|
9954
|
+
};
|
|
9955
|
+
// Is this ProcessEnv-typed value's provenance OPAQUE to the binding table? A location the table enumerates is opaque
|
|
9956
|
+
// only if one of its bindings is (a `null` binding is a value the table could not name — a reflective or compound
|
|
9957
|
+
// write — except on a plain variable, where it is the `undefined` of a declaration with no initializer).
|
|
9958
|
+
const envFloorOpaque = (e, depth, seen = new Set()) => {
|
|
9959
|
+
const x = unwrapEnvTransparent(e);
|
|
9960
|
+
if (!x || depth > 6) return false;
|
|
9961
|
+
if (ts.isAwaitExpression(x)) return envFloorOpaque(x.expression, depth + 1, seen);
|
|
9962
|
+
if (isProcessEnvExpr(x)) return false;
|
|
9963
|
+
if (ts.isObjectLiteralExpression(x) || ts.isArrayLiteralExpression(x)) return false;
|
|
9964
|
+
let t = null;
|
|
9965
|
+
try { t = checker.getTypeAtLocation(x); } catch { return false; }
|
|
9966
|
+
if (!isProcessEnvType(t)) return false;
|
|
9967
|
+
const tbl = envBindingTable;
|
|
9968
|
+
const viaBindings = (key, nullOpaque) => {
|
|
9969
|
+
if (seen.has(key)) return false;
|
|
9970
|
+
seen.add(key);
|
|
9971
|
+
return (tbl.bindings.get(key) ?? []).some((v) => {
|
|
9972
|
+
if (v === null || v === undefined) return nullOpaque;
|
|
9973
|
+
if (typeof v === "symbol") return false; // ENV_DESTRUCTURED: resolved
|
|
9974
|
+
if (typeof v === "object" && "same" in v) return tbl.bindings.has(v.same) ? viaBindings(v.same, nullOpaque) : false;
|
|
9975
|
+
return envValueLeaves(v).some((l) => envFloorOpaque(l, depth + 1, seen));
|
|
9976
|
+
});
|
|
9977
|
+
};
|
|
9978
|
+
if (ts.isCallExpression(x) || ts.isNewExpression(x)) {
|
|
9979
|
+
let d = null;
|
|
9980
|
+
try { d = checker.getResolvedSignature(x)?.declaration ?? null; } catch { d = null; }
|
|
9981
|
+
return !(d && projectFiles.has(path.resolve(d.getSourceFile().fileName)) && d.body);
|
|
9982
|
+
}
|
|
9983
|
+
if (ts.isPropertyAccessExpression(x) || ts.isElementAccessExpression(x)) {
|
|
9984
|
+
if (envPropKind(x)) return false;
|
|
9985
|
+
// a project GETTER with a body is judged inside it, as a project callee is (its `return` is a consumer there)
|
|
9986
|
+
let ps = null;
|
|
9987
|
+
try { ps = ts.isPropertyAccessExpression(x) ? checker.getSymbolAtLocation(x.name) : null; } catch { ps = null; }
|
|
9988
|
+
if ((ps?.declarations ?? []).some((dd) => ts.isGetAccessorDeclaration(dd) && dd.body && isProjectNode(dd))) return false;
|
|
9989
|
+
const key = tbl ? tbl.memberSymOf(x) : null;
|
|
9990
|
+
if (key && tbl.bindings.has(key)) return viaBindings(key, true);
|
|
9991
|
+
return true; // an interface member, a dynamic index, a dependency's field: the table cannot enumerate it
|
|
9992
|
+
}
|
|
9993
|
+
if (!ts.isIdentifier(x)) return false;
|
|
9994
|
+
const sym = envValueSymAt(x);
|
|
9995
|
+
if (!sym || envAliasSymbols.has(sym) || envMayAliasSymbols.has(sym)) return false;
|
|
9996
|
+
let d = sym.valueDeclaration ?? sym.declarations?.[0];
|
|
9997
|
+
if (!d) return false;
|
|
9998
|
+
while (d && ts.isBindingElement(d)) d = d.parent?.parent; // a destructured name answers as its pattern's root
|
|
9999
|
+
if (!d) return false;
|
|
10000
|
+
if (ts.isParameter(d)) {
|
|
10001
|
+
const f = d.parent;
|
|
10002
|
+
return !!f && (ts.isArrowFunction(f) || ts.isFunctionExpression(f)) && !!f.parent
|
|
10003
|
+
&& (ts.isCallExpression(f.parent) || ts.isNewExpression(f.parent)) && (f.parent.arguments ?? []).includes(f);
|
|
10004
|
+
}
|
|
10005
|
+
if (ts.isVariableDeclaration(d)) {
|
|
10006
|
+
if (d.parent?.parent && (ts.isForOfStatement(d.parent.parent) || ts.isForInStatement(d.parent.parent))) return true;
|
|
10007
|
+
if (!ts.isIdentifier(d.name)) return d.initializer ? envValueLeaves(d.initializer).some((l) => envFloorOpaque(l, depth + 1, seen)) : true;
|
|
10008
|
+
if (tbl && tbl.bindings.has(sym)) return viaBindings(sym, false);
|
|
10009
|
+
return d.initializer ? envValueLeaves(d.initializer).some((l) => envFloorOpaque(l, depth + 1, seen)) : true;
|
|
10010
|
+
}
|
|
10011
|
+
return false;
|
|
10012
|
+
};
|
|
10013
|
+
const envTypeFloorKind = (node) => {
|
|
10014
|
+
if (!(ts.isIdentifier(node) || ts.isCallExpression(node) || ts.isPropertyAccessExpression(node)
|
|
10015
|
+
|| ts.isElementAccessExpression(node))) return null;
|
|
10016
|
+
const p = node.parent;
|
|
10017
|
+
if (!p) return null;
|
|
10018
|
+
// not a value REFERENCE: a declared name, the member-name side of an access, a write target, a type position
|
|
10019
|
+
if (ts.isIdentifier(node)) {
|
|
10020
|
+
if ((ts.isVariableDeclaration(p) || ts.isParameter(p) || ts.isBindingElement(p) || ts.isPropertyAssignment(p)
|
|
10021
|
+
|| ts.isPropertyDeclaration(p) || ts.isFunctionDeclaration(p) || ts.isMethodDeclaration(p)
|
|
10022
|
+
|| ts.isPropertySignature(p) || ts.isShorthandPropertyAssignment(p)) && p.name === node) return null;
|
|
10023
|
+
if (ts.isBindingElement(p) && p.propertyName === node) return null;
|
|
10024
|
+
if ((ts.isPropertyAccessExpression(p) || ts.isQualifiedName(p)) && p.name === node) return null;
|
|
10025
|
+
if (ts.isTypeQueryNode(p) || ts.isExportSpecifier(p) || ts.isImportSpecifier(p) || ts.isImportClause(p)
|
|
10026
|
+
|| ts.isNamespaceImport(p) || ts.isImportEqualsDeclaration(p) || ts.isExportAssignment(p)) return null;
|
|
10027
|
+
}
|
|
10028
|
+
if (ts.isBinaryExpression(p) && p.left === node && envIsAssignOp(p.operatorToken.kind)) return null;
|
|
10029
|
+
if (ts.isDeleteExpression(p) || ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p))
|
|
10030
|
+
&& (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken))) return null;
|
|
10031
|
+
if (isDestructuringAssignTarget(node)) return null;
|
|
10032
|
+
// a TYPE position spelled as an expression: `interface E extends NodeJS.ProcessEnv`, `implements …`
|
|
10033
|
+
{
|
|
10034
|
+
let q = node;
|
|
10035
|
+
while (q.parent && ts.isPropertyAccessExpression(q.parent) && q.parent.expression === q) q = q.parent;
|
|
10036
|
+
const ew = q.parent;
|
|
10037
|
+
if (ew && ts.isExpressionWithTypeArguments(ew) && ew.parent && ts.isHeritageClause(ew.parent)
|
|
10038
|
+
&& (ew.parent.token === ts.SyntaxKind.ImplementsKeyword || ts.isInterfaceDeclaration(ew.parent.parent))) return null;
|
|
10039
|
+
}
|
|
10040
|
+
// a binding whose initializer is the floored value is judged at the initializer, not again at every use
|
|
10041
|
+
if (!envFloorOpaque(node, 0)) return null;
|
|
10042
|
+
if (ENVLOC_REACH) { const sf = node.getSourceFile(); ENVLOC_REACH(`floor ${ts.SyntaxKind[node.kind]} ${path.relative(rootDir, sf.fileName)}:${sf.getLineAndCharacterOfPosition(node.getStart()).line + 1}`); }
|
|
10043
|
+
return "may";
|
|
10044
|
+
};
|
|
9260
10045
|
// ⟨R928/R804⟩ Does the CONSUMER of this env value provably touch no key? Climb through what passes the
|
|
9261
10046
|
// value on unchanged (the transparent wrappers, both arms of `?:`, either side of `??`/`||`, the right of
|
|
9262
10047
|
// `&&`/`,`) to the node that consumes it, then answer from a CLOSED list of non-reading consumers.
|
|
9263
10048
|
// Everything not on it charges: that is the direction a list must fail in here (a missing entry is an
|
|
9264
10049
|
// over-charge on code that already names the environment, never a silence).
|
|
10050
|
+
// ⟨SOUNDNESS R935, the UNTYPED-CONTAINER half⟩ THE ENVIRONMENT STORED WHERE ITS TYPE IS LOST. The ProcessEnv floor
|
|
10051
|
+
// above rides on the type, so `new Map<string, any>([["e", process.env]])`, `rec["e"] = process.env` on a
|
|
10052
|
+
// `Record<string, any>`, `arr.push(process.env)` into an `any[]`, and `objAny.cfg = { env: process.env }` on an `any`
|
|
10053
|
+
// object left every reader of the container ABSENT (EXECUTED, `fxany`: each printed the planted value, `deny Env` and
|
|
10054
|
+
// `deny Env Unknown` exit 0). Narrowed to where the escape is SEEN rather than to every `any` read:
|
|
10055
|
+
// · the CONTAINERS are only those the environment value is actually put into — a builtin collection built with it
|
|
10056
|
+
// (`new Map|Set|WeakMap|Array(…)`) or mutated with it (`set`/`add`/`push`/`unshift`/`splice`), an element or
|
|
10057
|
+
// dynamic-key write (`c[k] = env`, a `Record` index), or a member write onto an `any`/`unknown` object — named by
|
|
10058
|
+
// the root binding of the receiver. An object literal under a static key is a LOCATION the table above already
|
|
10059
|
+
// enumerates, and a call argument (`spawn(cmd, { env })`) is charged at the call and stored nowhere visible;
|
|
10060
|
+
// · a READ discloses only when its receiver is reached FROM such a container through at least one element step
|
|
10061
|
+
// (a member/element read, a method call's result, a `for…of` variable, a callback parameter of a method called on
|
|
10062
|
+
// it, a parameter whose visible call site passes one) — `m.size` on the container itself reads no key — and its
|
|
10063
|
+
// value is not a primitive. MAY (`Unknown[env-maybe-read]`): which element is the environment is not known.
|
|
10064
|
+
const ENV_COLLECTION_CTORS = new Set(["Map", "Set", "WeakMap", "WeakSet", "Array", "WeakRef"]);
|
|
10065
|
+
const ENV_COLLECTION_MUTATORS = new Set(["set", "add", "push", "unshift", "splice", "fill"]);
|
|
10066
|
+
let envTaintedRoots = null;
|
|
10067
|
+
const envRootSym = (e0) => {
|
|
10068
|
+
let e = unwrapEnvTransparent(e0);
|
|
10069
|
+
for (let g = 0; e && g < 16 && (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e) || ts.isCallExpression(e)); g++)
|
|
10070
|
+
e = unwrapEnvTransparent(ts.isCallExpression(e) ? (ts.isPropertyAccessExpression(e.expression) ? e.expression.expression : null) : e.expression);
|
|
10071
|
+
if (!e || !ts.isIdentifier(e)) return null;
|
|
10072
|
+
const sym = envValueSymAt(e);
|
|
10073
|
+
return sym && (sym.flags & (ts.SymbolFlags.Variable | ts.SymbolFlags.Property)) ? sym : null;
|
|
10074
|
+
};
|
|
10075
|
+
const envIsCollectionType = (e) => {
|
|
10076
|
+
let t; try { t = checker.getNonNullableType(checker.getTypeAtLocation(e)); } catch { return false; }
|
|
10077
|
+
const nm = (t?.getSymbol?.() ?? t?.aliasSymbol)?.getName?.();
|
|
10078
|
+
return ["Map", "Set", "WeakMap", "WeakSet", "Array", "ReadonlyArray"].includes(nm) || !!checker.isArrayType?.(t);
|
|
10079
|
+
};
|
|
10080
|
+
const envAnyish = (e) => {
|
|
10081
|
+
let t; try { t = checker.getTypeAtLocation(e); } catch { return false; }
|
|
10082
|
+
if (!t) return false;
|
|
10083
|
+
if (t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return true;
|
|
10084
|
+
return !!(t.getStringIndexType?.() || t.getNumberIndexType?.());
|
|
10085
|
+
};
|
|
10086
|
+
const computeEnvTaint = () => {
|
|
10087
|
+
const roots = new Set();
|
|
10088
|
+
const seed = (n) => {
|
|
10089
|
+
let cur = n;
|
|
10090
|
+
for (let g = 0; g < 24; g++) {
|
|
10091
|
+
const p = cur.parent;
|
|
10092
|
+
if (!p) return;
|
|
10093
|
+
if ((ts.isParenthesizedExpression(p) || ts.isAsExpression(p) || ts.isTypeAssertionExpression(p) || ts.isNonNullExpression(p)
|
|
10094
|
+
|| ts.isSatisfiesExpression?.(p)) && p.expression === cur) { cur = p; continue; }
|
|
10095
|
+
if (ts.isConditionalExpression(p) && p.condition !== cur) { cur = p; continue; }
|
|
10096
|
+
if (ts.isBinaryExpression(p) && (p.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken || p.operatorToken.kind === ts.SyntaxKind.BarBarToken)) { cur = p; continue; }
|
|
10097
|
+
if (ts.isArrayLiteralExpression(p) || ts.isSpreadElement(p)) { cur = p; continue; }
|
|
10098
|
+
if ((ts.isPropertyAssignment(p) && p.initializer === cur) || ts.isShorthandPropertyAssignment(p)) {
|
|
10099
|
+
// a STATIC member of an object literal is a location — unless the literal itself lands in a container (climb on)
|
|
10100
|
+
cur = p.parent; continue;
|
|
10101
|
+
}
|
|
10102
|
+
if (ts.isNewExpression(p) && (p.arguments ?? []).includes(cur)) {
|
|
10103
|
+
const c = unwrapEnvTransparent(p.expression);
|
|
10104
|
+
if (ts.isIdentifier(c) && ENV_COLLECTION_CTORS.has(c.text) && identIsGlobal(c)) { cur = p; continue; }
|
|
10105
|
+
return;
|
|
10106
|
+
}
|
|
10107
|
+
if (ts.isCallExpression(p) && (p.arguments ?? []).includes(cur)) {
|
|
10108
|
+
const ce = unwrapEnvTransparent(p.expression);
|
|
10109
|
+
if (ts.isPropertyAccessExpression(ce) && ENV_COLLECTION_MUTATORS.has(ce.name.text) && envIsCollectionType(ce.expression)) {
|
|
10110
|
+
const r = envRootSym(ce.expression); if (r) roots.add(r);
|
|
10111
|
+
}
|
|
10112
|
+
return;
|
|
10113
|
+
}
|
|
10114
|
+
if (ts.isBinaryExpression(p) && p.right === cur && p.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
10115
|
+
const l = unwrapEnvTransparent(p.left);
|
|
10116
|
+
const dyn = ts.isElementAccessExpression(l) ? (!l.argumentExpression || !ts.isNumericLiteral(l.argumentExpression) || envAnyish(l.expression))
|
|
10117
|
+
: ts.isPropertyAccessExpression(l) && envAnyish(l.expression);
|
|
10118
|
+
if (dyn) { const r = envRootSym(l.expression); if (r) roots.add(r); }
|
|
10119
|
+
return;
|
|
10120
|
+
}
|
|
10121
|
+
if (ts.isVariableDeclaration(p) && p.initializer === cur) {
|
|
10122
|
+
// only a value that went through a CONTAINER on the way (an array/collection) — a bare alias or a static
|
|
10123
|
+
// object literal is the binding table's question
|
|
10124
|
+
if (cur !== n && ts.isIdentifier(p.name) && (ts.isNewExpression(cur) || ts.isArrayLiteralExpression(cur)
|
|
10125
|
+
|| (ts.isObjectLiteralExpression(cur) && envAnyish(p.name)))) {
|
|
10126
|
+
const sym = checker.getSymbolAtLocation(p.name); if (sym) roots.add(sym);
|
|
10127
|
+
}
|
|
10128
|
+
return;
|
|
10129
|
+
}
|
|
10130
|
+
return;
|
|
10131
|
+
}
|
|
10132
|
+
};
|
|
10133
|
+
const visit = (n) => {
|
|
10134
|
+
if (envRefKind(n)) seed(n);
|
|
10135
|
+
ts.forEachChild(n, visit);
|
|
10136
|
+
};
|
|
10137
|
+
for (const sf of sources) visit(sf);
|
|
10138
|
+
return roots;
|
|
10139
|
+
};
|
|
10140
|
+
// How many element steps separate `e` from a tainted container (-1: not reached from one).
|
|
10141
|
+
const envFromTainted = (e0, steps, depth, seen) => {
|
|
10142
|
+
const e = unwrapEnvTransparent(e0);
|
|
10143
|
+
if (!e || depth > 8) return -1;
|
|
10144
|
+
if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) return envFromTainted(e.expression, steps + 1, depth + 1, seen);
|
|
10145
|
+
if (ts.isCallExpression(e)) {
|
|
10146
|
+
const ce = unwrapEnvTransparent(e.expression);
|
|
10147
|
+
return ts.isPropertyAccessExpression(ce) ? envFromTainted(ce.expression, steps + 1, depth + 1, seen) : -1;
|
|
10148
|
+
}
|
|
10149
|
+
if (!ts.isIdentifier(e)) return -1;
|
|
10150
|
+
const sym = envValueSymAt(e);
|
|
10151
|
+
if (!sym || seen.has(sym)) return -1;
|
|
10152
|
+
seen.add(sym);
|
|
10153
|
+
if (envTaintedRoots.has(sym)) { envTaintHitRoot = sym; return steps; }
|
|
10154
|
+
const d = sym.valueDeclaration;
|
|
10155
|
+
if (!d) return -1;
|
|
10156
|
+
if (ts.isVariableDeclaration(d)) {
|
|
10157
|
+
const loop = d.parent?.parent;
|
|
10158
|
+
if (loop && ts.isForOfStatement(loop)) return ts.isIdentifier(d.name) ? envFromTainted(loop.expression, steps + 1, depth + 1, seen) : -1;
|
|
10159
|
+
return d.initializer && ts.isIdentifier(d.name) ? envFromTainted(d.initializer, steps, depth + 1, seen) : -1;
|
|
10160
|
+
}
|
|
10161
|
+
if (ts.isParameter(d) && ts.isIdentifier(d.name)) {
|
|
10162
|
+
const f = d.parent;
|
|
10163
|
+
if ((ts.isArrowFunction(f) || ts.isFunctionExpression(f)) && f.parent && ts.isCallExpression(f.parent) && f.parent.arguments.includes(f)) {
|
|
10164
|
+
const ce = unwrapEnvTransparent(f.parent.expression);
|
|
10165
|
+
return ts.isPropertyAccessExpression(ce) && f.parameters[0] === d ? envFromTainted(ce.expression, steps + 1, depth + 1, seen) : -1;
|
|
10166
|
+
}
|
|
10167
|
+
let best = -1;
|
|
10168
|
+
for (const a of paramArgs.get(sym) ?? []) {
|
|
10169
|
+
const r = a ? envFromTainted(a, steps, depth + 1, new Set(seen)) : -1;
|
|
10170
|
+
if (r >= 0 && (best < 0 || r > best)) best = r; // the farthest site: MUST only if every reached site is one step
|
|
10171
|
+
}
|
|
10172
|
+
return best;
|
|
10173
|
+
}
|
|
10174
|
+
return -1;
|
|
10175
|
+
};
|
|
10176
|
+
let envTaintHitRoot = null;
|
|
10177
|
+
// ⟨R935⟩ THE RESOLUTION: a container whose every value is the environment and that never escapes. A `const`, not
|
|
10178
|
+
// exported, whose initializer is empty or holds only environment values (`new Map([[k, env]…])`, `[env]`, `{}`), every
|
|
10179
|
+
// mutation stores the environment (`set(k, env)`, `add(env)`, `push(env)`, `c[k] = env`), and every other reference
|
|
10180
|
+
// is a read-only method or element read — then an ELEMENT (exactly one step in) IS the environment, and its member
|
|
10181
|
+
// read is `Env`, not `Unknown`. Anything else stays MAY.
|
|
10182
|
+
const ENV_CONTAINER_READERS = new Set(["get", "has", "at", "find", "findLast", "values", "forEach", "map", "filter", "some",
|
|
10183
|
+
"every", "reduce", "includes", "indexOf", "keys", "entries", "size", "length", "slice", "flatMap"]);
|
|
10184
|
+
const envAllEnvMemo = new Map();
|
|
10185
|
+
const envContainerAllEnv = (sym) => {
|
|
10186
|
+
if (envAllEnvMemo.has(sym)) return envAllEnvMemo.get(sym);
|
|
10187
|
+
envAllEnvMemo.set(sym, false);
|
|
10188
|
+
const d = sym.valueDeclaration;
|
|
10189
|
+
const isEnv = (x) => !!x && envRefKind(unwrapEnvTransparent(x)) === "env";
|
|
10190
|
+
if (!d || !ts.isVariableDeclaration(d) || !ts.isIdentifier(d.name) || !(ts.getCombinedNodeFlags(d) & ts.NodeFlags.Const)
|
|
10191
|
+
|| !projectFiles.has(path.resolve(d.getSourceFile().fileName))) return false;
|
|
10192
|
+
const st = d.parent?.parent;
|
|
10193
|
+
if (st && ts.isVariableStatement(st) && st.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) return false;
|
|
10194
|
+
const init = unwrapEnvTransparent(d.initializer);
|
|
10195
|
+
const litOk = (lit, tuple) => ts.isArrayLiteralExpression(lit) && lit.elements.every((el) => tuple
|
|
10196
|
+
? (ts.isArrayLiteralExpression(el) && el.elements.length === 2 && isEnv(el.elements[1])) : isEnv(el));
|
|
10197
|
+
let ok = false;
|
|
10198
|
+
if (init && ts.isArrayLiteralExpression(init)) ok = litOk(init, false);
|
|
10199
|
+
else if (init && ts.isObjectLiteralExpression(init)) ok = init.properties.length === 0;
|
|
10200
|
+
else if (init && ts.isNewExpression(init) && ts.isIdentifier(init.expression)) {
|
|
10201
|
+
const a0 = init.arguments?.[0] ? unwrapEnvTransparent(init.arguments[0]) : null;
|
|
10202
|
+
const nm = init.expression.text;
|
|
10203
|
+
ok = !a0 || ((nm === "Map" || nm === "WeakMap") ? litOk(a0, true) : (nm === "Set" || nm === "WeakSet") ? litOk(a0, false) : false);
|
|
10204
|
+
}
|
|
10205
|
+
if (!ok) return false;
|
|
10206
|
+
let clean = true;
|
|
10207
|
+
const visit = (n) => {
|
|
10208
|
+
if (!clean) return;
|
|
10209
|
+
if (ts.isIdentifier(n) && n.text === d.name.text && n !== d.name && envValueSymAt(n) === sym) {
|
|
10210
|
+
let cur = n;
|
|
10211
|
+
while (cur.parent && (ts.isParenthesizedExpression(cur.parent) || ts.isNonNullExpression(cur.parent) || ts.isAsExpression(cur.parent))) cur = cur.parent;
|
|
10212
|
+
const p = cur.parent;
|
|
10213
|
+
if (p && ts.isPropertyAccessExpression(p) && p.expression === cur) {
|
|
10214
|
+
const call = p.parent && ts.isCallExpression(p.parent) && p.parent.expression === p ? p.parent : null;
|
|
10215
|
+
const m = p.name.text;
|
|
10216
|
+
if (call && (m === "set")) clean = isEnv(call.arguments[1]);
|
|
10217
|
+
else if (call && (m === "add" || m === "push" || m === "unshift")) clean = call.arguments.every((x) => isEnv(x));
|
|
10218
|
+
else if (!ENV_CONTAINER_READERS.has(m)) clean = false;
|
|
10219
|
+
} else if (p && ts.isElementAccessExpression(p) && p.expression === cur) {
|
|
10220
|
+
const asg = p.parent && ts.isBinaryExpression(p.parent) && p.parent.left === p ? p.parent : null;
|
|
10221
|
+
if (asg) clean = asg.operatorToken.kind === ts.SyntaxKind.EqualsToken && isEnv(asg.right);
|
|
10222
|
+
} else if (p && ts.isForOfStatement(p) && p.expression === cur) { /* iteration reads */ }
|
|
10223
|
+
else if (p && ts.isTypeQueryNode(p)) { /* type position */ }
|
|
10224
|
+
else clean = false; // passed, returned, aliased, spread: escapes
|
|
10225
|
+
}
|
|
10226
|
+
ts.forEachChild(n, visit);
|
|
10227
|
+
};
|
|
10228
|
+
visit(d.getSourceFile());
|
|
10229
|
+
envAllEnvMemo.set(sym, clean);
|
|
10230
|
+
return clean;
|
|
10231
|
+
};
|
|
10232
|
+
const envTaintKind = (node) => {
|
|
10233
|
+
if (!(ts.isIdentifier(node) || ts.isCallExpression(node) || ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)
|
|
10234
|
+
|| ts.isParenthesizedExpression(node) || ts.isNonNullExpression(node))) return null;
|
|
10235
|
+
const p = node.parent;
|
|
10236
|
+
// only the RECEIVER of a member read
|
|
10237
|
+
if (!p || !((ts.isPropertyAccessExpression(p) || ts.isElementAccessExpression(p)) && p.expression === node)) return null;
|
|
10238
|
+
if (ts.isBinaryExpression(p.parent) && p.parent.left === p && envIsAssignOp(p.parent.operatorToken.kind)) return null;
|
|
10239
|
+
if (ts.isCallExpression(p.parent) && p.parent.expression === p) return null; // a method call on it, not a key read
|
|
10240
|
+
envTaintedRoots ??= computeEnvTaint();
|
|
10241
|
+
if (!envTaintedRoots.size) return null;
|
|
10242
|
+
let t; try { t = checker.getTypeAtLocation(node); } catch { return null; }
|
|
10243
|
+
if (t && (t.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike | ts.TypeFlags.BigIntLike
|
|
10244
|
+
| ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Void))) return null;
|
|
10245
|
+
if (envIsCollectionType(node)) return null; // a nested container is reached, not read
|
|
10246
|
+
// a member the LOCATION model enumerates (a const literal's element at a literal index, a declared property) has
|
|
10247
|
+
// its own answer — env, may, or provably not — and the taint does not overrule it
|
|
10248
|
+
{
|
|
10249
|
+
const u = unwrapEnvTransparent(node);
|
|
10250
|
+
if ((ts.isPropertyAccessExpression(u) || ts.isElementAccessExpression(u)) && envBindingTable) {
|
|
10251
|
+
const key = envBindingTable.memberSymOf(u);
|
|
10252
|
+
if (key && envBindingTable.bindings.has(key)) return null;
|
|
10253
|
+
}
|
|
10254
|
+
}
|
|
10255
|
+
envTaintHitRoot = null;
|
|
10256
|
+
const steps = envFromTainted(node, 0, 0, new Set());
|
|
10257
|
+
if (steps < 1) return null;
|
|
10258
|
+
const must = steps === 1 && envTaintHitRoot && envContainerAllEnv(envTaintHitRoot);
|
|
10259
|
+
if (ENVLOC_REACH) { const sf = node.getSourceFile(); ENVLOC_REACH(`taint ${must ? "env" : "may"} ${path.relative(rootDir, sf.fileName)}:${sf.getLineAndCharacterOfPosition(node.getStart()).line + 1}`); }
|
|
10260
|
+
return must ? "env" : "may";
|
|
10261
|
+
};
|
|
9265
10262
|
const envConsumerInert = (node) => {
|
|
9266
10263
|
let cur = node;
|
|
9267
10264
|
for (;;) {
|
|
@@ -9358,7 +10355,7 @@ const ENV_TOUCHING_BUILTIN = new Set([
|
|
|
9358
10355
|
// BARE-identifier global builtins that enumerate every key of an object argument (deep-clone reads them all).
|
|
9359
10356
|
const ENV_TOUCHING_GLOBAL = new Set(["structuredClone"]);
|
|
9360
10357
|
const identIsGlobal = (id) => // an identifier that is the ambient global (no project-local declaration shadows it)
|
|
9361
|
-
!(checker.getSymbolAtLocation(id)?.declarations
|
|
10358
|
+
!projectShadows(checker.getSymbolAtLocation(id)?.declarations); // ⟨R1074⟩
|
|
9362
10359
|
// THE `globalThis.` QUALIFIER, WHICH DEFEATED FIVE TEXT-KEYED ARMS AT ONCE (SOUNDNESS row id pending —
|
|
9363
10360
|
// filed by the coordinator, not invented here). Every arm below
|
|
9364
10361
|
// recognises a whole-object builtin by the callee's TEXT (`Object.assign`, `Reflect.set`, `Object.keys`),
|
|
@@ -9641,6 +10638,12 @@ function visitCalls(node) {
|
|
|
9641
10638
|
}
|
|
9642
10639
|
const sig = checker.getResolvedSignature(node);
|
|
9643
10640
|
let decl = sig && sig.declaration;
|
|
10641
|
+
// ⟨R1016⟩ a construction of a class expression runs its constructor unit, whatever the signature names.
|
|
10642
|
+
const classExprCtor = rec ? classExprCtorOfNew(node) : null;
|
|
10643
|
+
if (classExprCtor && classExprCtor !== owner) {
|
|
10644
|
+
rec.edges.add(classExprCtor);
|
|
10645
|
+
if (process.env.CANDOR_R1016_REACH) console.error(`R1016-REACH new ${owner} -> ${classExprCtor}`);
|
|
10646
|
+
}
|
|
9644
10647
|
// ⟨R103⟩ A WRITABLE SLOT IS AN INCOMPLETE CANDIDATE SET — see `openCallSlot`, and see the class-
|
|
9645
10648
|
// override fan-out below, which is the authority this converges on rather than a second rule: it
|
|
9646
10649
|
// edges to every candidate it CAN name and adds `Unknown` when the set it enumerated is not
|
|
@@ -9660,8 +10663,8 @@ function visitCalls(node) {
|
|
|
9660
10663
|
if (!decl) {
|
|
9661
10664
|
// `new C()` on a class with an IMPLICIT constructor resolves to no declaration — edge to
|
|
9662
10665
|
// 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)) {
|
|
10666
|
+
let edged = !!classExprCtor, externalClass = false; // ⟨R1016⟩
|
|
10667
|
+
if (!edged && ts.isNewExpression(node) && node.expression && ts.isIdentifier(node.expression)) {
|
|
9665
10668
|
const cd = realDecl(checker.getSymbolAtLocation(node.expression));
|
|
9666
10669
|
const t = cd && nodeName.get(cd);
|
|
9667
10670
|
if (t) { rec.edges.add(t); edged = true; }
|
|
@@ -10131,6 +11134,12 @@ function visitCalls(node) {
|
|
|
10131
11134
|
const confAt = isSuperReceiver(confRecv) ? null
|
|
10132
11135
|
: (localReceiverClass(confRecv) ?? (ts.isClassDeclaration(decl.parent) ? decl.parent : null));
|
|
10133
11136
|
const confBodies = confAt ? conformerBodies(confAt, decl.name?.getText?.()) : [];
|
|
11137
|
+
// ⟨SOUNDNESS R1061⟩ the receiver's class is a DI token whose substitute the scan cannot name: the body that
|
|
11138
|
+
// runs may be none of the candidates above — disclose, as a dispatch over an open family does.
|
|
11139
|
+
if (confAt && openSubstitutedClasses.has(confAt)) {
|
|
11140
|
+
rec.direct.add("Unknown");
|
|
11141
|
+
rec.why.add(dispatchWhy(`${moduleOf(confAt.getSourceFile())}.${namespacePrefixOf(confAt)}${confAt.name?.getText?.() ?? "<anonymous>"}`, decl.name?.getText?.()));
|
|
11142
|
+
}
|
|
10134
11143
|
if (allOverrides.length > 0 || confBodies.length > 0) {
|
|
10135
11144
|
// PRECISION: scope the fan-out to the RECEIVER's static-type subtree. A base-member
|
|
10136
11145
|
// dispatch on a receiver statically typed as subclass `Cat` can only ever bind to a
|
|
@@ -11088,7 +12097,7 @@ function visitCalls(node) {
|
|
|
11088
12097
|
// ⟨R934⟩ a DESTRUCTURED parameter that every visible call site hands the environment reads its keys
|
|
11089
12098
|
// as the frame is entered — the destructuring IS the read, in this function's frame.
|
|
11090
12099
|
if (ts.isParameter(node) && ts.isObjectBindingPattern(node.name) && envAliasSymbols.has(node)) { markEnv(); ENVLOC_REACH?.("param-pattern env"); }
|
|
11091
|
-
const kind = envRefKind(node);
|
|
12100
|
+
const kind = envRefKind(node) ?? envTypeFloorKind(node) ?? envTaintKind(node); // ⟨R935⟩ floor, then untyped containers
|
|
11092
12101
|
if (kind && ENVLOC_REACH && !envConsumerInert(node)) {
|
|
11093
12102
|
const vd = ts.isIdentifier(node) ? checker.getSymbolAtLocation(node)?.valueDeclaration : null;
|
|
11094
12103
|
const where = `${path.relative(process.cwd(), node.getSourceFile().fileName)}:${node.getStart() >= 0 ? node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).line + 1 : 0}`;
|
|
@@ -11210,8 +12219,9 @@ function visitCalls(node) {
|
|
|
11210
12219
|
// `globalThis.process.hrtime.bigint` → "hrtime.bigint", `(global as any).process.send` → "send". Reuses
|
|
11211
12220
|
// `identIsGlobalProcess` (which handles bare `process` AND `globalThis`/`global.process`, unwrapping casts,
|
|
11212
12221
|
// and rejects a project-local `const process = {…}` shadow so a pure local method never fabricates Clock/Ipc).
|
|
12222
|
+
// ⟨R1075⟩ …or rooted at a MEMBER imported from node's `process` module: `import { hrtime } from "node:process";
|
|
12223
|
+
// hrtime()` / `hrtime.bigint()` were ABSENT while `process.hrtime()` charged Clock (EXECUTED-shape, `fx75b`).
|
|
11213
12224
|
const processMemberPath = () => {
|
|
11214
|
-
if (!ts.isPropertyAccessExpression(callee)) return null;
|
|
11215
12225
|
const outer = [];
|
|
11216
12226
|
let n = callee;
|
|
11217
12227
|
while (ts.isPropertyAccessExpression(n)) {
|
|
@@ -11219,7 +12229,8 @@ function visitCalls(node) {
|
|
|
11219
12229
|
outer.push(n.name.text);
|
|
11220
12230
|
n = n.expression;
|
|
11221
12231
|
}
|
|
11222
|
-
|
|
12232
|
+
const im = ts.isIdentifier(n) ? processModuleBinding(n) : null;
|
|
12233
|
+
return im && im !== "process" ? [im, ...outer.reverse()].join(".") : null;
|
|
11223
12234
|
};
|
|
11224
12235
|
const pmp = processMemberPath();
|
|
11225
12236
|
// R114 — THE MEMBER SET IS THE SHARED CONSTANT, read on the LAST segment of the path. This arm and
|
|
@@ -11290,7 +12301,7 @@ function visitCalls(node) {
|
|
|
11290
12301
|
}
|
|
11291
12302
|
// Not an import at all: the ambient global, unless the project declares its own.
|
|
11292
12303
|
if (callee.text !== "fetch") return false;
|
|
11293
|
-
return !(sym0.declarations
|
|
12304
|
+
return !projectShadows(sym0.declarations); // ⟨R1074⟩
|
|
11294
12305
|
})())
|
|
11295
12306
|
geff = "Net";
|
|
11296
12307
|
// the fully-qualified global fetch — `globalThis.fetch`/`window.fetch`/`self.fetch` — is a
|
|
@@ -11380,8 +12391,7 @@ function visitCalls(node) {
|
|
|
11380
12391
|
// never a concrete effect. (Found by real-world corpus testing; sibling of the node:vm fix.)
|
|
11381
12392
|
if (ts.isIdentifier(callee) && callee.text === "require"
|
|
11382
12393
|
&& node.arguments?.length === 1 && !ts.isStringLiteralLike(node.arguments[0])
|
|
11383
|
-
&& !(checker.getSymbolAtLocation(callee)?.declarations
|
|
11384
|
-
.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)))) {
|
|
12394
|
+
&& !projectShadows(checker.getSymbolAtLocation(callee)?.declarations)) { // ⟨R1074⟩
|
|
11385
12395
|
const owner = enclosing(node);
|
|
11386
12396
|
if (owner) { fns.get(owner).direct.add("Unknown"); fns.get(owner).why.add("reflect:require"); }
|
|
11387
12397
|
}
|
|
@@ -11419,7 +12429,7 @@ function visitCalls(node) {
|
|
|
11419
12429
|
try { sym = checker.getAliasedSymbol(sym); } catch { /* unresolved import — treat as external */ }
|
|
11420
12430
|
}
|
|
11421
12431
|
const decls = sym?.declarations ?? [];
|
|
11422
|
-
return !decls
|
|
12432
|
+
return !projectShadows(decls); // ⟨R1074⟩
|
|
11423
12433
|
};
|
|
11424
12434
|
const fromCreateRequire = (expr) => {
|
|
11425
12435
|
if (!ts.isIdentifier(expr)) return false;
|
|
@@ -12112,14 +13122,27 @@ for (const sf of sources) visitCalls(sf);
|
|
|
12112
13122
|
add(staticBlockQual(m), "static"); // R815: a static field initialiser shares the block's unit
|
|
12113
13123
|
// R815: the `extends <expr>` unit — run by the definer (here) and reached by every construction (`super`).
|
|
12114
13124
|
const ctorQ = nodeName.get(cls);
|
|
12115
|
-
const herQ =
|
|
13125
|
+
const herQ = heritageQualOf(ctorQ);
|
|
12116
13126
|
// Wired only when the expression attributed something: a bare `extends Base` mints the unit (a reference was
|
|
12117
13127
|
// attributed) and carries nothing, and an edge to an empty unit is a `calls` entry on every subclass for no answer.
|
|
12118
13128
|
const herR = herQ && fns.get(herQ);
|
|
12119
13129
|
if (herR && ["direct", "edges", "why", "blind", "hosts", "paths", "cmds", "incomplete", "dispatch", "tables"]
|
|
12120
13130
|
.some((k) => herR[k]?.size)) { add(herQ, "heritage"); fns.get(ctorQ)?.edges.add(herQ); }
|
|
13131
|
+
// ⟨R1016⟩ the containment edge to a class expression's constructor, unless its value cannot escape a `new`.
|
|
13132
|
+
// Asked from the class itself rather than its parent, so a `static X = class {…}` lands on `<static-init>`.
|
|
13133
|
+
const ceq = ts.isClassExpression(cls) ? classExprCtorQual.get(cls) : null;
|
|
13134
|
+
if (ceq && classExprEscapes(cls)) {
|
|
13135
|
+
const cfrom = enclosing(cls);
|
|
13136
|
+
const crec = cfrom && cfrom !== ceq ? fns.get(cfrom) : null;
|
|
13137
|
+
if (crec && !crec.edges.has(ceq)) { crec.edges.add(ceq); hit("classexpr"); }
|
|
13138
|
+
} else if (ceq && process.env.CANDOR_R1016_REACH) console.error(`R1016-REACH contained-dropped ${ceq}`);
|
|
12121
13139
|
if (!targets.length) continue;
|
|
12122
|
-
|
|
13140
|
+
// ⟨SOUNDNESS R1060⟩ a class EXPRESSION is asked from itself, as the R1016 edge above is: climbing from its PARENT
|
|
13141
|
+
// starts at a `static X = class {…}` property with no child in hand, so R815's static-initialiser rule never
|
|
13142
|
+
// matched and the inner class's definition-time work was wired from the OUTER class's constructor — `new HB()`
|
|
13143
|
+
// charged for a `static {}` block that runs once, at import (EXECUTED), and the module that runs it silent.
|
|
13144
|
+
// A declaration is still asked from its parent: from itself, `nodeName` would answer its own constructor.
|
|
13145
|
+
const from = ts.isClassExpression(cls) ? enclosing(cls) : enclosing(cls.parent);
|
|
12123
13146
|
const rec = from && fns.get(from);
|
|
12124
13147
|
if (!rec) continue;
|
|
12125
13148
|
for (const [q, k] of targets) if (q !== from && !rec.edges.has(q)) { rec.edges.add(q); hit(k); }
|