@afokapu/atdd-bun 0.10.58 → 0.10.59
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/detectors/bun_interlocking_coverage/_shared/interlocking.mjs +258 -36
- package/detectors/bun_interlocking_coverage/checks/interlocking_production_runner.mjs +6 -2
- package/detectors/bun_interlocking_coverage/checks/interlocking_route_coverage.mjs +10 -1
- package/detectors/bun_interlocking_coverage/checks/interlocking_smoke_coverage.mjs +11 -10
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/e2e/smoke/resolve-match.smoke.test.ts +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/plan/_trains/_interlockings/match.yaml +9 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/server.ts +10 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/src/trains/interlocking.ts +12 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/src/trains/journey.ts +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_mediated_train_path_runner/src/trains/runner.ts +6 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/interlockings/scenario-compilation/compile.routes.test.ts +14 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/interlockings/session-replay/replay.routes.test.ts +14 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/interlockings/source-corpus-admission/refuse.routes.test.ts +14 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/smoke/runtime-station-master.test.ts +44 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/e2e/support/production-train.ts +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_journeys/create-world-pack.yaml +18 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_journeys/play-world-scenario.yaml +24 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_journeys/replay-world-session.yaml +18 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_trains/_interlockings/scenario-compilation.yaml +14 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_trains/_interlockings/session-replay.yaml +14 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/plan/_trains/_interlockings/source-corpus-admission.yaml +14 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/src/server.ts +85 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/src/trains/interlocking.ts +64 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/src/trains/journey.ts +71 -0
- package/detectors/bun_interlocking_coverage/fixtures/clean/journey_station_master_production_shape/src/trains/runner.ts +28 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/e2e/smoke/resolve-match.smoke.test.ts +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/plan/_trains/_interlockings/match.yaml +9 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/server.ts +21 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/src/trains/interlocking.ts +12 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/src/trains/journey.ts +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_mediated_direct_business/src/trains/runner.ts +6 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/e2e/smoke/resolve-match.smoke.test.ts +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/plan/_trains/_interlockings/match.yaml +9 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/server.ts +11 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/src/trains/interlocking.ts +12 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/src/trains/journey.test.ts +8 -0
- package/detectors/bun_interlocking_coverage/fixtures/dirty/journey_test_local_lookalike/src/trains/runner.ts +6 -0
- package/detectors/bun_interlocking_infrastructure/checks/interlocking_station_routing.mjs +5 -0
- package/detectors/bun_interlocking_infrastructure/checks/journey_station_routing.mjs +5 -1
- package/integrity.json +41 -7
- package/lib/journey-chain.mjs +677 -0
- package/package.json +3 -2
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
// Journey-mediated Station Master chain, proven on the TypeScript AST with binding resolution.
|
|
2
|
+
//
|
|
3
|
+
// One proof for both interlocking detector families (tester coverage and coder routing). Hop by hop:
|
|
4
|
+
// exported Station Master action -> JourneyRunner.execute -> InterlockingRunner.execute -> TrainRunner
|
|
5
|
+
// Each hop's runner identifier must BIND (type-checker symbol, not spelling) to a named import from a
|
|
6
|
+
// local, non-test production module, and that module must export the runner class with a single own
|
|
7
|
+
// `execute` method that no field, accessor, duplicate, constructor or prototype assignment overrides.
|
|
8
|
+
// Every live return of the hop's method (outside `catch`, which only runs once the hop has thrown) must
|
|
9
|
+
// carry the next hop's execution, and at least one must exist. Lexical scanning missed whole classes of
|
|
10
|
+
// dead, discarded, shadowed or overridden code; these are decided structurally here.
|
|
11
|
+
//
|
|
12
|
+
// Override contract: no module of the chain (Station Master, JourneyRunner, InterlockingRunner and
|
|
13
|
+
// TrainRunner modules), nor any local module the Station Master transitively imports (static,
|
|
14
|
+
// side-effect, re-export, dynamic import or require; relative or through tsconfig/jsconfig path aliases
|
|
15
|
+
// and package.json `imports`), may:
|
|
16
|
+
// - return a value from a runner constructor, or decorate a runner class or its members;
|
|
17
|
+
// - reference `<Runner>.prototype` or `__proto__`, access a runner by a dynamic key, or set a prototype;
|
|
18
|
+
// - write any property of a runner (`this` inside a runner class, a runner class or import, `new` of
|
|
19
|
+
// one, or a const alias) or of anything prototype-derived (`.prototype`, `__proto__`, a dynamic-key
|
|
20
|
+
// access, a getPrototypeOf result, or an alias of one) - casts and parentheses unwrapped;
|
|
21
|
+
// - define through Object.defineProperty/defineProperties/assign or Reflect.set/defineProperty/
|
|
22
|
+
// deleteProperty when aimed at such a target, naming `execute`/`resolveTrain`, or (defineProperty,
|
|
23
|
+
// defineProperties, Reflect.set/defineProperty) using a key that cannot be resolved statically.
|
|
24
|
+
// Runners are recognized by spelling and by checker binding. Any of these refuses the whole chain.
|
|
25
|
+
// Writes to unrelated objects (caches, maps, other classes' `execute`) are not tampering: they cannot
|
|
26
|
+
// change what the chain's freshly constructed runners execute.
|
|
27
|
+
//
|
|
28
|
+
// Known limits, accepted by the owner (operator@desk, M-20261010T151332Z) as static-analysis boundaries:
|
|
29
|
+
// R-a Returns inside `catch` are excluded (they run only after the hop threw), so an always-throwing
|
|
30
|
+
// call placed before the execution inside `try` with a success-shaped value returned from
|
|
31
|
+
// `catch` is not detected. A carrying return outside `catch` is still required.
|
|
32
|
+
// R-b Carriage is structural dependence: any member chain off an execution (e.g.
|
|
33
|
+
// `run.executed.wagons.length`) carries it, and at the HTTP entry a returned value (or the first
|
|
34
|
+
// argument of a returned call) derived from the awaited result counts. The execution itself is
|
|
35
|
+
// still proven to run on every live path.
|
|
36
|
+
import ts from "typescript";
|
|
37
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
38
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
39
|
+
|
|
40
|
+
const programs = new Map();
|
|
41
|
+
|
|
42
|
+
// A single-file program: enough for the checker to bind every identifier in the module to its
|
|
43
|
+
// declaration (imports stay unresolved aliases, so nothing outside the file is read).
|
|
44
|
+
function analyze(file, text = readFileSync(file, "utf8")) {
|
|
45
|
+
const key = `${file}\0${text}`;
|
|
46
|
+
if (programs.has(key)) return programs.get(key);
|
|
47
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
48
|
+
const host = {
|
|
49
|
+
getSourceFile: (name) => (name === file ? source : undefined),
|
|
50
|
+
getDefaultLibFileName: () => "lib.d.ts",
|
|
51
|
+
writeFile: () => {},
|
|
52
|
+
getCurrentDirectory: () => dirname(file),
|
|
53
|
+
getDirectories: () => [],
|
|
54
|
+
fileExists: (name) => name === file,
|
|
55
|
+
readFile: (name) => (name === file ? text : undefined),
|
|
56
|
+
getCanonicalFileName: (name) => name,
|
|
57
|
+
useCaseSensitiveFileNames: () => true,
|
|
58
|
+
getNewLine: () => "\n",
|
|
59
|
+
};
|
|
60
|
+
const program = ts.createProgram([file], { noLib: true, noResolve: true, types: [], target: ts.ScriptTarget.Latest }, host);
|
|
61
|
+
const result = { file, source, checker: program.getTypeChecker() };
|
|
62
|
+
programs.set(key, result);
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Wrappers that keep the operand's value: grouping, await, and type-only assertions.
|
|
67
|
+
function unwrap(node) {
|
|
68
|
+
while (node && (ts.isParenthesizedExpression(node) || ts.isAwaitExpression(node) || ts.isAsExpression(node) ||
|
|
69
|
+
ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node) || ts.isTypeAssertionExpression(node))) {
|
|
70
|
+
node = node.expression;
|
|
71
|
+
}
|
|
72
|
+
return node;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function declarationOf(ctx, identifier) {
|
|
76
|
+
const symbol = ts.isShorthandPropertyAssignment(identifier.parent) && identifier.parent.name === identifier
|
|
77
|
+
? ctx.checker.getShorthandAssignmentValueSymbol(identifier.parent)
|
|
78
|
+
: ctx.checker.getSymbolAtLocation(identifier);
|
|
79
|
+
return symbol?.declarations?.[0] ?? null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const symbolOf = (ctx, identifier) =>
|
|
83
|
+
(ts.isShorthandPropertyAssignment(identifier.parent) && identifier.parent.name === identifier
|
|
84
|
+
? ctx.checker.getShorthandAssignmentValueSymbol(identifier.parent)
|
|
85
|
+
: ctx.checker.getSymbolAtLocation(identifier)) ?? null;
|
|
86
|
+
|
|
87
|
+
// A free identifier (no declaration in the module), i.e. the ambient global such as Object/console.
|
|
88
|
+
const isGlobal = (ctx, node, name) => ts.isIdentifier(node) && node.text === name && !declarationOf(ctx, node);
|
|
89
|
+
|
|
90
|
+
function productionModule(specifier, file, croot) {
|
|
91
|
+
if (!specifier.startsWith(".")) return null;
|
|
92
|
+
const target = resolve(dirname(file), specifier);
|
|
93
|
+
const root = resolve(croot) + sep;
|
|
94
|
+
const candidates = [target, `${target}.ts`, `${target}.tsx`, `${target}.js`, `${target}.mjs`, join(target, "index.ts")];
|
|
95
|
+
return candidates.find((candidate) => existsSync(candidate) && statSync(candidate).isFile() && candidate.startsWith(root) &&
|
|
96
|
+
!/\.(test|spec)\.[cm]?[jt]sx?$/.test(candidate)) ?? null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The production module `identifier` binds to when it is a value import of `name`, else null.
|
|
100
|
+
function importedRunner(ctx, identifier, name, croot) {
|
|
101
|
+
if (!identifier || !ts.isIdentifier(identifier)) return null;
|
|
102
|
+
const declaration = declarationOf(ctx, identifier);
|
|
103
|
+
if (!declaration || !ts.isImportSpecifier(declaration) || declaration.isTypeOnly) return null;
|
|
104
|
+
if ((declaration.propertyName ?? declaration.name).text !== name) return null;
|
|
105
|
+
const clause = declaration.parent.parent;
|
|
106
|
+
const statement = clause.parent;
|
|
107
|
+
if (clause.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier)) return null;
|
|
108
|
+
return productionModule(statement.moduleSpecifier.text, ctx.file, croot);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function constInitializer(ctx, identifier) {
|
|
112
|
+
const declaration = declarationOf(ctx, identifier);
|
|
113
|
+
return declaration && ts.isVariableDeclaration(declaration) && declaration.initializer &&
|
|
114
|
+
ts.isVariableDeclarationList(declaration.parent) && declaration.parent.flags & ts.NodeFlags.Const
|
|
115
|
+
? declaration.initializer
|
|
116
|
+
: null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const memberName = (member) => {
|
|
120
|
+
const name = member.name;
|
|
121
|
+
if (!name) return null;
|
|
122
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isPrivateIdentifier(name)) return name.text;
|
|
123
|
+
if (ts.isComputedPropertyName(name) && ts.isStringLiteralLike(name.expression)) return name.expression.text;
|
|
124
|
+
return null;
|
|
125
|
+
};
|
|
126
|
+
const isStatic = (member) => Boolean(ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Static);
|
|
127
|
+
|
|
128
|
+
const RUNNERS = new Set(["JourneyRunner", "InterlockingRunner", "TrainRunner"]);
|
|
129
|
+
const MEMBERS = new Set(["execute", "resolveTrain"]);
|
|
130
|
+
const accessName = (node) =>
|
|
131
|
+
ts.isPropertyAccessExpression(node) ? node.name.text
|
|
132
|
+
: ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text
|
|
133
|
+
: null;
|
|
134
|
+
|
|
135
|
+
// Why the module can replace a runner's behaviour outside its declared method (see the override
|
|
136
|
+
// contract above), or null. Deliberately module-wide: it fails closed.
|
|
137
|
+
function tampered(ctx) {
|
|
138
|
+
let found = null;
|
|
139
|
+
const runnerClass = (node) => (ts.isClassDeclaration(node) || ts.isClassExpression(node)) && RUNNERS.has(node.name?.text);
|
|
140
|
+
const insideRunnerClass = (node) => {
|
|
141
|
+
for (let parent = node.parent; parent; parent = parent.parent) if (ts.isClassLike(parent)) return runnerClass(parent);
|
|
142
|
+
return false;
|
|
143
|
+
};
|
|
144
|
+
const dynamic = (access) => ts.isElementAccessExpression(access) && !ts.isStringLiteralLike(access.argumentExpression) &&
|
|
145
|
+
!ts.isNumericLiteral(access.argumentExpression);
|
|
146
|
+
// A runner: `this` inside a runner class, a runner by spelling, a runner class declaration or import,
|
|
147
|
+
// `new` of a runner, or a const alias of any of these (followed through the checker binding).
|
|
148
|
+
const runnerish = (node, seen = new Set()) => {
|
|
149
|
+
node = unwrap(node);
|
|
150
|
+
if (!node) return false;
|
|
151
|
+
if (node.kind === ts.SyntaxKind.ThisKeyword) return insideRunnerClass(node);
|
|
152
|
+
if (ts.isNewExpression(node)) return runnerish(node.expression, seen);
|
|
153
|
+
if (!ts.isIdentifier(node)) return false;
|
|
154
|
+
if (RUNNERS.has(node.text)) return true;
|
|
155
|
+
const declaration = declarationOf(ctx, node);
|
|
156
|
+
if (!declaration || seen.has(declaration)) return false;
|
|
157
|
+
seen.add(declaration);
|
|
158
|
+
if (ts.isClassDeclaration(declaration) && RUNNERS.has(declaration.name?.text)) return true;
|
|
159
|
+
if (ts.isImportSpecifier(declaration) && RUNNERS.has((declaration.propertyName ?? declaration.name).text)) return true;
|
|
160
|
+
const initializer = constInitializer(ctx, node);
|
|
161
|
+
return Boolean(initializer && runnerish(initializer, seen));
|
|
162
|
+
};
|
|
163
|
+
// Something that may be a prototype: `.prototype`/`__proto__`, a dynamic-key access, a
|
|
164
|
+
// getPrototypeOf result, or a const alias of one.
|
|
165
|
+
const prototypeish = (node, seen = new Set()) => {
|
|
166
|
+
node = unwrap(node);
|
|
167
|
+
if (!node) return false;
|
|
168
|
+
const name = accessName(node);
|
|
169
|
+
if (name === "prototype" || name === "__proto__" || dynamic(node)) return true;
|
|
170
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "getPrototypeOf" &&
|
|
171
|
+
(isGlobal(ctx, node.expression.expression, "Object") || isGlobal(ctx, node.expression.expression, "Reflect"))) return true;
|
|
172
|
+
if (!ts.isIdentifier(node)) return false;
|
|
173
|
+
const declaration = declarationOf(ctx, node);
|
|
174
|
+
if (!declaration || seen.has(declaration)) return false;
|
|
175
|
+
seen.add(declaration);
|
|
176
|
+
const initializer = constInitializer(ctx, node);
|
|
177
|
+
return Boolean(initializer && prototypeish(initializer, seen));
|
|
178
|
+
};
|
|
179
|
+
// A write target that can change what the chain's `new Runner(...).execute` runs.
|
|
180
|
+
const reachesRunner = (node) => runnerish(node) || prototypeish(node);
|
|
181
|
+
const literalKey = (node) => {
|
|
182
|
+
const key = node && unwrap(node);
|
|
183
|
+
return Boolean(key) && (ts.isStringLiteralLike(key) || ts.isNumericLiteral(key));
|
|
184
|
+
};
|
|
185
|
+
const plainMembers = (node) => {
|
|
186
|
+
const value = node && unwrap(node);
|
|
187
|
+
return Boolean(value) && ts.isObjectLiteralExpression(value) && value.properties.every((property) => memberName(property) !== null);
|
|
188
|
+
};
|
|
189
|
+
const namesMember = (node) => {
|
|
190
|
+
const value = node && unwrap(node);
|
|
191
|
+
if (!value) return false;
|
|
192
|
+
if (ts.isStringLiteralLike(value)) return MEMBERS.has(value.text);
|
|
193
|
+
return ts.isObjectLiteralExpression(value) && value.properties.some((property) => MEMBERS.has(memberName(property)));
|
|
194
|
+
};
|
|
195
|
+
walk(ctx.source, (node) => {
|
|
196
|
+
if (found) return false;
|
|
197
|
+
const name = accessName(node);
|
|
198
|
+
if (name === "__proto__") found = "__proto__";
|
|
199
|
+
if (name === "prototype" && runnerish(node.expression)) found = "runner prototype";
|
|
200
|
+
if (dynamic(node) && runnerish(node.expression)) found = "dynamic runner access";
|
|
201
|
+
// Decorators can replace a runner class or its members at definition time.
|
|
202
|
+
if (ts.isDecorator(node)) {
|
|
203
|
+
const owner = ts.isClassLike(node.parent) ? node.parent : node.parent?.parent;
|
|
204
|
+
if (owner && runnerClass(owner)) found = "runner decorator";
|
|
205
|
+
}
|
|
206
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
|
207
|
+
node.operatorToken.kind <= ts.SyntaxKind.LastAssignment) {
|
|
208
|
+
const target = unwrap(node.left);
|
|
209
|
+
if ((ts.isPropertyAccessExpression(target) || ts.isElementAccessExpression(target)) &&
|
|
210
|
+
(reachesRunner(target.expression) || (MEMBERS.has(accessName(target)) && prototypeish(target.expression)))) found = "runner write";
|
|
211
|
+
}
|
|
212
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
213
|
+
const api = node.expression.name.text;
|
|
214
|
+
const object = node.expression.expression;
|
|
215
|
+
const args = node.arguments;
|
|
216
|
+
const isObject = isGlobal(ctx, object, "Object"), isReflect = isGlobal(ctx, object, "Reflect");
|
|
217
|
+
if ((isObject || isReflect) && api === "setPrototypeOf") found = "prototype api";
|
|
218
|
+
// Reflective definitions: aimed at a runner or prototype, naming execute/resolveTrain, or (for
|
|
219
|
+
// defineProperty/defineProperties/Reflect.set/defineProperty) using a key that cannot be resolved.
|
|
220
|
+
const defines = (isObject && ["defineProperty", "defineProperties", "assign"].includes(api)) ||
|
|
221
|
+
(isReflect && ["set", "defineProperty", "deleteProperty"].includes(api));
|
|
222
|
+
if (defines && (reachesRunner(args[0]) || args.slice(1).some(namesMember) ||
|
|
223
|
+
(["defineProperty", "set"].includes(api) && !literalKey(args[1])) ||
|
|
224
|
+
(api === "defineProperties" && !plainMembers(args[1])))) found = "reflective define";
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
return found;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The config Bun applies to `file`: walking up from its directory, the first directory holding a
|
|
231
|
+
// tsconfig.json (preferred) or a jsconfig.json.
|
|
232
|
+
function nearestConfig(file) {
|
|
233
|
+
for (let dir = dirname(resolve(file)); ; dir = dirname(dir)) {
|
|
234
|
+
for (const name of ["tsconfig.json", "jsconfig.json"]) {
|
|
235
|
+
const candidate = join(dir, name);
|
|
236
|
+
if (existsSync(candidate)) return candidate;
|
|
237
|
+
}
|
|
238
|
+
if (dirname(dir) === dir) return "";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Compiler options for resolving `file`'s non-relative specifiers the way Bun does: the nearest
|
|
243
|
+
// tsconfig.json or jsconfig.json (its `baseUrl`/`paths`), in Bundler resolution, which also honors
|
|
244
|
+
// package.json `imports`.
|
|
245
|
+
const resolutionOptions = new Map();
|
|
246
|
+
function optionsFor(file) {
|
|
247
|
+
const config = nearestConfig(file);
|
|
248
|
+
if (!resolutionOptions.has(config)) {
|
|
249
|
+
let options = {};
|
|
250
|
+
if (config) {
|
|
251
|
+
const read = ts.readConfigFile(config, ts.sys.readFile);
|
|
252
|
+
if (!read.error) options = ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(config)).options;
|
|
253
|
+
}
|
|
254
|
+
resolutionOptions.set(config, {
|
|
255
|
+
...options,
|
|
256
|
+
module: ts.ModuleKind.ESNext,
|
|
257
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
258
|
+
allowImportingTsExtensions: true,
|
|
259
|
+
noEmit: true,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return resolutionOptions.get(config);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Every local module reachable from `file` through static, side-effect, re-export, dynamic `import()`
|
|
266
|
+
// and `require()` specifiers, including `file` itself. Relative specifiers resolve against the file;
|
|
267
|
+
// non-relative ones (tsconfig path aliases, package.json `#imports`) resolve through the TypeScript
|
|
268
|
+
// resolver and count when they land inside `croot` outside node_modules.
|
|
269
|
+
function localImportClosure(file, croot) {
|
|
270
|
+
const root = resolve(croot) + sep;
|
|
271
|
+
const seen = new Set([file]);
|
|
272
|
+
const queue = [file];
|
|
273
|
+
while (queue.length) {
|
|
274
|
+
const current = queue.pop();
|
|
275
|
+
let ctx;
|
|
276
|
+
try { ctx = analyze(current); } catch { continue; }
|
|
277
|
+
const specifiers = [];
|
|
278
|
+
walk(ctx.source, (node) => {
|
|
279
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) specifiers.push(node.moduleSpecifier.text);
|
|
280
|
+
if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || isGlobal(ctx, node.expression, "require")) &&
|
|
281
|
+
node.arguments[0] && ts.isStringLiteralLike(node.arguments[0])) specifiers.push(node.arguments[0].text);
|
|
282
|
+
});
|
|
283
|
+
for (const specifier of specifiers) {
|
|
284
|
+
let found;
|
|
285
|
+
if (specifier.startsWith(".")) {
|
|
286
|
+
const target = resolve(dirname(current), specifier);
|
|
287
|
+
found = [target, `${target}.ts`, `${target}.tsx`, `${target}.js`, `${target}.mjs`, join(target, "index.ts")]
|
|
288
|
+
.find((candidate) => existsSync(candidate) && statSync(candidate).isFile() && candidate.startsWith(root));
|
|
289
|
+
} else {
|
|
290
|
+
const resolved = ts.resolveModuleName(specifier, current, optionsFor(current), ts.sys).resolvedModule;
|
|
291
|
+
const target = resolved && !resolved.isExternalLibraryImport ? resolve(resolved.resolvedFileName) : null;
|
|
292
|
+
found = target && target.startsWith(root) && !target.split(sep).includes("node_modules") && !target.endsWith(".d.ts")
|
|
293
|
+
? target : undefined;
|
|
294
|
+
}
|
|
295
|
+
if (found && !seen.has(found)) { seen.add(found); queue.push(found); }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return seen;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// A runner constructor that returns a value makes `new Runner(...)` yield that value, not the class.
|
|
302
|
+
function constructorReturnsValue(cls) {
|
|
303
|
+
let found = false;
|
|
304
|
+
for (const member of cls.members) {
|
|
305
|
+
if (!ts.isConstructorDeclaration(member) || !member.body) continue;
|
|
306
|
+
walk(member.body, (node) => { if (ts.isReturnStatement(node) && node.expression) found = true; }, false);
|
|
307
|
+
}
|
|
308
|
+
return found;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function exportedClass(ctx, name) {
|
|
312
|
+
let found = null, exported = false;
|
|
313
|
+
for (const statement of ctx.source.statements) {
|
|
314
|
+
if (ts.isClassDeclaration(statement) && statement.name?.text === name) {
|
|
315
|
+
if (found) return null;
|
|
316
|
+
found = statement;
|
|
317
|
+
exported ||= Boolean(ts.getCombinedModifierFlags(statement) & ts.ModifierFlags.Export);
|
|
318
|
+
}
|
|
319
|
+
if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
320
|
+
exported ||= statement.exportClause.elements.some((element) => (element.propertyName ?? element.name).text === name);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return found && exported ? found : null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function walk(node, visit, intoFunctions = true) {
|
|
327
|
+
(function step(child) {
|
|
328
|
+
if (visit(child) === false) return;
|
|
329
|
+
if (!intoFunctions && child !== node && ts.isFunctionLike(child)) return;
|
|
330
|
+
ts.forEachChild(child, step);
|
|
331
|
+
})(node);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// The class's own instance method `name`, provided nothing else can replace it on an instance.
|
|
335
|
+
function soleMethod(ctx, cls, name) {
|
|
336
|
+
const members = cls.members.filter((member) => !ts.isConstructorDeclaration(member) && memberName(member) === name);
|
|
337
|
+
const method = members[0];
|
|
338
|
+
if (members.length !== 1 || !ts.isMethodDeclaration(method) || !method.body || isStatic(method)) return null;
|
|
339
|
+
let overridden = false;
|
|
340
|
+
const assigns = (target) => {
|
|
341
|
+
const node = unwrap(target);
|
|
342
|
+
return (ts.isPropertyAccessExpression(node) && node.name.text === name) ||
|
|
343
|
+
(ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression) && node.argumentExpression.text === name);
|
|
344
|
+
};
|
|
345
|
+
walk(ctx.source, (node) => {
|
|
346
|
+
if (overridden) return false;
|
|
347
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && assigns(node.left)) {
|
|
348
|
+
const owner = unwrap(unwrap(node.left).expression);
|
|
349
|
+
if (owner.kind === ts.SyntaxKind.ThisKeyword || /\bprototype\b/.test(owner.getText())) overridden = true;
|
|
350
|
+
}
|
|
351
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && isGlobal(ctx, node.expression.expression, "Object") &&
|
|
352
|
+
["defineProperty", "defineProperties", "assign", "setPrototypeOf"].includes(node.expression.name.text) &&
|
|
353
|
+
node.arguments[0] && /\bprototype\b|^this$/.test(node.arguments[0].getText())) overridden = true;
|
|
354
|
+
});
|
|
355
|
+
return overridden ? null : method;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Statically known truthiness of a condition, or undefined.
|
|
359
|
+
function constant(node) {
|
|
360
|
+
node = unwrap(node);
|
|
361
|
+
if (!node) return undefined;
|
|
362
|
+
switch (node.kind) {
|
|
363
|
+
case ts.SyntaxKind.TrueKeyword: return true;
|
|
364
|
+
case ts.SyntaxKind.FalseKeyword: case ts.SyntaxKind.NullKeyword: return false;
|
|
365
|
+
}
|
|
366
|
+
if (ts.isNumericLiteral(node)) return Number(node.text) !== 0;
|
|
367
|
+
if (ts.isStringLiteralLike(node)) return node.text.length > 0;
|
|
368
|
+
if (ts.isIdentifier(node) && node.text === "undefined") return false;
|
|
369
|
+
if (ts.isVoidExpression(node)) return false;
|
|
370
|
+
if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) {
|
|
371
|
+
const inner = constant(node.operand);
|
|
372
|
+
return inner === undefined ? undefined : !inner;
|
|
373
|
+
}
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// True when control can never continue past `statement`.
|
|
378
|
+
function terminates(statement) {
|
|
379
|
+
if (ts.isReturnStatement(statement) || ts.isThrowStatement(statement) || ts.isBreakStatement(statement) || ts.isContinueStatement(statement)) return true;
|
|
380
|
+
if (ts.isBlock(statement)) return statement.statements.some(terminates);
|
|
381
|
+
if (ts.isIfStatement(statement)) {
|
|
382
|
+
const value = constant(statement.expression);
|
|
383
|
+
if (value === true) return terminates(statement.thenStatement);
|
|
384
|
+
if (value === false) return Boolean(statement.elseStatement && terminates(statement.elseStatement));
|
|
385
|
+
return Boolean(statement.elseStatement && terminates(statement.thenStatement) && terminates(statement.elseStatement));
|
|
386
|
+
}
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// True when `node` can never run: a constant-false branch or loop, or code after a terminating statement.
|
|
391
|
+
function dead(node, boundary) {
|
|
392
|
+
for (let child = node, parent = node.parent; child !== boundary && parent; child = parent, parent = parent.parent) {
|
|
393
|
+
if (ts.isIfStatement(parent)) {
|
|
394
|
+
const value = constant(parent.expression);
|
|
395
|
+
if (child === parent.thenStatement && value === false) return true;
|
|
396
|
+
if (child === parent.elseStatement && value === true) return true;
|
|
397
|
+
}
|
|
398
|
+
if ((ts.isWhileStatement(parent) || ts.isForStatement(parent)) && child === parent.statement && parent.condition !== undefined &&
|
|
399
|
+
constant(ts.isWhileStatement(parent) ? parent.expression : parent.condition) === false) return true;
|
|
400
|
+
const statements = ts.isBlock(parent) || ts.isSourceFile(parent) || ts.isCaseClause(parent) || ts.isDefaultClause(parent) ? parent.statements : null;
|
|
401
|
+
if (statements) {
|
|
402
|
+
const index = statements.indexOf(child);
|
|
403
|
+
if (statements.slice(0, index).some(terminates)) return true;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const insideCatch = (node, boundary) => {
|
|
410
|
+
for (let parent = node.parent; parent && parent !== boundary; parent = parent.parent) if (ts.isCatchClause(parent)) return true;
|
|
411
|
+
return false;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// Expressions of the function's live, non-catch returns; null when a `finally` can override them.
|
|
415
|
+
function liveReturns(fn) {
|
|
416
|
+
if (!fn.body) return null;
|
|
417
|
+
if (!ts.isBlock(fn.body)) return [fn.body];
|
|
418
|
+
const returns = [];
|
|
419
|
+
let overridable = false;
|
|
420
|
+
walk(fn.body, (node) => {
|
|
421
|
+
if (ts.isTryStatement(node) && node.finallyBlock) {
|
|
422
|
+
walk(node.finallyBlock, (inner) => { if (ts.isReturnStatement(inner)) overridable = true; }, false);
|
|
423
|
+
}
|
|
424
|
+
if (ts.isReturnStatement(node) && !insideCatch(node, fn.body) && !dead(node, fn.body)) returns.push(node.expression ?? null);
|
|
425
|
+
}, false);
|
|
426
|
+
return overridable ? null : returns;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// The value of `expression` IS a matching expression or carries it unchanged: directly, through both
|
|
430
|
+
// arms of a conditional, as a member/spread of it, an object property or array element, an
|
|
431
|
+
// `Object.freeze` argument, or a `const` bound to it. Logical, comma, arithmetic, unary (`void`, `!`)
|
|
432
|
+
// and calls to anything else replace or may skip the value, so they never carry it.
|
|
433
|
+
function carries(ctx, expression, matches, seen = new Set()) {
|
|
434
|
+
const node = unwrap(expression);
|
|
435
|
+
if (!node) return false;
|
|
436
|
+
if (matches(node)) return true;
|
|
437
|
+
if (ts.isIdentifier(node)) {
|
|
438
|
+
const initializer = constInitializer(ctx, node);
|
|
439
|
+
// `seen` guards cycles along this path only; sibling branches may resolve the same const.
|
|
440
|
+
if (!initializer || seen.has(initializer)) return false;
|
|
441
|
+
seen.add(initializer);
|
|
442
|
+
const carried = carries(ctx, initializer, matches, seen);
|
|
443
|
+
seen.delete(initializer);
|
|
444
|
+
return carried;
|
|
445
|
+
}
|
|
446
|
+
if (ts.isConditionalExpression(node)) return carries(ctx, node.whenTrue, matches, seen) && carries(ctx, node.whenFalse, matches, seen);
|
|
447
|
+
if (ts.isPropertyAccessExpression(node)) return carries(ctx, node.expression, matches, seen);
|
|
448
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
449
|
+
return node.properties.some((property) =>
|
|
450
|
+
(ts.isPropertyAssignment(property) && carries(ctx, property.initializer, matches, seen)) ||
|
|
451
|
+
(ts.isShorthandPropertyAssignment(property) && carries(ctx, property.name, matches, seen)) ||
|
|
452
|
+
(ts.isSpreadAssignment(property) && carries(ctx, property.expression, matches, seen)));
|
|
453
|
+
}
|
|
454
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
455
|
+
return node.elements.some((element) => carries(ctx, ts.isSpreadElement(element) ? element.expression : element, matches, seen));
|
|
456
|
+
}
|
|
457
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) &&
|
|
458
|
+
isGlobal(ctx, node.expression.expression, "Object") && node.expression.name.text === "freeze") {
|
|
459
|
+
return node.arguments.some((argument) => carries(ctx, argument, matches, seen));
|
|
460
|
+
}
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function allReturnsCarry(ctx, fn, matches) {
|
|
465
|
+
const returns = liveReturns(fn);
|
|
466
|
+
return Boolean(returns?.length) && returns.every((expression) => expression && carries(ctx, expression, matches));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// `<receiver>.execute(...)` where the receiver is `new <Runner>(...)` (or a const bound to one) and
|
|
470
|
+
// <Runner> binds to a production import; each such module is added to `modules`.
|
|
471
|
+
function executesImported(ctx, runner, croot, modules, accept = () => true) {
|
|
472
|
+
const construction = (node) => {
|
|
473
|
+
node = unwrap(node);
|
|
474
|
+
if (ts.isIdentifier(node)) node = unwrap(constInitializer(ctx, node));
|
|
475
|
+
if (!node || !ts.isNewExpression(node) || !accept(node)) return null;
|
|
476
|
+
return importedRunner(ctx, node.expression, runner, croot);
|
|
477
|
+
};
|
|
478
|
+
return (node) => {
|
|
479
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "execute") return false;
|
|
480
|
+
const module = construction(node.expression.expression);
|
|
481
|
+
if (module) modules.add(module);
|
|
482
|
+
return Boolean(module);
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Each module must export `runner` whose sole execute satisfies `proof(ctx, method)`.
|
|
487
|
+
function everyModule(modules, runner, proof) {
|
|
488
|
+
return modules.size > 0 && [...modules].every((file) => {
|
|
489
|
+
const ctx = analyze(file);
|
|
490
|
+
const cls = exportedClass(ctx, runner);
|
|
491
|
+
const method = cls && !tampered(ctx) && !constructorReturnsValue(cls) && soleMethod(ctx, cls, "execute");
|
|
492
|
+
return Boolean(method && proof(ctx, cls, method));
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// The TrainRunner execution of the resolution bound to `symbol`: legacy
|
|
497
|
+
// `new TrainRunner(r.trainId|r.selectedTrainId).execute(...)`, or declaration
|
|
498
|
+
// `new TrainRunner(r.trainPath, ...args without r).execute(r, ...)`.
|
|
499
|
+
function trainExecutionOf(ctx, symbol, croot, modules = new Set()) {
|
|
500
|
+
const isResolution = (node) => Boolean(node) && ts.isIdentifier(unwrap(node)) && symbolOf(ctx, unwrap(node)) === symbol;
|
|
501
|
+
const member = (node, names) => ts.isPropertyAccessExpression(node) && isResolution(node.expression) && names.includes(node.name.text);
|
|
502
|
+
const mentions = (node) => {
|
|
503
|
+
let found = false;
|
|
504
|
+
walk(node, (child) => { if (found) return false; if (ts.isIdentifier(child) && symbolOf(ctx, child) === symbol) found = true; });
|
|
505
|
+
return found;
|
|
506
|
+
};
|
|
507
|
+
return (node) => {
|
|
508
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "execute") return false;
|
|
509
|
+
const runner = unwrap(node.expression.expression);
|
|
510
|
+
const module = ts.isNewExpression(runner) ? importedRunner(ctx, runner.expression, "TrainRunner", croot) : null;
|
|
511
|
+
if (!module) return false;
|
|
512
|
+
const [first, ...rest] = runner.arguments ?? [];
|
|
513
|
+
if (!first) return false;
|
|
514
|
+
const matched = (rest.length === 0 && member(first, ["trainId", "selectedTrainId"])) ||
|
|
515
|
+
(member(first, ["trainPath"]) && !rest.some(mentions) && isResolution(node.arguments[0]));
|
|
516
|
+
if (matched) modules.add(module);
|
|
517
|
+
return matched;
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Every reference to the resolution must leave it unchanged and unaliased: member reads (never
|
|
522
|
+
// assigned, updated, deleted or called), `void` and global `console.*` operands, the TrainRunner
|
|
523
|
+
// execution's own resolution argument, and object/array values inside a live returned expression.
|
|
524
|
+
function resolutionUnchanged(ctx, method, declaration, symbol, returns, isExecution) {
|
|
525
|
+
let safe = true;
|
|
526
|
+
const inReturn = (node) => returns.some((expression) => expression && node.pos >= expression.pos && node.end <= expression.end);
|
|
527
|
+
walk(method, (node) => {
|
|
528
|
+
if (!safe) return false;
|
|
529
|
+
if (!ts.isIdentifier(node) || node === declaration.name || symbolOf(ctx, node) !== symbol) return;
|
|
530
|
+
const parent = node.parent;
|
|
531
|
+
if (ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) {
|
|
532
|
+
if (parent.expression !== node) return;
|
|
533
|
+
let top = parent;
|
|
534
|
+
while ((ts.isPropertyAccessExpression(top.parent) || ts.isElementAccessExpression(top.parent)) && top.parent.expression === top) top = top.parent;
|
|
535
|
+
const holder = top.parent;
|
|
536
|
+
if ((ts.isBinaryExpression(holder) && holder.left === top && holder.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
|
537
|
+
holder.operatorToken.kind <= ts.SyntaxKind.LastAssignment) ||
|
|
538
|
+
((ts.isPrefixUnaryExpression(holder) || ts.isPostfixUnaryExpression(holder)) &&
|
|
539
|
+
(holder.operator === ts.SyntaxKind.PlusPlusToken || holder.operator === ts.SyntaxKind.MinusMinusToken)) ||
|
|
540
|
+
ts.isDeleteExpression(holder) || (ts.isCallExpression(holder) && holder.expression === top)) safe = false;
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (ts.isVoidExpression(parent)) return;
|
|
544
|
+
if (ts.isCallExpression(parent) && parent.arguments.includes(node) && ts.isPropertyAccessExpression(parent.expression) &&
|
|
545
|
+
isGlobal(ctx, parent.expression.expression, "console")) return;
|
|
546
|
+
if (ts.isCallExpression(parent) && parent.arguments[0] === node && isExecution(parent)) return;
|
|
547
|
+
if ((ts.isShorthandPropertyAssignment(parent) || (ts.isPropertyAssignment(parent) && parent.initializer === node) ||
|
|
548
|
+
ts.isArrayLiteralExpression(parent)) && inReturn(node)) return;
|
|
549
|
+
safe = false;
|
|
550
|
+
});
|
|
551
|
+
return safe;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// InterlockingRunner.execute: `const r = this.resolveTrain(...)` (resolveTrain also a sole method), and
|
|
555
|
+
// every live return carries r's TrainRunner execution with r unchanged throughout.
|
|
556
|
+
function interlockingExecutes(croot) {
|
|
557
|
+
return (ctx, cls, method) => {
|
|
558
|
+
if (!soleMethod(ctx, cls, "resolveTrain")) return false;
|
|
559
|
+
const returns = liveReturns(method);
|
|
560
|
+
if (!returns?.length) return false;
|
|
561
|
+
const declarations = [];
|
|
562
|
+
walk(method.body, (node) => {
|
|
563
|
+
if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name) || !node.initializer) return;
|
|
564
|
+
const call = unwrap(node.initializer);
|
|
565
|
+
if (ts.isCallExpression(call) && ts.isPropertyAccessExpression(call.expression) &&
|
|
566
|
+
call.expression.expression.kind === ts.SyntaxKind.ThisKeyword && call.expression.name.text === "resolveTrain") declarations.push(node);
|
|
567
|
+
}, false);
|
|
568
|
+
return declarations.some((declaration) => {
|
|
569
|
+
const symbol = symbolOf(ctx, declaration.name);
|
|
570
|
+
const trains = new Set();
|
|
571
|
+
const isExecution = trainExecutionOf(ctx, symbol, croot, trains);
|
|
572
|
+
return Boolean(symbol) && allReturnsCarry(ctx, method, isExecution) &&
|
|
573
|
+
resolutionUnchanged(ctx, method, declaration, symbol, returns, isExecution) &&
|
|
574
|
+
everyModule(trains, "TrainRunner", () => true);
|
|
575
|
+
});
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function journeyExecutes(croot) {
|
|
580
|
+
return (ctx, _cls, method) => {
|
|
581
|
+
const modules = new Set();
|
|
582
|
+
return allReturnsCarry(ctx, method, executesImported(ctx, "InterlockingRunner", croot, modules)) &&
|
|
583
|
+
everyModule(modules, "InterlockingRunner", interlockingExecutes(croot));
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Exported functions of the module by name: declarations and const-bound arrow/function expressions.
|
|
588
|
+
function exportedFunctions(ctx) {
|
|
589
|
+
const out = new Map();
|
|
590
|
+
for (const statement of ctx.source.statements) {
|
|
591
|
+
const exported = Boolean(ts.getCombinedModifierFlags(statement) & ts.ModifierFlags.Export);
|
|
592
|
+
if (!exported) continue;
|
|
593
|
+
if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) out.set(statement.name.text, statement);
|
|
594
|
+
if (ts.isVariableStatement(statement) && statement.declarationList.flags & ts.NodeFlags.Const) {
|
|
595
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
596
|
+
const init = declaration.initializer && unwrap(declaration.initializer);
|
|
597
|
+
if (ts.isIdentifier(declaration.name) && init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) out.set(declaration.name.text, init);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return out;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// The exported Station Master action `name` returns, on every live path, a JourneyRunner execution
|
|
605
|
+
// (constructed from a mapped `.path`) whose chain reaches TrainRunner as described above.
|
|
606
|
+
export function provenJourneyAction(stationText, stationFile, croot, name) {
|
|
607
|
+
if (!stationText || !stationFile || !croot) return false;
|
|
608
|
+
const ctx = analyze(stationFile, stationText);
|
|
609
|
+
const fn = exportedFunctions(ctx).get(name);
|
|
610
|
+
// The override contract covers the Station Master and every local module it reaches, so a
|
|
611
|
+
// side-effect import cannot patch a runner from outside the chain.
|
|
612
|
+
if (!fn || tampered(ctx) || [...localImportClosure(stationFile, croot)].some((module) => tampered(analyze(module)))) return false;
|
|
613
|
+
const modules = new Set();
|
|
614
|
+
const fromMappedPath = (construction) => {
|
|
615
|
+
const first = construction.arguments?.[0] && unwrap(construction.arguments[0]);
|
|
616
|
+
return Boolean(first && ts.isPropertyAccessExpression(first) && first.name.text === "path");
|
|
617
|
+
};
|
|
618
|
+
return allReturnsCarry(ctx, fn, executesImported(ctx, "JourneyRunner", croot, modules, fromMappedPath)) &&
|
|
619
|
+
everyModule(modules, "JourneyRunner", journeyExecutes(croot));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Names of every exported Station Master action proven by provenJourneyAction.
|
|
623
|
+
export function provenJourneyActions(stationText, stationFile, croot) {
|
|
624
|
+
if (!stationText || !stationFile || !croot) return [];
|
|
625
|
+
const ctx = analyze(stationFile, stationText);
|
|
626
|
+
return [...exportedFunctions(ctx).keys()].filter((name) => provenJourneyAction(stationText, stationFile, croot, name));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// An exported Station Master entry (e.g. an HTTP-shaped stationMaster) that awaits one exported action
|
|
630
|
+
// with a literal action name into a live `const`, and whose every live return's value (for a returned
|
|
631
|
+
// call or construction, its first argument) depends on that const, directly or through consts derived
|
|
632
|
+
// from it. Returns { action, callee } or null.
|
|
633
|
+
export function entryDelegatedAction(stationText, stationFile, entry) {
|
|
634
|
+
if (!stationText || !stationFile) return null;
|
|
635
|
+
const ctx = analyze(stationFile, stationText);
|
|
636
|
+
const functions = exportedFunctions(ctx);
|
|
637
|
+
const fn = functions.get(entry);
|
|
638
|
+
if (!fn || !ts.isBlock(fn.body)) return null;
|
|
639
|
+
let delegation = null;
|
|
640
|
+
walk(fn.body, (node) => {
|
|
641
|
+
if (delegation || !ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name) || !node.initializer) return;
|
|
642
|
+
if (!(ts.isVariableDeclarationList(node.parent) && node.parent.flags & ts.NodeFlags.Const)) return;
|
|
643
|
+
const call = unwrap(node.initializer);
|
|
644
|
+
if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression) || !ts.isStringLiteralLike(call.arguments[0] ?? {})) return;
|
|
645
|
+
const callee = declarationOf(ctx, call.expression);
|
|
646
|
+
const name = call.expression.text;
|
|
647
|
+
if (!callee || functions.get(name) !== (ts.isVariableDeclaration(callee) ? unwrap(callee.initializer) : callee) || name === entry) return;
|
|
648
|
+
if (dead(node, fn.body) || insideCatch(node, fn.body)) return;
|
|
649
|
+
delegation = { action: call.arguments[0].text, callee: name, symbol: symbolOf(ctx, node.name) };
|
|
650
|
+
}, false);
|
|
651
|
+
if (!delegation) return null;
|
|
652
|
+
const derived = new Set([delegation.symbol]);
|
|
653
|
+
const dependsOn = (expression) => {
|
|
654
|
+
let found = false;
|
|
655
|
+
walk(expression, (child) => {
|
|
656
|
+
if (found) return false;
|
|
657
|
+
if (!ts.isIdentifier(child)) return;
|
|
658
|
+
const symbol = symbolOf(ctx, child);
|
|
659
|
+
if (derived.has(symbol)) { found = true; return false; }
|
|
660
|
+
const initializer = constInitializer(ctx, child);
|
|
661
|
+
if (initializer && initializer !== expression && dependsOn(initializer)) { derived.add(symbol); found = true; return false; }
|
|
662
|
+
});
|
|
663
|
+
return found;
|
|
664
|
+
};
|
|
665
|
+
// A returned call or construction (e.g. `Response.json(body, init)`) must take its body, the first
|
|
666
|
+
// argument, from the execution; status or headers derived from it alone are not the result.
|
|
667
|
+
const valueDepends = (expression) => {
|
|
668
|
+
const node = unwrap(expression);
|
|
669
|
+
if (ts.isConditionalExpression(node)) return valueDepends(node.whenTrue) && valueDepends(node.whenFalse);
|
|
670
|
+
if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && node.arguments?.length) return dependsOn(node.arguments[0]);
|
|
671
|
+
return dependsOn(node);
|
|
672
|
+
};
|
|
673
|
+
const returns = liveReturns(fn);
|
|
674
|
+
return returns?.length && returns.every((expression) => expression && valueDepends(expression))
|
|
675
|
+
? { action: delegation.action, callee: delegation.callee }
|
|
676
|
+
: null;
|
|
677
|
+
}
|