@vantageos/backend-doctor 0.1.13 → 0.2.0

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.
Files changed (58) hide show
  1. package/README.md +19 -2
  2. package/dist/args.d.ts +16 -0
  3. package/dist/args.js +54 -0
  4. package/dist/build-stamp.d.ts +13 -0
  5. package/dist/build-stamp.js +76 -0
  6. package/dist/build-stamp.json +4 -0
  7. package/dist/caller-resolution.d.ts +222 -0
  8. package/dist/caller-resolution.js +1195 -0
  9. package/dist/cli.js +217 -10
  10. package/dist/coverage-cli.js +8 -0
  11. package/dist/crud-coverage.d.ts +81 -0
  12. package/dist/crud-coverage.js +375 -0
  13. package/dist/detectors/oracle-axis1.d.ts +5 -3
  14. package/dist/detectors/oracle-axis1.js +24 -15
  15. package/dist/detectors/oracle-axis2.js +5 -6
  16. package/dist/detectors/predicates.d.ts +3 -0
  17. package/dist/detectors/predicates.js +81 -16
  18. package/dist/detectors/shared.js +5 -5
  19. package/dist/detectors/source/_scan.js +6 -1
  20. package/dist/detectors/source/backfill.js +9 -1
  21. package/dist/detectors/source/guard-helper-flow.d.ts +112 -0
  22. package/dist/detectors/source/guard-helper-flow.js +1173 -0
  23. package/dist/detectors/source/identity.d.ts +51 -2
  24. package/dist/detectors/source/identity.js +815 -14
  25. package/dist/detectors/source/isolation.d.ts +11 -1
  26. package/dist/detectors/source/isolation.js +106 -25
  27. package/dist/detectors/source/jobs.js +165 -2
  28. package/dist/detectors/source/readbudget.d.ts +1 -0
  29. package/dist/detectors/source/readbudget.js +67 -4
  30. package/dist/detectors/source/security.d.ts +16 -0
  31. package/dist/detectors/source/security.js +225 -7
  32. package/dist/detectors/source/write-isolation.d.ts +8 -1
  33. package/dist/detectors/source/write-isolation.js +64 -30
  34. package/dist/detectors/standard/r46-returns-validator.d.ts +8 -0
  35. package/dist/detectors/standard/r46-returns-validator.js +45 -14
  36. package/dist/detectors/structural.js +14 -10
  37. package/dist/oracle.d.ts +92 -3
  38. package/dist/oracle.js +229 -12
  39. package/dist/registry.d.ts +7 -0
  40. package/dist/registry.js +48 -6
  41. package/dist/report.d.ts +11 -1
  42. package/dist/report.js +15 -2
  43. package/dist/state-rank.d.ts +5 -0
  44. package/dist/state-rank.js +9 -0
  45. package/dist/tally.d.ts +35 -0
  46. package/dist/tally.js +76 -8
  47. package/dist/target.d.ts +32 -0
  48. package/dist/target.js +173 -0
  49. package/fixtures/cr1-classes/_generated/api.d.ts +24 -0
  50. package/fixtures/cr1-classes/surface.ts +206 -0
  51. package/fixtures/cr1-empty/nothing.ts +1 -0
  52. package/fixtures/cr1-refusal-on-resolved/_generated/api.d.ts +24 -0
  53. package/fixtures/cr1-refusal-on-resolved/doors.ts +180 -0
  54. package/fixtures/cr1-refusal-on-resolved/lib/auth.ts +104 -0
  55. package/fixtures/r34-vp-auth-30e821b/auth-805-812.ts.txt +8 -0
  56. package/fixtures/r34-vp-auth-30e821b/auth-878-890.ts.txt +13 -0
  57. package/fixtures/vp-writer-tier-1e4ab36.csv +55 -0
  58. package/package.json +2 -2
@@ -0,0 +1,1173 @@
1
+ // R-6 guard helpers judged on CONTROL FLOW, never on the text of their body.
2
+ //
3
+ // A public mutation often enforces its boundary in one line that calls a helper
4
+ // defined elsewhere (`await requireOrgAdmin(ctx, args.orgSlug)`). Crediting such
5
+ // a helper by the presence of an identity read and a `throw` in its text is
6
+ // wrong both ways (Eta, backend-doctor #36 @ a43e1bf):
7
+ // - a body wrapped in `try { … } catch {}` (R6T) or moved into a closure that is
8
+ // never called (R6N) still CONTAINS the read and the throw, yet refuses no
9
+ // one;
10
+ // - a `return` inside a `.filter((x) => { return … })` callback (R6Q) is not a
11
+ // return from the helper, yet a token scan reads it as one.
12
+ //
13
+ // So the helper's body is parsed with the TypeScript compiler API and walked as
14
+ // structured control flow. Each path carries what it has PASSED:
15
+ // - `guarded` — a branch whose condition depends on the verified identity
16
+ // (a value derived from `ctx.auth.getUserIdentity()` / `withOrgScope(ctx)`)
17
+ // and whose other arm always throws (a refusal for the absent/mismatch case);
18
+ // - `bound` — such a guard whose condition also reads one of the helper's own
19
+ // non-context parameters (the target the helper binds the identity to);
20
+ // - `master` — such a guard whose condition tests `isMaster` on the identity.
21
+ // A path ends at a `return` at the helper's OWN level, at the fall-through end of
22
+ // the body, or at a `throw`. A `throw` inside a `try` goes to its `catch`, which
23
+ // is entered from the state at the `try`'s entry (any statement of the try block
24
+ // may raise before the guard). Nested functions and arrows are never executed:
25
+ // their `return` and `throw` belong to them, and their reads read nothing here.
26
+ //
27
+ // The helper is HONOURED only when every path reaching a normal exit has passed
28
+ // a guard (plus `bound` for a binds-arg helper, `master` for a master-only one).
29
+ // Any construct the walk does not model (switch, labelled statements, a loop
30
+ // carrying return/throw/break/continue, a `finally`, an expression-bodied
31
+ // arrow) makes the verdict COULD-NOT-JUDGE — never honoured.
32
+ import * as ts from "typescript";
33
+ class Undecidable extends Error {
34
+ }
35
+ const isFunctionLike = (n) => ts.isFunctionDeclaration(n) ||
36
+ ts.isFunctionExpression(n) ||
37
+ ts.isArrowFunction(n) ||
38
+ ts.isMethodDeclaration(n) ||
39
+ ts.isGetAccessor(n) ||
40
+ ts.isSetAccessor(n) ||
41
+ ts.isConstructorDeclaration(n) ||
42
+ ts.isClassLike(n);
43
+ /** Visit `n` and its descendants, never entering a nested function or class. */
44
+ function walkOwn(n, visit) {
45
+ if (isFunctionLike(n))
46
+ return;
47
+ visit(n);
48
+ n.forEachChild((c) => walkOwn(c, visit));
49
+ }
50
+ /** A call that reads the verified identity, at this function's own level. */
51
+ function isIdentityRead(n) {
52
+ if (!ts.isCallExpression(n))
53
+ return false;
54
+ const callee = n.expression;
55
+ if (ts.isPropertyAccessExpression(callee))
56
+ return callee.name.text === "getUserIdentity";
57
+ return ts.isIdentifier(callee) && callee.text === "withOrgScope";
58
+ }
59
+ /** Does `expr` depend on the verified identity (a tainted local, or a read)? */
60
+ function dependsOnIdentity(expr, tainted) {
61
+ let hit = false;
62
+ walkOwn(expr, (x) => {
63
+ if (hit)
64
+ return;
65
+ if (isIdentityRead(x))
66
+ hit = true;
67
+ else if (ts.isIdentifier(x) && tainted.has(x.text)) {
68
+ const p = x.parent;
69
+ // `foo.identity` names a property, it does not read a local.
70
+ if (!(p && ts.isPropertyAccessExpression(p) && p.name === x))
71
+ hit = true;
72
+ }
73
+ });
74
+ return hit;
75
+ }
76
+ function readsAny(expr, names) {
77
+ let hit = false;
78
+ walkOwn(expr, (x) => {
79
+ if (hit || !ts.isIdentifier(x) || !names.has(x.text))
80
+ return;
81
+ const p = x.parent;
82
+ if (!(p && ts.isPropertyAccessExpression(p) && p.name === x))
83
+ hit = true;
84
+ });
85
+ return hit;
86
+ }
87
+ function testsIsMaster(expr, tainted) {
88
+ let hit = false;
89
+ walkOwn(expr, (x) => {
90
+ if (hit)
91
+ return;
92
+ if (ts.isPropertyAccessExpression(x) &&
93
+ x.name.text === "isMaster" &&
94
+ dependsOnIdentity(x.expression, tainted))
95
+ hit = true;
96
+ else if (ts.isIdentifier(x) &&
97
+ x.text === "isMaster" &&
98
+ tainted.has("isMaster"))
99
+ hit = true;
100
+ });
101
+ return hit;
102
+ }
103
+ function bindingNames(b, out) {
104
+ if (ts.isIdentifier(b))
105
+ out.push(b.text);
106
+ else
107
+ for (const el of b.elements)
108
+ if (!ts.isOmittedExpression(el))
109
+ bindingNames(el.name, out);
110
+ return out;
111
+ }
112
+ function merge(a, b) {
113
+ if (!a)
114
+ return b;
115
+ if (!b)
116
+ return a;
117
+ return {
118
+ tainted: new Set([...a.tainted].filter((t) => b.tainted.has(t))),
119
+ guarded: a.guarded && b.guarded,
120
+ bound: a.bound && b.bound,
121
+ master: a.master && b.master,
122
+ absent: a.absent && b.absent,
123
+ };
124
+ }
125
+ /** Statements that can transfer control out of a loop or switch body. */
126
+ function hasAbrupt(n) {
127
+ let hit = false;
128
+ walkOwn(n, (x) => {
129
+ if (ts.isReturnStatement(x) ||
130
+ ts.isThrowStatement(x) ||
131
+ ts.isBreakStatement(x) ||
132
+ ts.isContinueStatement(x))
133
+ hit = true;
134
+ });
135
+ return hit;
136
+ }
137
+ /** Truth of `e` when it reads only the call-site constants (`opts?.masterOnly`
138
+ * where the call passed `{ masterOnly: true }`); `undefined` when it does not. */
139
+ function constTruth(e, consts) {
140
+ if (ts.isParenthesizedExpression(e))
141
+ return constTruth(e.expression, consts);
142
+ if (e.kind === ts.SyntaxKind.TrueKeyword)
143
+ return true;
144
+ if (e.kind === ts.SyntaxKind.FalseKeyword)
145
+ return false;
146
+ if (ts.isPrefixUnaryExpression(e) &&
147
+ e.operator === ts.SyntaxKind.ExclamationToken) {
148
+ const v = constTruth(e.operand, consts);
149
+ return v === undefined ? undefined : !v;
150
+ }
151
+ if (ts.isBinaryExpression(e)) {
152
+ const op = e.operatorToken.kind;
153
+ if (op !== ts.SyntaxKind.AmpersandAmpersandToken &&
154
+ op !== ts.SyntaxKind.BarBarToken)
155
+ return undefined;
156
+ const a = constTruth(e.left, consts);
157
+ const b = constTruth(e.right, consts);
158
+ if (op === ts.SyntaxKind.AmpersandAmpersandToken) {
159
+ if (a === false || b === false)
160
+ return false;
161
+ return a === true && b === true ? true : undefined;
162
+ }
163
+ if (a === true || b === true)
164
+ return true;
165
+ return a === false && b === false ? false : undefined;
166
+ }
167
+ if (ts.isIdentifier(e) && consts.has(e.text))
168
+ return consts.get(e.text) !== null;
169
+ if (ts.isPropertyAccessExpression(e) &&
170
+ ts.isIdentifier(e.expression) &&
171
+ consts.has(e.expression.text)) {
172
+ const obj = consts.get(e.expression.text);
173
+ if (obj === null)
174
+ return false; // the argument was not passed
175
+ if (!obj)
176
+ return undefined;
177
+ let found;
178
+ for (const p of obj.properties) {
179
+ // A spread, a shorthand or a computed key: the literal is not readable.
180
+ if (!ts.isPropertyAssignment(p))
181
+ return undefined;
182
+ if (!ts.isIdentifier(p.name) && !ts.isStringLiteral(p.name))
183
+ return undefined;
184
+ if (p.name.text === e.name.text)
185
+ found = p.initializer;
186
+ }
187
+ if (!found)
188
+ return false; // absent key reads undefined
189
+ if (found.kind === ts.SyntaxKind.TrueKeyword)
190
+ return true;
191
+ if (found.kind === ts.SyntaxKind.FalseKeyword)
192
+ return false;
193
+ return undefined;
194
+ }
195
+ return undefined;
196
+ }
197
+ function awaitedCall(e) {
198
+ let x = e;
199
+ while (ts.isParenthesizedExpression(x))
200
+ x = x.expression;
201
+ return ts.isAwaitExpression(x);
202
+ }
203
+ /** `async`, or a declared `Promise<…>` return: its refusal is a rejection. */
204
+ function returnsPromise(fn) {
205
+ const isAsync = (ts.getModifiers(fn) ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword);
206
+ return isAsync || (fn.type?.getText().startsWith("Promise") ?? false);
207
+ }
208
+ class Walker {
209
+ params;
210
+ v;
211
+ constructor(params, v) {
212
+ this.params = params;
213
+ this.v = v;
214
+ }
215
+ /** The state entering the TRUE arm of `if (cond)`, for a validator: a
216
+ * positive `isMaster` test on the pure identity (the fleet carve-out) is a
217
+ * master path; a condition that is truthy only when the target parameter is
218
+ * owned by the identity (posBinds) is a bound path. */
219
+ positiveArm(cond, s) {
220
+ const v = this.v;
221
+ if (!v)
222
+ return s;
223
+ if (positiveMasterTest(cond, v.pure))
224
+ return { ...s, guarded: true, master: true };
225
+ if (this.posBinds(cond, s.tainted))
226
+ return { ...s, guarded: true, bound: true };
227
+ return s;
228
+ }
229
+ /** STRONG binding, for a name-free validator only: `cond` can be truthy only
230
+ * when the target parameter equals / lies under an identity-derived value,
231
+ * or a predicate judged on its body says the identity owns it. (#37's
232
+ * allowlisted helper walk credits any identity condition that merely READS
233
+ * the parameter; a function the doctor did not name earns no such latitude.) */
234
+ posBinds(cond, tainted) {
235
+ const v = this.v;
236
+ if (!v)
237
+ return false;
238
+ return (this.bindingPredicate(cond, v) ||
239
+ bindsByEquality(cond, tainted, this.params));
240
+ }
241
+ /** `cond` FALSE implies the target is bound: `!P(…)`, `x !== own`, or a
242
+ * disjunction any one of whose negations binds. */
243
+ negBinds(cond, tainted) {
244
+ const c = unwrap(cond);
245
+ if (ts.isPrefixUnaryExpression(c) &&
246
+ c.operator === ts.SyntaxKind.ExclamationToken)
247
+ return this.posBinds(c.operand, tainted);
248
+ if (!ts.isBinaryExpression(c))
249
+ return false;
250
+ const op = c.operatorToken.kind;
251
+ if (op === ts.SyntaxKind.BarBarToken)
252
+ return this.negBinds(c.left, tainted) || this.negBinds(c.right, tainted);
253
+ if (op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
254
+ op === ts.SyntaxKind.ExclamationEqualsToken) {
255
+ const side = (a, b) => readsAny(a, this.params) &&
256
+ !dependsOnIdentity(a, tainted) &&
257
+ dependsOnIdentity(b, tainted) &&
258
+ !readsAny(b, this.params);
259
+ return side(c.left, c.right) || side(c.right, c.left);
260
+ }
261
+ return false;
262
+ }
263
+ /** `cond` FALSE implies the caller is the fleet master: `!scope.isMaster`, or
264
+ * a disjunction carrying it. */
265
+ negMaster(cond, pure) {
266
+ const c = unwrap(cond);
267
+ if (ts.isPrefixUnaryExpression(c) &&
268
+ c.operator === ts.SyntaxKind.ExclamationToken)
269
+ return positiveMasterTest(c.operand, pure);
270
+ if (ts.isBinaryExpression(c) &&
271
+ c.operatorToken.kind === ts.SyntaxKind.BarBarToken)
272
+ return this.negMaster(c.left, pure) || this.negMaster(c.right, pure);
273
+ return false;
274
+ }
275
+ bindingPredicate(cond, v) {
276
+ const c = unwrap(cond);
277
+ if (ts.isBinaryExpression(c) &&
278
+ c.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
279
+ return (this.bindingPredicate(c.left, v) || this.bindingPredicate(c.right, v));
280
+ if (!ts.isCallExpression(c) || !ts.isIdentifier(c.expression))
281
+ return false;
282
+ const args = c.arguments.map(unwrap);
283
+ const i = args.findIndex((a) => ts.isIdentifier(a) && v.pure.has(a.text));
284
+ const j = args.findIndex((a) => ts.isIdentifier(a) && this.params.has(a.text));
285
+ if (i < 0 || j < 0)
286
+ return false;
287
+ const d = v.resolve(c.expression.text);
288
+ if (!d)
289
+ return false;
290
+ // An un-awaited async predicate is a Promise: always truthy.
291
+ if (!awaitedCall(cond) && returnsPromise(d.fn))
292
+ return false;
293
+ return judgePredicateFunction(d.fn, i, j).verdict === "honoured";
294
+ }
295
+ /** A statement `[await] Q(…)` whose callee the tree defines: Q, walked as a
296
+ * validator with the identity and target this call hands it, either binds
297
+ * the target on every normal exit, admits the fleet master only, or never
298
+ * returns. `null` = not such a call (the statement is read as before). */
299
+ callStatement(e, s) {
300
+ const v = this.v;
301
+ if (!v)
302
+ return null;
303
+ const c = unwrap(e);
304
+ if (!ts.isCallExpression(c) || !ts.isIdentifier(c.expression))
305
+ return null;
306
+ const d = v.resolve(c.expression.text);
307
+ if (!d)
308
+ return null;
309
+ // An async validator that is not awaited refuses in a promise nobody
310
+ // waits on: the statement after it runs either way.
311
+ if (!awaitedCall(e) && returnsPromise(d.fn))
312
+ return null;
313
+ const args = c.arguments.map(unwrap);
314
+ const identityIdx = args.findIndex((a) => ts.isIdentifier(a) && v.pure.has(a.text));
315
+ const k = args.findIndex((a) => ts.isIdentifier(a) && this.params.has(a.text));
316
+ const sum = (boundIdx) => summarizeValidator(d.fn, {
317
+ boundIdx,
318
+ identityIdx: identityIdx < 0 ? undefined : identityIdx,
319
+ callArgs: args,
320
+ resolve: d.resolve,
321
+ key: d.key,
322
+ depth: v.depth + 1,
323
+ });
324
+ const master = sum(undefined);
325
+ if (master.ok && master.never)
326
+ return { normal: null, returns: [], throws: 1 };
327
+ let out = s;
328
+ if (master.ok && master.exitsMaster)
329
+ out = { ...out, guarded: true, master: true };
330
+ if (k >= 0) {
331
+ const b = sum(k);
332
+ if (b.ok && b.exitsBound)
333
+ out = { ...out, guarded: true, bound: true };
334
+ }
335
+ return out === s ? null : { normal: out, returns: [], throws: 0 };
336
+ }
337
+ /** Taint every local assigned from an identity-dependent expression. */
338
+ assign(s, names, rhs) {
339
+ if (!dependsOnIdentity(rhs, s.tainted)) {
340
+ // Re-assigned from something else: no longer the identity.
341
+ if (!names.some((n) => s.tainted.has(n)))
342
+ return s;
343
+ const t = new Set(s.tainted);
344
+ for (const n of names)
345
+ t.delete(n);
346
+ return { ...s, tainted: t };
347
+ }
348
+ return { ...s, tainted: new Set([...s.tainted, ...names]) };
349
+ }
350
+ expression(e, s) {
351
+ let out = s;
352
+ walkOwn(e, (x) => {
353
+ if (ts.isBinaryExpression(x) &&
354
+ x.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
355
+ ts.isIdentifier(x.left))
356
+ out = this.assign(out, [x.left.text], x.right);
357
+ });
358
+ return out;
359
+ }
360
+ list(stmts, s) {
361
+ let normal = s;
362
+ const returns = [];
363
+ let throws = 0;
364
+ for (const st of stmts) {
365
+ if (!normal)
366
+ break; // unreachable code adds no path
367
+ const f = this.stmt(st, normal);
368
+ returns.push(...f.returns);
369
+ throws += f.throws;
370
+ normal = f.normal;
371
+ }
372
+ return { normal, returns, throws };
373
+ }
374
+ stmt(st, s) {
375
+ const none = { returns: [], throws: 0 };
376
+ if (ts.isBlock(st))
377
+ return this.list(st.statements, s);
378
+ if (ts.isVariableStatement(st)) {
379
+ let out = s;
380
+ for (const d of st.declarationList.declarations)
381
+ if (d.initializer)
382
+ out = this.assign(this.expression(d.initializer, out), bindingNames(d.name, []), d.initializer);
383
+ return { normal: out, ...none };
384
+ }
385
+ if (ts.isExpressionStatement(st)) {
386
+ const after = this.expression(st.expression, s);
387
+ return (this.callStatement(st.expression, after) ?? { normal: after, ...none });
388
+ }
389
+ if (ts.isReturnStatement(st))
390
+ return { normal: null, returns: [{ state: s, node: st }], throws: 0 };
391
+ if (ts.isThrowStatement(st))
392
+ return { normal: null, returns: [], throws: 1 };
393
+ if (ts.isIfStatement(st)) {
394
+ const cs = this.expression(st.expression, s);
395
+ if (this.v) {
396
+ // A condition fixed by the call site takes one arm only.
397
+ const k = constTruth(st.expression, this.v.consts);
398
+ if (k === true)
399
+ return this.stmt(st.thenStatement, cs);
400
+ if (k === false)
401
+ return st.elseStatement
402
+ ? this.stmt(st.elseStatement, cs)
403
+ : { normal: cs, ...none };
404
+ }
405
+ const a = this.stmt(st.thenStatement, this.positiveArm(st.expression, cs));
406
+ const b = st.elseStatement
407
+ ? this.stmt(st.elseStatement, cs)
408
+ : { normal: cs, returns: [], throws: 0 };
409
+ const alwaysThrows = (f) => f.normal === null && f.returns.length === 0 && f.throws > 0;
410
+ const v = this.v;
411
+ if (v && dependsOnIdentity(st.expression, cs.tainted)) {
412
+ // Validator walk: what the continuing arm KNOWS, by polarity.
413
+ const cond = st.expression;
414
+ const strong = (p, holds) => p && {
415
+ ...p,
416
+ guarded: true,
417
+ bound: p.bound ||
418
+ (holds
419
+ ? this.posBinds(cond, cs.tainted)
420
+ : this.negBinds(cond, cs.tainted)),
421
+ master: p.master ||
422
+ (holds
423
+ ? positiveMasterTest(cond, v.pure)
424
+ : this.negMaster(cond, v.pure)),
425
+ };
426
+ if (alwaysThrows(a))
427
+ b.normal = strong(b.normal, false);
428
+ else if (alwaysThrows(b))
429
+ a.normal = strong(a.normal, true);
430
+ }
431
+ else if (dependsOnIdentity(st.expression, cs.tainted)) {
432
+ const guard = (p) => p && {
433
+ ...p,
434
+ guarded: true,
435
+ bound: p.bound ||
436
+ (this.params.size > 0 && readsAny(st.expression, this.params)),
437
+ master: p.master || testsIsMaster(st.expression, cs.tainted),
438
+ absent: p.absent || !readsAny(st.expression, this.params),
439
+ };
440
+ // the arm that continues past a branch whose other arm always refuses
441
+ if (alwaysThrows(a))
442
+ b.normal = guard(b.normal);
443
+ else if (alwaysThrows(b))
444
+ a.normal = guard(a.normal);
445
+ }
446
+ return {
447
+ normal: merge(a.normal, b.normal),
448
+ returns: [...a.returns, ...b.returns],
449
+ throws: a.throws + b.throws,
450
+ };
451
+ }
452
+ if (ts.isTryStatement(st)) {
453
+ if (st.finallyBlock)
454
+ throw new Undecidable("a `finally` block (not modelled)");
455
+ const body = this.list(st.tryBlock.statements, s);
456
+ if (!st.catchClause)
457
+ return body;
458
+ // Any statement of the try block may raise before its guard ran, so the
459
+ // catch is entered from the state at the try's entry.
460
+ let cs = s;
461
+ const v = st.catchClause.variableDeclaration;
462
+ if (v) {
463
+ const t = new Set(cs.tainted);
464
+ for (const n of bindingNames(v.name, []))
465
+ t.delete(n);
466
+ cs = { ...cs, tainted: t };
467
+ }
468
+ const c = this.list(st.catchClause.block.statements, cs);
469
+ return {
470
+ normal: merge(body.normal, c.normal),
471
+ returns: [...body.returns, ...c.returns],
472
+ throws: c.throws,
473
+ };
474
+ }
475
+ if (ts.isForStatement(st) ||
476
+ ts.isForOfStatement(st) ||
477
+ ts.isForInStatement(st) ||
478
+ ts.isWhileStatement(st) ||
479
+ ts.isDoStatement(st)) {
480
+ if (hasAbrupt(st))
481
+ throw new Undecidable("a loop carrying return/throw/break/continue");
482
+ // Runs zero or more times: nothing it does can be counted on.
483
+ const once = this.stmt(st.statement, s);
484
+ return { normal: merge(s, once.normal), ...none };
485
+ }
486
+ if (ts.isEmptyStatement(st) ||
487
+ ts.isFunctionDeclaration(st) ||
488
+ ts.isClassDeclaration(st) ||
489
+ ts.isTypeAliasDeclaration(st) ||
490
+ ts.isInterfaceDeclaration(st))
491
+ return { normal: s, ...none };
492
+ throw new Undecidable(`a ${ts.SyntaxKind[st.kind]} (not modelled)`);
493
+ }
494
+ }
495
+ function lineOf(n) {
496
+ const sf = n.getSourceFile();
497
+ return sf.getLineAndCharacterOfPosition(n.getStart(sf)).line + 1;
498
+ }
499
+ /** Judge one helper function on its control flow. */
500
+ export function judgeHelperFunction(fn, kind) {
501
+ if (!fn.body || !ts.isBlock(fn.body))
502
+ return {
503
+ verdict: "could-not-judge",
504
+ why: "an expression-bodied helper (not modelled)",
505
+ };
506
+ // The first parameter is the Convex context; the rest are what the helper
507
+ // binds the identity to.
508
+ const params = new Set();
509
+ for (const p of fn.parameters.slice(1))
510
+ for (const n of bindingNames(p.name, []))
511
+ params.add(n);
512
+ const start = {
513
+ tainted: new Set(),
514
+ guarded: false,
515
+ bound: false,
516
+ master: false,
517
+ absent: false,
518
+ };
519
+ let flow;
520
+ try {
521
+ flow = new Walker(params).list(fn.body.statements, start);
522
+ }
523
+ catch (e) {
524
+ if (e instanceof Undecidable)
525
+ return { verdict: "could-not-judge", why: `its body has ${e.message}` };
526
+ throw e;
527
+ }
528
+ const exits = flow.returns.map((r) => ({
529
+ state: r.state,
530
+ where: `the \`return\` at line ${lineOf(r.node)}`,
531
+ }));
532
+ if (flow.normal)
533
+ exits.push({ state: flow.normal, where: "the end of its body" });
534
+ if (exits.length === 0)
535
+ return {
536
+ verdict: "refused",
537
+ why: "it never completes normally (it refuses every caller), so it guards nothing",
538
+ };
539
+ for (const x of exits) {
540
+ if (!x.state.guarded)
541
+ return {
542
+ verdict: "refused",
543
+ why: `a path reaches ${x.where} without passing a refusal conditioned on the verified identity`,
544
+ };
545
+ if (kind === "binds-arg" && !x.state.bound)
546
+ return {
547
+ verdict: "refused",
548
+ why: `a path reaches ${x.where} without a refusal comparing the identity with the helper's target parameter`,
549
+ };
550
+ if (kind === "master-only" && !x.state.master)
551
+ return {
552
+ verdict: "refused",
553
+ why: `a path reaches ${x.where} without a refusal testing isMaster on the verified identity`,
554
+ };
555
+ }
556
+ return { verdict: "honoured" };
557
+ }
558
+ /** Every definition, in `source`, of a helper named in `kinds`. */
559
+ export function findHelperDefinitions(fileName, source, kinds) {
560
+ const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
561
+ const out = [];
562
+ const exportedMod = (n) => ts.canHaveModifiers(n) &&
563
+ (ts.getModifiers(n) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
564
+ for (const st of sf.statements) {
565
+ if (ts.isFunctionDeclaration(st) && st.name && st.name.text in kinds) {
566
+ out.push({
567
+ name: st.name.text,
568
+ line: lineOf(st),
569
+ exported: exportedMod(st),
570
+ judgement: judgeHelperFunction(st, kinds[st.name.text]),
571
+ });
572
+ }
573
+ else if (ts.isVariableStatement(st)) {
574
+ for (const d of st.declarationList.declarations) {
575
+ if (!ts.isIdentifier(d.name) || !(d.name.text in kinds))
576
+ continue;
577
+ const init = d.initializer;
578
+ const judgement = init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))
579
+ ? judgeHelperFunction(init, kinds[d.name.text])
580
+ : {
581
+ verdict: "could-not-judge",
582
+ why: "its definition is not a function literal",
583
+ };
584
+ out.push({
585
+ name: d.name.text,
586
+ line: lineOf(d),
587
+ exported: exportedMod(st),
588
+ judgement,
589
+ });
590
+ }
591
+ }
592
+ }
593
+ return out;
594
+ }
595
+ /** The arguments of each `await <helper>(…)` that is a DIRECT statement of the
596
+ * handler `name` (`export const name = mutation({ handler: … })`) and starts
597
+ * before `beforeLine`. A helper call nested in a branch, a try, or a closure of
598
+ * the handler is not a statement every path runs, so it is not returned.
599
+ * `null` = the handler's body could not be located in the file. */
600
+ export function topLevelHelperCalls(fileName, source, handlerName,
601
+ /** `null` = any identifier callee (judged by the caller on its body). */
602
+ helpers, beforeLine) {
603
+ const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
604
+ let body;
605
+ for (const st of sf.statements) {
606
+ if (!ts.isVariableStatement(st))
607
+ continue;
608
+ for (const d of st.declarationList.declarations) {
609
+ if (!ts.isIdentifier(d.name) || d.name.text !== handlerName)
610
+ continue;
611
+ const init = d.initializer;
612
+ if (!init || !ts.isCallExpression(init))
613
+ continue;
614
+ const cfg = init.arguments[0];
615
+ if (!cfg || !ts.isObjectLiteralExpression(cfg))
616
+ continue;
617
+ for (const p of cfg.properties) {
618
+ if (!p.name || !ts.isIdentifier(p.name) || p.name.text !== "handler")
619
+ continue;
620
+ const fn = ts.isPropertyAssignment(p) ? p.initializer : p;
621
+ if ((ts.isArrowFunction(fn) ||
622
+ ts.isFunctionExpression(fn) ||
623
+ ts.isMethodDeclaration(fn)) &&
624
+ fn.body &&
625
+ ts.isBlock(fn.body))
626
+ body = fn.body;
627
+ }
628
+ }
629
+ }
630
+ if (!body)
631
+ return null;
632
+ const out = [];
633
+ for (const st of body.statements) {
634
+ if (lineOf(st) >= beforeLine)
635
+ break;
636
+ let e;
637
+ if (ts.isExpressionStatement(st))
638
+ e = st.expression;
639
+ else if (ts.isVariableStatement(st))
640
+ e = st.declarationList.declarations[0]?.initializer;
641
+ if (!e || !ts.isAwaitExpression(e))
642
+ continue;
643
+ const call = e.expression;
644
+ if (ts.isCallExpression(call) &&
645
+ ts.isIdentifier(call.expression) &&
646
+ (helpers === null || helpers.has(call.expression.text)))
647
+ out.push({
648
+ name: call.expression.text,
649
+ args: call.arguments.map((a) => a.getText(sf)).join(", "),
650
+ argNodes: call.arguments,
651
+ line: lineOf(st),
652
+ });
653
+ }
654
+ return out;
655
+ }
656
+ const parse = (fileName, source) => ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
657
+ /** Every top-level function declaration and `const f = (…) => {…}` /
658
+ * `function (…) {…}` of a file. */
659
+ export function topLevelFunctions(fileName, source) {
660
+ const sf = parse(fileName, source);
661
+ const out = [];
662
+ const exportedMod = (n) => ts.canHaveModifiers(n) &&
663
+ (ts.getModifiers(n) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
664
+ for (const st of sf.statements) {
665
+ if (ts.isFunctionDeclaration(st) && st.name && st.body)
666
+ out.push({
667
+ name: st.name.text,
668
+ line: lineOf(st),
669
+ exported: exportedMod(st),
670
+ fn: st,
671
+ });
672
+ else if (ts.isVariableStatement(st))
673
+ for (const d of st.declarationList.declarations) {
674
+ const init = d.initializer;
675
+ if (ts.isIdentifier(d.name) &&
676
+ init &&
677
+ (ts.isArrowFunction(init) || ts.isFunctionExpression(init)))
678
+ out.push({
679
+ name: d.name.text,
680
+ line: lineOf(d),
681
+ exported: exportedMod(st),
682
+ fn: init,
683
+ });
684
+ }
685
+ }
686
+ return out;
687
+ }
688
+ /** Strip `await`, parentheses, `!` non-null and `as` casts. */
689
+ function unwrap(e) {
690
+ let x = e;
691
+ for (;;) {
692
+ if (ts.isAwaitExpression(x) || ts.isParenthesizedExpression(x))
693
+ x = x.expression;
694
+ else if (ts.isNonNullExpression(x) || ts.isAsExpression(x))
695
+ x = x.expression;
696
+ else
697
+ return x;
698
+ }
699
+ }
700
+ /** Locals of `fn`'s own body bound ONLY to the verified identity: a `const`/
701
+ * `let` initialised from an identity read (or from another such local) and
702
+ * never re-assigned. A value BUILT from the identity (`{ ...scope, orgSlug: x }`)
703
+ * is not the identity. */
704
+ function pureIdentityLocals(fn, seed = new Set()) {
705
+ const decls = new Map();
706
+ const reassigned = new Set();
707
+ if (!fn.body)
708
+ return new Set(seed);
709
+ walkOwn(fn.body, (x) => {
710
+ if (ts.isVariableDeclaration(x) && ts.isIdentifier(x.name) && x.initializer)
711
+ decls.set(x.name.text, x.initializer);
712
+ if (ts.isBinaryExpression(x) &&
713
+ x.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
714
+ x.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
715
+ ts.isIdentifier(x.left))
716
+ reassigned.add(x.left.text);
717
+ });
718
+ const pure = new Set([...seed].filter((n) => !reassigned.has(n)));
719
+ let grew = true;
720
+ while (grew) {
721
+ grew = false;
722
+ for (const [n, init] of decls) {
723
+ if (pure.has(n) || reassigned.has(n))
724
+ continue;
725
+ const u = unwrap(init);
726
+ if (isIdentityRead(u) || (ts.isIdentifier(u) && pure.has(u.text))) {
727
+ pure.add(n);
728
+ grew = true;
729
+ }
730
+ }
731
+ }
732
+ return pure;
733
+ }
734
+ /** Judge a function as an IDENTITY RESOLVER (shape 1 above). */
735
+ export function judgeResolverFunction(fn) {
736
+ if (!fn.body || !ts.isBlock(fn.body))
737
+ return {
738
+ verdict: "could-not-judge",
739
+ why: "an expression-bodied helper (not modelled)",
740
+ };
741
+ const params = new Set();
742
+ for (const p of fn.parameters.slice(1))
743
+ for (const n of bindingNames(p.name, []))
744
+ params.add(n);
745
+ let flow;
746
+ try {
747
+ flow = new Walker(params).list(fn.body.statements, {
748
+ tainted: new Set(),
749
+ guarded: false,
750
+ bound: false,
751
+ master: false,
752
+ absent: false,
753
+ });
754
+ }
755
+ catch (e) {
756
+ if (e instanceof Undecidable)
757
+ return { verdict: "could-not-judge", why: `its body has ${e.message}` };
758
+ throw e;
759
+ }
760
+ if (flow.normal)
761
+ return {
762
+ verdict: "refused",
763
+ why: "a path falls off the end of its body (returns undefined, not the identity)",
764
+ };
765
+ if (flow.returns.length === 0)
766
+ return { verdict: "refused", why: "it never returns a value" };
767
+ const pure = pureIdentityLocals(fn);
768
+ for (const r of flow.returns) {
769
+ const where = `the \`return\` at line ${lineOf(r.node)}`;
770
+ const e = ts.isReturnStatement(r.node) ? r.node.expression : undefined;
771
+ const u = e ? unwrap(e) : undefined;
772
+ if (!u || !(isIdentityRead(u) || (ts.isIdentifier(u) && pure.has(u.text))))
773
+ return {
774
+ verdict: "refused",
775
+ why: `${where} hands back a value that is not itself the verified identity/scope`,
776
+ };
777
+ if (!r.state.absent)
778
+ return {
779
+ verdict: "refused",
780
+ why: `a path reaches ${where} without refusing an absent identity (a guard on the identity alone)`,
781
+ };
782
+ }
783
+ return { verdict: "honoured" };
784
+ }
785
+ const MAX_VALIDATOR_DEPTH = 6;
786
+ const validatorMemo = new WeakMap();
787
+ export function summarizeValidator(fn, o) {
788
+ if (o.depth > MAX_VALIDATOR_DEPTH)
789
+ return { ok: false, why: "validator nesting deeper than modelled" };
790
+ if (!fn.body || !ts.isBlock(fn.body))
791
+ return { ok: false, why: "an expression-bodied function (not modelled)" };
792
+ const consts = new Map();
793
+ fn.parameters.forEach((p, i) => {
794
+ if (!ts.isIdentifier(p.name))
795
+ return;
796
+ if (i >= o.callArgs.length)
797
+ consts.set(p.name.text, null);
798
+ else {
799
+ const a = unwrap(o.callArgs[i]);
800
+ if (ts.isObjectLiteralExpression(a))
801
+ consts.set(p.name.text, a);
802
+ }
803
+ });
804
+ const sig = [
805
+ o.boundIdx,
806
+ o.identityIdx,
807
+ [...consts].map(([n, x]) => `${n}=${x ? x.getText() : "undefined"}`),
808
+ ].join("|");
809
+ let memo = validatorMemo.get(fn);
810
+ if (!memo) {
811
+ memo = new Map();
812
+ validatorMemo.set(fn, memo);
813
+ }
814
+ const hit = memo.get(sig);
815
+ if (hit)
816
+ return hit;
817
+ // A cycle reads as undecided while it is being judged.
818
+ memo.set(sig, { ok: false, why: "a recursive validator (not modelled)" });
819
+ const nameAt = (i) => {
820
+ if (i === undefined)
821
+ return undefined;
822
+ const p = fn.parameters[i];
823
+ return p && ts.isIdentifier(p.name) ? p.name.text : null;
824
+ };
825
+ const boundName = nameAt(o.boundIdx);
826
+ const identityName = nameAt(o.identityIdx);
827
+ let result;
828
+ if (boundName === null || identityName === null)
829
+ result = { ok: false, why: "a parameter that is not a plain identifier" };
830
+ else {
831
+ let reassigned = false;
832
+ walkOwn(fn.body, (x) => {
833
+ if (ts.isBinaryExpression(x) &&
834
+ x.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
835
+ x.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
836
+ ts.isIdentifier(x.left) &&
837
+ (x.left.text === boundName || x.left.text === identityName))
838
+ reassigned = true;
839
+ });
840
+ if (reassigned)
841
+ result = {
842
+ ok: false,
843
+ why: "it re-assigns its scope or target parameter (not modelled)",
844
+ };
845
+ else {
846
+ const seed = new Set(identityName ? [identityName] : []);
847
+ const pure = pureIdentityLocals(fn, seed);
848
+ const walker = new Walker(new Set(boundName ? [boundName] : []), {
849
+ consts,
850
+ pure,
851
+ resolve: o.resolve,
852
+ depth: o.depth,
853
+ });
854
+ try {
855
+ const flow = walker.list(fn.body.statements, {
856
+ tainted: seed,
857
+ guarded: false,
858
+ bound: false,
859
+ master: false,
860
+ absent: false,
861
+ });
862
+ const exits = flow.returns.map((r) => r.state);
863
+ if (flow.normal)
864
+ exits.push(flow.normal);
865
+ result = {
866
+ ok: true,
867
+ exitsBound: boundName !== undefined &&
868
+ exits.every((x) => x.master || (x.guarded && x.bound)),
869
+ exitsMaster: exits.every((x) => x.master),
870
+ never: exits.length === 0,
871
+ };
872
+ }
873
+ catch (e) {
874
+ if (!(e instanceof Undecidable))
875
+ throw e;
876
+ result = { ok: false, why: `its body has ${e.message}` };
877
+ }
878
+ }
879
+ }
880
+ memo.set(sig, result);
881
+ return result;
882
+ }
883
+ /** `cond` is a POSITIVE test of `isMaster` on the identity (alone or as a
884
+ * conjunct): the arm it guards runs for the fleet master only. */
885
+ function positiveMasterTest(cond, tainted) {
886
+ const c = unwrap(cond);
887
+ if (ts.isBinaryExpression(c) &&
888
+ c.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
889
+ return (positiveMasterTest(c.left, tainted) ||
890
+ positiveMasterTest(c.right, tainted));
891
+ if (ts.isBinaryExpression(c) &&
892
+ (c.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken ||
893
+ c.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken) &&
894
+ c.right.kind === ts.SyntaxKind.TrueKeyword)
895
+ return positiveMasterTest(c.left, tainted);
896
+ return (ts.isPropertyAccessExpression(c) &&
897
+ c.name.text === "isMaster" &&
898
+ dependsOnIdentity(c.expression, tainted));
899
+ }
900
+ /** `e` can only be truthy when the bound value EQUALS an identity-derived one,
901
+ * or lies UNDER an identity-derived prefix (`ns.startsWith(`${ownPrefix}/`)`).
902
+ * A conjunction binds when either conjunct does; a disjunction only when BOTH
903
+ * disjuncts do (either one alone admits the caller). */
904
+ function bindsByEquality(e, tainted, bound) {
905
+ const x = unwrap(e);
906
+ // The bound value is the receiver, the identity-derived value the argument:
907
+ // `<bound>.startsWith(<identity>)`. A fallback inside the prefix
908
+ // (`scope.orgSlug ?? ""`) can widen it to everything, so it binds nothing.
909
+ if (ts.isCallExpression(x) &&
910
+ ts.isPropertyAccessExpression(x.expression) &&
911
+ x.expression.name.text === "startsWith" &&
912
+ x.arguments.length === 1) {
913
+ const recv = x.expression.expression;
914
+ const arg = unwrap(x.arguments[0]);
915
+ const fallback = ts.isBinaryExpression(arg) &&
916
+ (arg.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ||
917
+ arg.operatorToken.kind === ts.SyntaxKind.BarBarToken);
918
+ return (!fallback &&
919
+ readsAny(recv, bound) &&
920
+ !dependsOnIdentity(recv, tainted) &&
921
+ dependsOnIdentity(arg, tainted) &&
922
+ !readsAny(arg, bound));
923
+ }
924
+ if (!ts.isBinaryExpression(x))
925
+ return false;
926
+ const op = x.operatorToken.kind;
927
+ if (op === ts.SyntaxKind.AmpersandAmpersandToken)
928
+ return (bindsByEquality(x.left, tainted, bound) ||
929
+ bindsByEquality(x.right, tainted, bound));
930
+ if (op === ts.SyntaxKind.BarBarToken)
931
+ return (bindsByEquality(x.left, tainted, bound) &&
932
+ bindsByEquality(x.right, tainted, bound));
933
+ if (op !== ts.SyntaxKind.EqualsEqualsEqualsToken &&
934
+ op !== ts.SyntaxKind.EqualsEqualsToken)
935
+ return false;
936
+ const side = (a, b) => readsAny(a, bound) &&
937
+ !dependsOnIdentity(a, tainted) &&
938
+ dependsOnIdentity(b, tainted) &&
939
+ !readsAny(b, bound);
940
+ return side(x.left, x.right) || side(x.right, x.left);
941
+ }
942
+ const isFalsyLiteral = (e) => {
943
+ if (!e)
944
+ return true;
945
+ const u = unwrap(e);
946
+ return (u.kind === ts.SyntaxKind.FalseKeyword ||
947
+ u.kind === ts.SyntaxKind.NullKeyword ||
948
+ (ts.isIdentifier(u) && u.text === "undefined"));
949
+ };
950
+ /** Judge a function as a REFUSING PREDICATE (shape 2 above): parameter
951
+ * `identityIdx` receives the caller's verified scope, `boundIdx` the value the
952
+ * caller then writes. */
953
+ export function judgePredicateFunction(fn, identityIdx, boundIdx) {
954
+ if (!fn.body || !ts.isBlock(fn.body))
955
+ return {
956
+ verdict: "could-not-judge",
957
+ why: "an expression-bodied predicate (not modelled)",
958
+ };
959
+ const ip = fn.parameters[identityIdx];
960
+ const bp = fn.parameters[boundIdx];
961
+ if (!ip || !bp || !ts.isIdentifier(ip.name) || !ts.isIdentifier(bp.name))
962
+ return {
963
+ verdict: "could-not-judge",
964
+ why: "its scope/value parameters are not plain identifiers",
965
+ };
966
+ const tainted = new Set([ip.name.text]);
967
+ const bound = new Set([bp.name.text]);
968
+ let reassigned = false;
969
+ walkOwn(fn.body, (x) => {
970
+ if (ts.isBinaryExpression(x) &&
971
+ x.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
972
+ x.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
973
+ ts.isIdentifier(x.left) &&
974
+ (tainted.has(x.left.text) || bound.has(x.left.text)))
975
+ reassigned = true;
976
+ });
977
+ if (reassigned)
978
+ return {
979
+ verdict: "could-not-judge",
980
+ why: "it re-assigns its scope or value parameter (not modelled)",
981
+ };
982
+ const judgeReturn = (e, cond, line) => {
983
+ if (isFalsyLiteral(e))
984
+ return null;
985
+ const u = unwrap(e);
986
+ if (u.kind === ts.SyntaxKind.TrueKeyword) {
987
+ if (cond && positiveMasterTest(cond, tainted))
988
+ return null;
989
+ return {
990
+ verdict: "refused",
991
+ why: `the \`return true\` at line ${line} is not conditioned on isMaster of the scope parameter — it admits a caller whose scope does not own the value`,
992
+ };
993
+ }
994
+ if (bindsByEquality(u, tainted, bound))
995
+ return null;
996
+ return {
997
+ verdict: "refused",
998
+ why: `the \`return\` at line ${line} can be truthy without the value equalling an identity-derived one`,
999
+ };
1000
+ };
1001
+ for (const st of fn.body.statements) {
1002
+ if (ts.isVariableStatement(st)) {
1003
+ for (const d of st.declarationList.declarations) {
1004
+ if (!d.initializer)
1005
+ continue;
1006
+ const names = bindingNames(d.name, []);
1007
+ if (dependsOnIdentity(d.initializer, tainted))
1008
+ for (const n of names)
1009
+ tainted.add(n);
1010
+ else if (ts.isIdentifier(unwrap(d.initializer)) &&
1011
+ bound.has(unwrap(d.initializer).text))
1012
+ for (const n of names)
1013
+ bound.add(n);
1014
+ }
1015
+ continue;
1016
+ }
1017
+ if (ts.isIfStatement(st) && !st.elseStatement) {
1018
+ let ret = st.thenStatement;
1019
+ if (ts.isBlock(ret) && ret.statements.length === 1)
1020
+ ret = ret.statements[0];
1021
+ if (!ts.isReturnStatement(ret))
1022
+ return {
1023
+ verdict: "could-not-judge",
1024
+ why: `the \`if\` at line ${lineOf(st)} does not simply return (not modelled)`,
1025
+ };
1026
+ const v = judgeReturn(ret.expression, st.expression, lineOf(ret));
1027
+ if (v)
1028
+ return v;
1029
+ continue;
1030
+ }
1031
+ if (ts.isReturnStatement(st)) {
1032
+ return (judgeReturn(st.expression, undefined, lineOf(st)) ?? {
1033
+ verdict: "honoured",
1034
+ });
1035
+ }
1036
+ return {
1037
+ verdict: "could-not-judge",
1038
+ why: `its body has a ${ts.SyntaxKind[st.kind]} (not modelled)`,
1039
+ };
1040
+ }
1041
+ // Falling off the end returns undefined — a refusal.
1042
+ return { verdict: "honoured" };
1043
+ }
1044
+ /** A statement that always throws: `throw …` or a block whose LAST statement
1045
+ * is a throw and that holds no `return` of its own. */
1046
+ function alwaysThrowsStmt(st) {
1047
+ if (ts.isThrowStatement(st))
1048
+ return true;
1049
+ if (!ts.isBlock(st) || st.statements.length === 0)
1050
+ return false;
1051
+ let ret = false;
1052
+ walkOwn(st, (x) => {
1053
+ if (ts.isReturnStatement(x))
1054
+ ret = true;
1055
+ });
1056
+ return !ret && ts.isThrowStatement(st.statements[st.statements.length - 1]);
1057
+ }
1058
+ /** Read the direct statements of handler `name` before `beforeLine`. `null`
1059
+ * when the handler's body cannot be located. */
1060
+ export function readHandlerGuards(fileName, source, handlerName, beforeLine) {
1061
+ const sf = parse(fileName, source);
1062
+ let body;
1063
+ for (const st of sf.statements) {
1064
+ if (!ts.isVariableStatement(st))
1065
+ continue;
1066
+ for (const d of st.declarationList.declarations) {
1067
+ if (!ts.isIdentifier(d.name) || d.name.text !== handlerName)
1068
+ continue;
1069
+ const init = d.initializer;
1070
+ if (!init || !ts.isCallExpression(init))
1071
+ continue;
1072
+ const cfg = init.arguments[0];
1073
+ if (!cfg || !ts.isObjectLiteralExpression(cfg))
1074
+ continue;
1075
+ for (const p of cfg.properties) {
1076
+ if (!p.name || !ts.isIdentifier(p.name) || p.name.text !== "handler")
1077
+ continue;
1078
+ const fn = ts.isPropertyAssignment(p) ? p.initializer : p;
1079
+ if ((ts.isArrowFunction(fn) ||
1080
+ ts.isFunctionExpression(fn) ||
1081
+ ts.isMethodDeclaration(fn)) &&
1082
+ fn.body &&
1083
+ ts.isBlock(fn.body))
1084
+ body = fn.body;
1085
+ }
1086
+ }
1087
+ }
1088
+ if (!body)
1089
+ return null;
1090
+ const out = { consts: new Map(), refusals: [] };
1091
+ for (const st of body.statements) {
1092
+ if (lineOf(st) >= beforeLine)
1093
+ break;
1094
+ if (ts.isVariableStatement(st) &&
1095
+ (st.declarationList.flags & ts.NodeFlags.Const) !== 0) {
1096
+ for (const d of st.declarationList.declarations) {
1097
+ if (!ts.isIdentifier(d.name) || !d.initializer)
1098
+ continue;
1099
+ const u = unwrap(d.initializer);
1100
+ const awaited = ts.isAwaitExpression(d.initializer)
1101
+ ? unwrap(d.initializer)
1102
+ : undefined;
1103
+ out.consts.set(d.name.text, {
1104
+ identityRead: isIdentityRead(u),
1105
+ awaitedCallee: awaited &&
1106
+ ts.isCallExpression(awaited) &&
1107
+ ts.isIdentifier(awaited.expression)
1108
+ ? awaited.expression.text
1109
+ : undefined,
1110
+ });
1111
+ }
1112
+ continue;
1113
+ }
1114
+ if (!ts.isIfStatement(st) || st.elseStatement)
1115
+ continue;
1116
+ if (!alwaysThrowsStmt(st.thenStatement))
1117
+ continue;
1118
+ const c = unwrap(st.expression);
1119
+ if (!ts.isPrefixUnaryExpression(c) ||
1120
+ c.operator !== ts.SyntaxKind.ExclamationToken)
1121
+ continue;
1122
+ const call = unwrap(c.operand);
1123
+ if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression))
1124
+ continue;
1125
+ out.refusals.push({
1126
+ predicate: call.expression.text,
1127
+ args: call.arguments.map((a) => {
1128
+ const ua = unwrap(a);
1129
+ return {
1130
+ text: ua.getText(sf),
1131
+ ident: ts.isIdentifier(ua) ? ua.text : undefined,
1132
+ memberBase: ts.isPropertyAccessExpression(ua) && ts.isIdentifier(ua.expression)
1133
+ ? ua.expression.text
1134
+ : undefined,
1135
+ };
1136
+ }),
1137
+ line: lineOf(st),
1138
+ });
1139
+ }
1140
+ return out;
1141
+ }
1142
+ export function conditionalBoundaries(text, fields) {
1143
+ const sf = parse("write-site.ts", text);
1144
+ const out = [];
1145
+ const readsArgs = (e) => readsAny(e, new Set(["args"]));
1146
+ const visit = (n) => {
1147
+ if (ts.isPropertyAssignment(n) &&
1148
+ ts.isIdentifier(n.name) &&
1149
+ fields.has(n.name.text)) {
1150
+ const c = unwrap(n.initializer);
1151
+ if (ts.isConditionalExpression(c)) {
1152
+ let cond = unwrap(c.condition);
1153
+ if (ts.isBinaryExpression(cond) &&
1154
+ cond.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken &&
1155
+ cond.right.kind === ts.SyntaxKind.TrueKeyword)
1156
+ cond = unwrap(cond.left);
1157
+ out.push({
1158
+ field: n.name.text,
1159
+ masterBase: ts.isPropertyAccessExpression(cond) &&
1160
+ cond.name.text === "isMaster" &&
1161
+ ts.isIdentifier(cond.expression)
1162
+ ? cond.expression.text
1163
+ : undefined,
1164
+ trueReadsArgs: readsArgs(c.whenTrue),
1165
+ falseReadsArgs: readsArgs(c.whenFalse),
1166
+ });
1167
+ }
1168
+ }
1169
+ n.forEachChild(visit);
1170
+ };
1171
+ visit(sf);
1172
+ return out;
1173
+ }