supercov 0.0.44 → 0.0.45

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 (37) hide show
  1. package/README.md +21 -12
  2. package/docs/agent-loop.md +12 -8
  3. package/docs/assertion-agent.md +156 -0
  4. package/docs/assertion-evidence.md +9 -694
  5. package/docs/assertion-maps.md +252 -0
  6. package/docs/assertions.md +82 -0
  7. package/docs/cli.md +23 -8
  8. package/docs/coverage-model.md +12 -0
  9. package/package.json +34 -35
  10. package/runtime/javascript/runtime.mjs +18 -33
  11. package/schemas/assertions.schema.json +276 -0
  12. package/analyzers/typescript/README.md +0 -59
  13. package/analyzers/typescript/bin/compiler-identity.mjs +0 -78
  14. package/analyzers/typescript/bin/identity.mjs +0 -71
  15. package/analyzers/typescript/bin/query.mjs +0 -29
  16. package/analyzers/typescript/dist/analyze.js +0 -5273
  17. package/analyzers/typescript/dist/archive.js +0 -337
  18. package/analyzers/typescript/dist/awaited-observations.js +0 -376
  19. package/analyzers/typescript/dist/build-identity.json +0 -1
  20. package/analyzers/typescript/dist/compiler.js +0 -32
  21. package/analyzers/typescript/dist/frontend.js +0 -75
  22. package/analyzers/typescript/dist/mock-counts.js +0 -2517
  23. package/analyzers/typescript/dist/native-frontend.js +0 -271
  24. package/analyzers/typescript/dist/pragmas.js +0 -186
  25. package/analyzers/typescript/dist/types.js +0 -1
  26. package/analyzers/typescript/package.json +0 -27
  27. package/analyzers/typescript/src/analyze.ts +0 -6180
  28. package/analyzers/typescript/src/archive.ts +0 -471
  29. package/analyzers/typescript/src/awaited-observations.ts +0 -561
  30. package/analyzers/typescript/src/compiler.ts +0 -49
  31. package/analyzers/typescript/src/frontend.ts +0 -136
  32. package/analyzers/typescript/src/mock-counts.ts +0 -3219
  33. package/analyzers/typescript/src/native-frontend.ts +0 -315
  34. package/analyzers/typescript/src/pragmas.ts +0 -284
  35. package/analyzers/typescript/src/types.ts +0 -45
  36. package/analyzers/typescript/tsconfig.json +0 -12
  37. package/docs/code-verification.md +0 -4
@@ -1,2517 +0,0 @@
1
- /** A closed registration table, not title-prefix guessing or runtime-value inference. */
2
- export function sourceTestRows(syntax, fn, model) {
3
- const ts = syntax;
4
- let ancestor = fn.parent;
5
- while (ancestor &&
6
- !ts.isForOfStatement(ancestor) &&
7
- !ts.isSourceFile(ancestor))
8
- ancestor = ancestor.parent;
9
- if (!ancestor || !ts.isForOfStatement(ancestor))
10
- return undefined;
11
- const unsupported = (reason) => ({ rows: [], reason });
12
- const loop = ancestor, sf = fn.getSourceFile();
13
- const registration = fn.parent;
14
- if (!ts.isSourceFile(loop.parent) ||
15
- loop.awaitModifier ||
16
- !ts.isBlock(loop.statement) ||
17
- loop.statement.statements.length !== 1 ||
18
- !ts.isCallExpression(registration) ||
19
- registration.arguments.length !== 2 ||
20
- registration.arguments[1] !== fn ||
21
- !model.nativeTest(registration) ||
22
- !ts.isExpressionStatement(registration.parent) ||
23
- registration.parent !== loop.statement.statements[0])
24
- return unsupported("unsupported-row-registration");
25
- const peel = (raw) => {
26
- let e = raw;
27
- while (ts.isParenthesizedExpression(e) ||
28
- ts.isAsExpression(e) ||
29
- ts.isTypeAssertionExpression(e) ||
30
- ts.isNonNullExpression(e) ||
31
- ts.isSatisfiesExpression(e))
32
- e = e.expression;
33
- return e;
34
- };
35
- const iterable = peel(loop.expression);
36
- if (!ts.isIdentifier(iterable))
37
- return unsupported("unsupported-row-table");
38
- const tableDeclaration = model.declaration(iterable);
39
- if (!tableDeclaration ||
40
- !ts.isVariableDeclaration(tableDeclaration) ||
41
- !ts.isIdentifier(tableDeclaration.name) ||
42
- !tableDeclaration.initializer ||
43
- !ts.isVariableDeclarationList(tableDeclaration.parent) ||
44
- !(tableDeclaration.parent.flags & ts.NodeFlags.Const) ||
45
- !ts.isVariableStatement(tableDeclaration.parent.parent) ||
46
- tableDeclaration.parent.parent.parent !== sf ||
47
- tableDeclaration.end >= loop.getStart(sf) ||
48
- tableDeclaration.parent.parent.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))
49
- return unsupported("unsupported-row-table-binding");
50
- const table = peel(tableDeclaration.initializer);
51
- if (!ts.isArrayLiteralExpression(table) ||
52
- !table.elements.length ||
53
- table.elements.length > 256)
54
- return unsupported("unsupported-row-table");
55
- // The literal's sole read must be this loop. Alias creation, mutation, another
56
- // consumer or direct eval makes a private const array insufficient evidence.
57
- let exclusive = true, budget = 16384;
58
- const scan = (node) => {
59
- if (!exclusive)
60
- return;
61
- if (--budget < 0) {
62
- exclusive = false;
63
- return;
64
- }
65
- if (ts.isIdentifier(node) &&
66
- model.declaration(node) === tableDeclaration &&
67
- node !== tableDeclaration.name &&
68
- node !== iterable)
69
- exclusive = false;
70
- if (ts.isIdentifier(node) && node.text === "eval")
71
- exclusive = false;
72
- ts.forEachChild(node, scan);
73
- };
74
- scan(sf);
75
- if (!exclusive)
76
- return unsupported("row-table-escapes-or-may-change");
77
- if (!ts.isVariableDeclarationList(loop.initializer) ||
78
- !(loop.initializer.flags & ts.NodeFlags.Const) ||
79
- loop.initializer.declarations.length !== 1)
80
- return unsupported("mutable-or-unsupported-row-bindings");
81
- const binding = loop.initializer.declarations[0];
82
- if (!ts.isObjectBindingPattern(binding.name) ||
83
- binding.initializer ||
84
- binding.name.elements.some((e) => !ts.isIdentifier(e.name) ||
85
- e.dotDotDotToken ||
86
- e.initializer ||
87
- (e.propertyName &&
88
- !(ts.isIdentifier(e.propertyName) ||
89
- ts.isStringLiteralLike(e.propertyName)))))
90
- return unsupported("unsupported-row-destructuring");
91
- const literal = (raw) => {
92
- const e = peel(raw);
93
- if (ts.isStringLiteralLike(e))
94
- return e.text;
95
- if (ts.isNumericLiteral(e) && Number.isFinite(Number(e.text)))
96
- return Number(e.text);
97
- if (e.kind === ts.SyntaxKind.TrueKeyword)
98
- return true;
99
- if (e.kind === ts.SyntaxKind.FalseKeyword)
100
- return false;
101
- if (e.kind === ts.SyntaxKind.NullKeyword)
102
- return null;
103
- return undefined;
104
- };
105
- const rows = [], titles = new Set();
106
- for (const [rowIndex, raw] of table.elements.entries()) {
107
- const row = peel(raw);
108
- if (!ts.isObjectLiteralExpression(row))
109
- return unsupported("unsupported-row-value");
110
- const properties = new Map();
111
- for (const member of row.properties) {
112
- if (!ts.isPropertyAssignment(member) ||
113
- !(ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name)))
114
- return unsupported("unsupported-row-value");
115
- const value = literal(member.initializer), key = member.name.text;
116
- if (value === undefined || key === "__proto__" || properties.has(key))
117
- return unsupported("unsupported-row-value");
118
- properties.set(key, value);
119
- }
120
- const bindings = new Map();
121
- const evidenceBindings = [];
122
- for (const element of binding.name.elements) {
123
- const key = (element.propertyName ?? element.name);
124
- if (!properties.has(key.text))
125
- return unsupported("missing-row-own-property");
126
- const value = properties.get(key.text);
127
- bindings.set(element, value);
128
- evidenceBindings.push({
129
- declaration: model.location(element),
130
- name: element.name.text,
131
- value,
132
- });
133
- }
134
- const name = peel(registration.arguments[0]);
135
- let title;
136
- if (ts.isStringLiteralLike(name))
137
- title = name.text;
138
- else if (ts.isTemplateExpression(name)) {
139
- title = name.head.text;
140
- for (const span of name.templateSpans) {
141
- const expr = peel(span.expression), declaration = model.declaration(expr);
142
- if (!ts.isIdentifier(expr) ||
143
- !declaration ||
144
- !bindings.has(declaration))
145
- return unsupported("unsupported-row-title");
146
- title += String(bindings.get(declaration)) + span.literal.text;
147
- }
148
- }
149
- else
150
- return unsupported("unsupported-row-title");
151
- if (titles.has(title))
152
- return unsupported("ambiguous-row-title");
153
- titles.add(title);
154
- rows.push({
155
- bindings,
156
- evidence: {
157
- model: "node-test-for-of-v1",
158
- status: "source-checked",
159
- loop: model.location(loop),
160
- table: model.location(table),
161
- row: model.location(row),
162
- rowIndex,
163
- title,
164
- bindings: evidenceBindings,
165
- },
166
- });
167
- }
168
- return { rows };
169
- }
170
- export function analyzeMockCounts(syntax, fn, model, row) {
171
- return runMockCounts(syntax, fn, model, row);
172
- }
173
- function runMockCounts(syntax, fn, model, row, trial) {
174
- const ts = syntax;
175
- const checks = new Map();
176
- const payloadChecks = new Map();
177
- const directReturnChecks = new Map();
178
- const completionChecks = new Map();
179
- let completionTargetEvaluations = 0;
180
- let activeDirectCalls = 0;
181
- let directTargetEvaluations = 0;
182
- const callArguments = new WeakMap();
183
- const payloadReads = new Map();
184
- const copyCall = (call) => {
185
- const copy = { ...call };
186
- const args = callArguments.get(call);
187
- if (args)
188
- callArguments.set(copy, args);
189
- return copy;
190
- };
191
- const installed = new Map();
192
- let locals = new Map(row?.bindings);
193
- const moduleChecks = new Map();
194
- const modules = new Map();
195
- const loading = new Set();
196
- let initializing = false;
197
- let budget = 4096;
198
- class Unsupported extends Error {
199
- }
200
- class ReachedAssertion extends Error {
201
- }
202
- // A modeled language exception, never an evaluator error or control signal.
203
- class ProgramThrow {
204
- value;
205
- source;
206
- constructor(value, source) {
207
- this.value = value;
208
- this.source = source;
209
- }
210
- }
211
- const fail = (node, why) => {
212
- throw new Unsupported(`${why} at ${model.location(node)}`);
213
- };
214
- const primitive = (v) => v === null || typeof v !== "object";
215
- const peel = (expr) => {
216
- while (ts.isParenthesizedExpression(expr) ||
217
- ts.isAsExpression(expr) ||
218
- ts.isTypeAssertionExpression(expr) ||
219
- ts.isNonNullExpression(expr) ||
220
- ts.isSatisfiesExpression(expr))
221
- expr = expr.expression;
222
- return expr;
223
- };
224
- const snapshot = (mock, node, kind) => ({
225
- kind,
226
- evidence: {
227
- model: "node-sync-console-count-v2",
228
- status: "source-checked",
229
- instance: mock.source,
230
- createdAt: mock.source,
231
- resetAt: mock.resetAt,
232
- readAt: model.location(node),
233
- installedAtRead: installed.get(mock.target) === mock,
234
- observedCount: mock.calls.length,
235
- calls: mock.calls.map(copyCall),
236
- ...(row ? { rowBinding: row.evidence } : {}),
237
- },
238
- });
239
- const record = (mock, node, action, args) => {
240
- if (mock) {
241
- const call = {
242
- source: model.location(node),
243
- action,
244
- site: model.site(node),
245
- };
246
- mock.calls.push(call);
247
- if (trial?.payload)
248
- callArguments.set(call, args);
249
- }
250
- };
251
- function describe(v) {
252
- if (--budget < 0)
253
- return fail(fn, "payload-budget");
254
- if (v === undefined)
255
- return { kind: "undefined" };
256
- if (v === null)
257
- return { kind: "null" };
258
- if (typeof v === "string")
259
- return { kind: "string", value: v };
260
- if (typeof v === "boolean")
261
- return { kind: "boolean", value: v };
262
- if (typeof v === "number") {
263
- if (!Number.isFinite(v) || Object.is(v, -0))
264
- return fail(fn, "unsupported-payload-number");
265
- return { kind: "number", value: v };
266
- }
267
- if (v.kind === "opaque-inspect-string")
268
- return { kind: "opaque-string" };
269
- if (v.kind === "quoted-string" || v.kind === "substring-pattern")
270
- return { kind: v.kind, value: v.value };
271
- if (v.kind === "object" || v.kind === "array") {
272
- accessible(v, fn);
273
- return {
274
- kind: v.kind,
275
- properties: [...v.properties]
276
- .sort(([a], [b]) => a.localeCompare(b))
277
- .map(([name, value]) => ({ name, value: describe(value) })),
278
- };
279
- }
280
- return fail(fn, "unsupported-payload-value");
281
- }
282
- function equalPayload(a, b, deep) {
283
- if (--budget < 0)
284
- return fail(fn, "payload-budget");
285
- if (a.kind === "quoted-string" || b.kind === "quoted-string") {
286
- const other = a.kind === "quoted-string" ? b : a;
287
- if (other.kind === "string")
288
- return other.value.includes("'") ? undefined : false;
289
- return other.kind === "opaque-string" || other.kind === "quoted-string"
290
- ? undefined
291
- : false;
292
- }
293
- if (a.kind === "opaque-string" || b.kind === "opaque-string") {
294
- const other = a.kind === "opaque-string" ? b : a;
295
- return other.kind === "string" || other.kind === "opaque-string"
296
- ? undefined
297
- : false;
298
- }
299
- if (a.kind !== b.kind)
300
- return false;
301
- if (a.kind === "object" || a.kind === "array") {
302
- if (!deep)
303
- return undefined; // SameValue requires identity, not structural equality.
304
- if (a.properties.length !== b.properties.length)
305
- return false;
306
- let unknown = false;
307
- for (let i = 0; i < a.properties.length; i++) {
308
- const x = a.properties[i], y = b.properties[i];
309
- if (x.name !== y.name)
310
- return false;
311
- const eq = equalPayload(x.value, y.value, true);
312
- if (eq === false)
313
- return false;
314
- if (eq === undefined)
315
- unknown = true;
316
- }
317
- return unknown ? undefined : true;
318
- }
319
- return Object.is(a.value, b.value);
320
- }
321
- function substringPattern(e) {
322
- if (!ts.isRegularExpressionLiteral(e))
323
- return;
324
- const match = /^\/([A-Za-z0-9 _:-]{1,80})\/$/.exec(e.text);
325
- return match?.[1]; // No flags, metacharacters, escapes or stateful regex behavior.
326
- }
327
- function independentLiteral(raw) {
328
- if (--budget < 0)
329
- return false;
330
- const e = peel(raw);
331
- if (substringPattern(e) !== undefined)
332
- return true;
333
- if (ts.isStringLiteralLike(e) ||
334
- ts.isNumericLiteral(e) ||
335
- [
336
- ts.SyntaxKind.NullKeyword,
337
- ts.SyntaxKind.TrueKeyword,
338
- ts.SyntaxKind.FalseKeyword,
339
- ].includes(e.kind))
340
- return true;
341
- if (ts.isArrayLiteralExpression(e))
342
- return e.elements.every(independentLiteral);
343
- return (ts.isObjectLiteralExpression(e) &&
344
- e.properties.every((p) => ts.isPropertyAssignment(p) &&
345
- !ts.isComputedPropertyName(p.name) &&
346
- independentLiteral(p.initializer)));
347
- }
348
- function stableTarget(declaration) {
349
- const sf = declaration.getSourceFile();
350
- let safeModule = moduleChecks.get(sf);
351
- if (safeModule !== undefined)
352
- return safeModule;
353
- if (safeModule === undefined) {
354
- safeModule = sf.statements.every((s) => ts.isImportDeclaration(s) ||
355
- ts.isExportDeclaration(s) ||
356
- ts.isFunctionDeclaration(s) ||
357
- ts.isInterfaceDeclaration(s) ||
358
- ts.isTypeAliasDeclaration(s) ||
359
- ts.isEmptyStatement(s) ||
360
- (ts.isVariableStatement(s) &&
361
- !!(s.declarationList.flags & ts.NodeFlags.Const) &&
362
- s.declarationList.declarations.every((d) => ts.isIdentifier(d.name) && !!d.initializer)));
363
- }
364
- if (!safeModule) {
365
- moduleChecks.set(sf, false);
366
- return false;
367
- }
368
- let stable = true;
369
- const scan = (node) => {
370
- if (!stable)
371
- return;
372
- if (--budget < 0) {
373
- stable = false;
374
- return;
375
- }
376
- if (ts.isBinaryExpression(node) &&
377
- node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
378
- node.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
379
- stable = false;
380
- if ((ts.isPrefixUnaryExpression(node) ||
381
- ts.isPostfixUnaryExpression(node)) &&
382
- [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator))
383
- stable = false;
384
- if (ts.isDeleteExpression(node))
385
- stable = false;
386
- if (ts.isIdentifier(node) && node.text === "eval")
387
- stable = false;
388
- ts.forEachChild(node, scan);
389
- };
390
- scan(sf);
391
- moduleChecks.set(sf, stable);
392
- return stable;
393
- }
394
- // Initialization is evaluated with effects forbidden, never replayed as work
395
- // performed under a test's mock. Objects allocated here stay shared/unknown:
396
- // const prevents rebinding, not mutation by another caller or earlier test.
397
- function moduleEnvironment(declaration, depth) {
398
- const sf = declaration.getSourceFile();
399
- if (trial && sf !== trial.module)
400
- return fail(declaration, "omission-module-outside-checked-scope");
401
- if (loading.has(sf))
402
- return fail(declaration, "cyclic-or-forward-module-binding");
403
- const cached = modules.get(sf);
404
- if (cached)
405
- return cached;
406
- if (!stableTarget(declaration))
407
- return fail(declaration, "mutable-target-or-unsupported-module-initialization");
408
- const previous = locals, wasInitializing = initializing;
409
- locals = new Map();
410
- initializing = true;
411
- loading.add(sf);
412
- modules.set(sf, locals);
413
- try {
414
- for (const statement of sf.statements)
415
- if (ts.isFunctionDeclaration(statement))
416
- locals.set(statement, {
417
- kind: "closure",
418
- node: statement,
419
- environment: locals,
420
- });
421
- for (const statement of sf.statements)
422
- if (ts.isVariableStatement(statement))
423
- execute(statement, model.location(statement), depth + 1);
424
- return locals;
425
- }
426
- finally {
427
- locals = previous;
428
- initializing = wasInitializing;
429
- loading.delete(sf);
430
- }
431
- }
432
- function accessible(value, node) {
433
- if (!initializing &&
434
- !primitive(value) &&
435
- (value.kind === "object" || value.kind === "array") &&
436
- value.moduleOwned &&
437
- (!trial ||
438
- (trial.allocations &&
439
- (!value.allocation || !trial.allocations.has(value.allocation)))))
440
- return fail(node, "shared-module-object-history");
441
- return value;
442
- }
443
- function sourceValue(value) {
444
- if (--budget < 0)
445
- return fail(fn, "source-model-budget");
446
- return (primitive(value) ||
447
- value.kind === "closure" ||
448
- value.kind === "native-error" ||
449
- value.kind === "native-regexp" ||
450
- value.kind === "native-error-constructor" ||
451
- value.kind === "opaque-inspect-string" ||
452
- value.kind === "quoted-string" ||
453
- ((value.kind === "object" || value.kind === "array") &&
454
- [...value.properties.values()].every(sourceValue)));
455
- }
456
- function propertyName(node) {
457
- if (ts.isIdentifier(node) ||
458
- ts.isStringLiteralLike(node) ||
459
- ts.isNumericLiteral(node))
460
- return node.text;
461
- return fail(node, "computed-property-name");
462
- }
463
- function bind(name, declaration, value, action, depth) {
464
- if (ts.isIdentifier(name)) {
465
- locals.set(declaration, value);
466
- return;
467
- }
468
- if (!ts.isObjectBindingPattern(name) ||
469
- primitive(value) ||
470
- value.kind !== "object")
471
- return fail(name, "unsupported-parameter-binding");
472
- accessible(value, name);
473
- for (const element of name.elements) {
474
- if (element.dotDotDotToken || !ts.isIdentifier(element.name))
475
- return fail(element, "unsupported-parameter-binding");
476
- const key = propertyName(element.propertyName ?? element.name);
477
- // Missing properties could be inherited. No prototype lookup is guessed.
478
- if (!value.properties.has(key))
479
- return fail(element, "missing-own-property");
480
- let item = value.properties.get(key);
481
- if (item === undefined && element.initializer)
482
- item = evaluate(element.initializer, action, depth + 1);
483
- bind(element.name, element, item, action, depth + 1);
484
- }
485
- }
486
- function invoke(closure, args, at, action, depth) {
487
- const target = closure.node;
488
- const testCallback = !!trial?.completion && target.getSourceFile() === fn.getSourceFile();
489
- if (!target.body ||
490
- (!model.production(target) && !testCallback) ||
491
- target.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ||
492
- ("asteriskToken" in target && target.asteriskToken))
493
- return fail(at, "unsupported-call-target");
494
- if (!testCallback && !stableTarget(target))
495
- return fail(at, "mutable-target-or-unsupported-module-initialization");
496
- if (args.some((arg) => !sourceValue(arg)))
497
- return fail(at, "escaping-nonprimitive-argument");
498
- const previous = locals;
499
- locals = new Map(closure.environment);
500
- try {
501
- target.parameters.forEach((parameter, index) => {
502
- let value = args[index];
503
- if (parameter.dotDotDotToken) {
504
- value = {
505
- kind: "array",
506
- moduleOwned: initializing,
507
- properties: new Map(args.slice(index).map((v, i) => [String(i), v])),
508
- };
509
- }
510
- else if (value === undefined && parameter.initializer)
511
- value = evaluate(parameter.initializer, action, depth + 1);
512
- bind(parameter.name, parameter, value, action, depth + 1);
513
- });
514
- if (trial?.emptyMapCallback === target)
515
- return undefined;
516
- if (!ts.isBlock(target.body))
517
- return evaluate(target.body, action, depth + 1);
518
- for (const statement of target.body.statements) {
519
- const returned = execute(statement, action, depth + 1);
520
- if (returned)
521
- return returned.value;
522
- }
523
- return undefined;
524
- }
525
- finally {
526
- locals = previous;
527
- }
528
- }
529
- function argumentsOf(call, action, depth) {
530
- const result = [];
531
- for (const arg of call.arguments) {
532
- if (ts.isSpreadElement(arg)) {
533
- const value = evaluate(arg.expression, action, depth + 1);
534
- if (primitive(value) || value.kind !== "array")
535
- return fail(arg, "unsupported-spread");
536
- accessible(value, arg);
537
- result.push(...value.properties.values());
538
- }
539
- else
540
- result.push(evaluate(arg, action, depth + 1));
541
- if (result.length > 4096)
542
- return fail(call, "source-model-budget");
543
- }
544
- return result;
545
- }
546
- function evaluate(raw, action, depth) {
547
- if (--budget < 0 || depth > 32)
548
- return fail(raw, "source-model-budget");
549
- const e = peel(raw);
550
- // Forming a native regex literal or reading the pristine Error constructor
551
- // is distinct from executing a regex or invoking a matcher. The missing-
552
- // exception shortcut needs only that formation and safe diagnostic metadata.
553
- if (trial?.completion && ts.isRegularExpressionLiteral(e))
554
- return { kind: "native-regexp" };
555
- if (trial?.completion && model.globalError?.(e))
556
- return { kind: "native-error-constructor" };
557
- if (trial?.completion &&
558
- (ts.isNewExpression(e) || ts.isCallExpression(e)) &&
559
- model.globalError?.(e.expression)) {
560
- // No custom constructor, options/cause object, coercion hooks or stack
561
- // inspection. Preserve the primitive-derived message for the native
562
- // doesNotThrow failure diagnostic, not arbitrary Error property access.
563
- if (e.arguments && e.arguments.length > 1)
564
- return fail(e, "unsupported-error-constructor-options");
565
- let message = undefined;
566
- for (const arg of e.arguments ?? []) {
567
- message = evaluate(arg, action, depth + 1);
568
- if (!primitive(message))
569
- return fail(e, "unsupported-error-message-coercion");
570
- }
571
- return {
572
- kind: "native-error",
573
- message: message === undefined ? "" : String(message),
574
- };
575
- }
576
- if (trial?.directReturn?.target === e && activeDirectCalls > 0)
577
- directTargetEvaluations++;
578
- if (trial?.condition?.node === e && trial.condition.value !== "invert")
579
- return trial.condition.value;
580
- if (trial?.directReturn &&
581
- ts.isPrefixUnaryExpression(e) &&
582
- e.operator === ts.SyntaxKind.ExclamationToken) {
583
- const value = evaluate(e.operand, action, depth + 1);
584
- if (primitive(value))
585
- return !value;
586
- // ToBoolean on a fresh ordinary object/array never invokes conversion hooks.
587
- if (value.kind === "object" || value.kind === "array")
588
- return false;
589
- return fail(e, "unsupported-direct-return-truthiness");
590
- }
591
- if (ts.isTypeOfExpression(e)) {
592
- const v = evaluate(e.expression, action, depth + 1);
593
- if (primitive(v))
594
- return typeof v;
595
- if (v.kind === "object" || v.kind === "array")
596
- return "object";
597
- if (v.kind === "closure")
598
- return "function";
599
- if (v.kind === "opaque-inspect-string" || v.kind === "quoted-string")
600
- return "string";
601
- return fail(e, "unsupported-abstract-typeof");
602
- }
603
- if (trial?.allocations && model.nativeTty?.(e))
604
- return { kind: "opaque-native-tty" };
605
- if (ts.isStringLiteralLike(e))
606
- return e.text;
607
- if (trial?.payload && ts.isRegularExpressionLiteral(e)) {
608
- const value = substringPattern(e);
609
- if (value === undefined)
610
- return fail(e, "unsupported-payload-regexp");
611
- return { kind: "substring-pattern", value };
612
- }
613
- if (ts.isNumericLiteral(e))
614
- return Number(e.text);
615
- if (ts.isPrefixUnaryExpression(e) &&
616
- e.operator === ts.SyntaxKind.MinusToken) {
617
- const operand = evaluate(e.operand, action, depth + 1);
618
- if (typeof operand !== "number" || !Number.isFinite(operand))
619
- return fail(e, "unsupported-unary-operand");
620
- return -operand;
621
- }
622
- if (e.kind === ts.SyntaxKind.TrueKeyword)
623
- return true;
624
- if (e.kind === ts.SyntaxKind.FalseKeyword)
625
- return false;
626
- if (e.kind === ts.SyntaxKind.NullKeyword)
627
- return null;
628
- if (ts.isIdentifier(e)) {
629
- const d = model.declaration(e);
630
- if (e.text === "undefined" && (!d || d.getSourceFile().isDeclarationFile))
631
- return undefined;
632
- if (d && locals.has(d))
633
- return accessible(locals.get(d), e);
634
- if (d && model.production(d)) {
635
- const environment = moduleEnvironment(d, depth);
636
- if (environment.has(d))
637
- return accessible(environment.get(d), e);
638
- }
639
- return fail(e, "unresolved-value-binding");
640
- }
641
- if (ts.isArrowFunction(e) || ts.isFunctionExpression(e))
642
- return { kind: "closure", node: e, environment: locals };
643
- if (ts.isObjectLiteralExpression(e)) {
644
- const properties = new Map();
645
- for (const item of e.properties) {
646
- if (!(ts.isPropertyAssignment(item) ||
647
- ts.isShorthandPropertyAssignment(item)))
648
- return fail(item, "unsupported-object-member");
649
- const key = propertyName(item.name);
650
- if (key === "__proto__")
651
- return fail(item, "unsupported-object-prototype");
652
- properties.set(key, evaluate(ts.isPropertyAssignment(item) ? item.initializer : item.name, action, depth + 1));
653
- }
654
- return {
655
- kind: "object",
656
- properties,
657
- moduleOwned: initializing,
658
- allocation: e,
659
- };
660
- }
661
- if (ts.isArrayLiteralExpression(e)) {
662
- const properties = new Map();
663
- for (const item of e.elements) {
664
- if (ts.isOmittedExpression(item) || ts.isSpreadElement(item))
665
- return fail(item, "unsupported-array-member");
666
- properties.set(String(properties.size), evaluate(item, action, depth + 1));
667
- }
668
- return { kind: "array", properties, moduleOwned: initializing };
669
- }
670
- if (ts.isConditionalExpression(e)) {
671
- const condition = evaluate(e.condition, action, depth + 1);
672
- if (!primitive(condition))
673
- return fail(e, "nonprimitive-condition");
674
- return evaluate(condition ? e.whenTrue : e.whenFalse, action, depth + 1);
675
- }
676
- if (ts.isBinaryExpression(e) &&
677
- [
678
- ts.SyntaxKind.EqualsEqualsEqualsToken,
679
- ts.SyntaxKind.ExclamationEqualsEqualsToken,
680
- ].includes(e.operatorToken.kind)) {
681
- const left = evaluate(e.left, action, depth + 1), right = evaluate(e.right, action, depth + 1);
682
- if (!primitive(left) || !primitive(right))
683
- return fail(e, "nonprimitive-comparison");
684
- const result = e.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken
685
- ? left === right
686
- : left !== right;
687
- return trial?.condition?.node === e && trial.condition.value === "invert"
688
- ? !result
689
- : result;
690
- }
691
- if (ts.isPropertyAccessExpression(e)) {
692
- const base = evaluate(e.expression, action, depth + 1);
693
- if (!primitive(base)) {
694
- if (trial?.payload &&
695
- base.kind === "recorded-call" &&
696
- e.name.text === "arguments")
697
- return {
698
- kind: "array",
699
- moduleOwned: false,
700
- properties: new Map(base.args.map((v, i) => [String(i), v])),
701
- argumentSelection: base.selection,
702
- };
703
- if (base.kind === "mock" && e.name.text === "mock")
704
- return { kind: "context", mock: base };
705
- if (base.kind === "context" && e.name.text === "calls")
706
- return snapshot(base.mock, e, "history");
707
- if (base.kind === "history" && e.name.text === "length")
708
- return { ...base, kind: "count" };
709
- if (base.kind === "object" || base.kind === "array") {
710
- accessible(base, e);
711
- if (base.kind === "array" && e.name.text === "length")
712
- return base.properties.size;
713
- if (base.properties.has(e.name.text))
714
- return accessible(base.properties.get(e.name.text), e);
715
- }
716
- }
717
- return fail(e, "unsupported-property-read");
718
- }
719
- if (trial?.payload && ts.isElementAccessExpression(e)) {
720
- const base = evaluate(e.expression, action, depth + 1);
721
- const index = evaluate(e.argumentExpression, action, depth + 1);
722
- if (primitive(base) ||
723
- typeof index !== "number" ||
724
- !Number.isSafeInteger(index) ||
725
- index < 0)
726
- return fail(e, "unsupported-payload-index");
727
- if (base.kind === "history") {
728
- const call = base.evidence.calls?.[index], args = call && callArguments.get(call);
729
- if (!call || !args || !base.evidence.instance)
730
- return fail(e, "missing-selected-call");
731
- return {
732
- kind: "recorded-call",
733
- args,
734
- selection: {
735
- instance: base.evidence.instance,
736
- callSource: call.source,
737
- callIndex: index,
738
- historySelections: base.evidence.historySelections,
739
- },
740
- };
741
- }
742
- if (base.kind === "array" && base.argumentSelection) {
743
- if (!base.properties.has(String(index)))
744
- return fail(e, "missing-selected-argument");
745
- payloadReads.set(e, {
746
- ...base.argumentSelection,
747
- argumentIndex: index,
748
- readAt: model.location(e),
749
- });
750
- return accessible(base.properties.get(String(index)), e);
751
- }
752
- return fail(e, "unsupported-payload-projection");
753
- }
754
- if (!ts.isCallExpression(e))
755
- return fail(e, "unsupported-expression");
756
- // The checked edit replaces this expression-bodied arrow with () => undefined.
757
- // Its original arguments inside the body are NOT evaluated by that edit.
758
- // No source is executed and no test code is modified during this query.
759
- if (trial?.omit === e)
760
- return undefined;
761
- const callee = peel(e.expression);
762
- if (trial?.directReturn &&
763
- ts.isPropertyAccessExpression(callee) &&
764
- callee.name.text === "includes") {
765
- const base = evaluate(callee.expression, action, depth + 1);
766
- if (primitive(base) || base.kind !== "array")
767
- return fail(e, "unsupported-direct-return-includes-receiver");
768
- accessible(base, e);
769
- const args = argumentsOf(e, action, depth + 1);
770
- if (args.length !== 1 ||
771
- !primitive(args[0]) ||
772
- [...base.properties.values()].some((value) => !primitive(value)))
773
- return fail(e, "unsupported-direct-return-includes-input");
774
- const sought = args[0];
775
- return [...base.properties.values()].some((value) => value === sought ||
776
- (typeof value === "number" &&
777
- typeof sought === "number" &&
778
- Number.isNaN(value) &&
779
- Number.isNaN(sought)));
780
- }
781
- if (trial?.payload && model.globalString?.(callee)) {
782
- if (e.arguments.length !== 1)
783
- return fail(e, "unsupported-string-coercion-arity");
784
- const input = evaluate(e.arguments[0], action, depth + 1);
785
- let value, rule;
786
- if (typeof input === "string" ||
787
- (!primitive(input) &&
788
- (input.kind === "opaque-inspect-string" ||
789
- input.kind === "quoted-string"))) {
790
- value = input;
791
- rule = "string-identity";
792
- }
793
- else if (!primitive(input) &&
794
- input.kind === "object" &&
795
- !input.moduleOwned &&
796
- !input.properties.has("toString") &&
797
- !input.properties.has("valueOf")) {
798
- // Getters, custom symbols, altered prototypes and escaping writes are
799
- // excluded by closedCountScope. This does not execute user conversion code.
800
- value = "[object Object]";
801
- rule = "plain-object-default-string";
802
- }
803
- else
804
- return fail(e, "unsupported-string-coercion-input");
805
- const projection = payloadReads.get(peel(e.arguments[0]));
806
- if (projection)
807
- payloadReads.set(e, {
808
- ...projection,
809
- readAt: model.location(e),
810
- coercion: { source: model.location(e), rule, input: describe(input) },
811
- });
812
- return value;
813
- }
814
- if (trial?.allocations && model.nativeInspect?.(e)) {
815
- const plain = (v, level = 0) => {
816
- if (--budget < 0 || level > 32)
817
- return false;
818
- return (primitive(v) ||
819
- ((v.kind === "object" || v.kind === "array") &&
820
- !v.moduleOwned &&
821
- [...v.properties.values()].every((p) => plain(p, level + 1))));
822
- };
823
- const args = argumentsOf(e, action, depth + 1);
824
- if (args.length !== 2 ||
825
- !plain(args[0]) ||
826
- primitive(args[1]) ||
827
- args[1].kind !== "object" ||
828
- args[1].moduleOwned)
829
- return fail(e, "unsupported-inspect-summary-input");
830
- const options = args[1].properties, colors = options.get("colors");
831
- if (options.size !== 3 ||
832
- options.get("depth") !== null ||
833
- typeof options.get("compact") !== "boolean" ||
834
- !(typeof colors === "boolean" ||
835
- (!primitive(colors) && colors.kind === "opaque-native-tty")))
836
- return fail(e, "unsupported-inspect-summary-options");
837
- if (trial?.payload &&
838
- typeof args[0] === "string" &&
839
- /^[A-Za-z0-9 _-]{0,64}$/.test(args[0]))
840
- return { kind: "quoted-string", value: args[0] };
841
- return { kind: "opaque-inspect-string" };
842
- }
843
- if (ts.isPropertyAccessExpression(callee) &&
844
- ["map", "slice"].includes(callee.name.text)) {
845
- // Resolve the receiver once, before evaluating arguments. A factory call
846
- // here can itself log; history getters also snapshot before index effects.
847
- const base = evaluate(callee.expression, action, depth + 1);
848
- if (primitive(base))
849
- return fail(callee, "unsupported-array-receiver");
850
- if (base.kind === "object") {
851
- accessible(base, callee);
852
- const target = base.properties.get(callee.name.text);
853
- if (!target || primitive(target) || target.kind !== "closure")
854
- return fail(callee, "unsupported-array-receiver");
855
- // A source method named map/slice is not the native array operation.
856
- return accessible(invoke(target, argumentsOf(e, action, depth + 1), e, action, depth + 1), e);
857
- }
858
- if (base.kind !== "array" && base.kind !== "history")
859
- return fail(callee, "unsupported-array-receiver");
860
- if (base.kind === "array")
861
- accessible(base, callee);
862
- const args = argumentsOf(e, action, depth + 1);
863
- if (callee.name.text === "map") {
864
- if (base.kind !== "array" ||
865
- args.length !== 1 ||
866
- primitive(args[0]) ||
867
- args[0].kind !== "closure")
868
- return fail(e, "unsupported-array-map-callback");
869
- const properties = new Map();
870
- // All admitted arrays are fresh, dense arrays with native prototypes.
871
- // Mutation/escape in a callback remains unsupported by invoke/evaluate.
872
- const length = base.properties.size;
873
- for (let index = 0; index < length; index++)
874
- properties.set(String(index), invoke(args[0], [base.properties.get(String(index)), index, base], e, action, depth + 1));
875
- return { kind: "array", properties, moduleOwned: initializing };
876
- }
877
- if (args.length > 2 ||
878
- args.some((value) => value !== undefined &&
879
- (typeof value !== "number" || !Number.isSafeInteger(value))))
880
- return fail(e, "unsupported-slice-index");
881
- const length = base.kind === "history"
882
- ? base.evidence.calls.length
883
- : base.properties.size;
884
- const clamp = (n) => n < 0 ? Math.max(length + n, 0) : Math.min(n, length);
885
- const from = clamp(args[0] ?? 0);
886
- const to = Math.max(from, clamp(args[1] ?? length));
887
- if (base.kind === "history") {
888
- const calls = base.evidence.calls.slice(from, to).map(copyCall);
889
- return {
890
- kind: "history",
891
- evidence: {
892
- ...base.evidence,
893
- calls,
894
- observedCount: calls.length,
895
- historySelections: [
896
- ...(base.evidence.historySelections ?? []),
897
- { source: model.location(e), inputCount: length, from, to },
898
- ],
899
- },
900
- };
901
- }
902
- return {
903
- kind: "array",
904
- moduleOwned: initializing,
905
- properties: new Map([...base.properties.values()]
906
- .slice(from, to)
907
- .map((value, index) => [String(index), value])),
908
- };
909
- }
910
- if (model.nativeMock(e)) {
911
- if (initializing)
912
- return fail(e, "effectful-module-initialization");
913
- const [receiver, method, replacement] = e.arguments;
914
- if (!ts.isPropertyAccessExpression(callee) ||
915
- callee.name.text !== "method" ||
916
- e.arguments.length !== 3 ||
917
- !model.globalConsole(receiver) ||
918
- !ts.isStringLiteralLike(method) ||
919
- !["log", "error"].includes(method.text) ||
920
- !ts.isArrowFunction(replacement) ||
921
- replacement.parameters.length ||
922
- replacement.modifiers?.length ||
923
- !ts.isBlock(replacement.body) ||
924
- replacement.body.statements.length)
925
- return fail(e, "unsupported-mock-installation");
926
- const target = `console.${method.text}`;
927
- const mock = {
928
- kind: "mock",
929
- target,
930
- source: model.location(e),
931
- previous: installed.get(target),
932
- calls: [],
933
- };
934
- installed.set(target, mock);
935
- return mock;
936
- }
937
- if (ts.isPropertyAccessExpression(callee) &&
938
- model.globalConsole(callee.expression) &&
939
- ["log", "error"].includes(callee.name.text)) {
940
- if (initializing)
941
- return fail(e, "effectful-module-initialization");
942
- // JS resolves the callee before argument evaluation (which may itself call it).
943
- const mock = installed.get(`console.${callee.name.text}`);
944
- const args = argumentsOf(e, action, depth + 1);
945
- // An accepted installed mock has an empty replacement: Node records the
946
- // argument references but does not format/inspect them. This establishes
947
- // a call count only, not argument identity or value protection. Without
948
- // that mock, native console formatting may execute user code.
949
- if (args.some((arg) => (mock ? !sourceValue(arg) : !primitive(arg))))
950
- return fail(e, "nonprimitive-console-argument");
951
- record(mock, e, action, args);
952
- return undefined;
953
- }
954
- if (ts.isPropertyAccessExpression(callee) &&
955
- ["callCount", "resetCalls", "restore"].includes(callee.name.text)) {
956
- const base = evaluate(callee.expression, action, depth + 1);
957
- if (primitive(base) || base.kind !== "context" || e.arguments.length)
958
- return fail(e, "unsupported-mock-operation");
959
- if (callee.name.text === "callCount")
960
- return snapshot(base.mock, e, "count");
961
- if (callee.name.text === "resetCalls") {
962
- base.mock.calls = [];
963
- base.mock.resetAt = model.location(e);
964
- }
965
- else
966
- installed.set(base.mock.target, base.mock.previous);
967
- return undefined;
968
- }
969
- const exceptionMethod = trial?.completion && model.nativeException?.(e);
970
- if (exceptionMethod) {
971
- if (initializing ||
972
- !e.arguments.length ||
973
- e.arguments.length > 3 ||
974
- (exceptionMethod === "doesNotThrow" && e.arguments.length !== 1))
975
- return fail(e, "unsupported-completion-assertion-shape");
976
- if (e.arguments.length > 1 && trial.assertion !== e)
977
- return fail(e, "completion-earlier-matcher-assertion-unresolved");
978
- // Evaluate the operand OUTSIDE the assertion's catch. A factory's own
979
- // error is not a thrown result of the callback it was meant to produce.
980
- // JS evaluates every argument before native getActual invokes the callback.
981
- if (e.arguments.some(ts.isSpreadElement))
982
- return fail(e, "unsupported-completion-argument-spread");
983
- const args = argumentsOf(e, action, depth + 1);
984
- const callback = args[0];
985
- if (primitive(callback) || callback.kind !== "closure")
986
- return fail(e, "completion-operand-not-source-callback");
987
- let matcher;
988
- const expected = args[1];
989
- if (args.length > 1) {
990
- let kind;
991
- if (expected === null || expected === undefined)
992
- kind = "none";
993
- else if (typeof expected === "string") {
994
- if (args.length === 3)
995
- return fail(e, "completion-invalid-message-overload");
996
- kind = "message-overload";
997
- }
998
- else if (primitive(expected))
999
- return fail(e, "completion-invalid-error-matcher");
1000
- else if (expected.kind === "closure")
1001
- kind = "source-function";
1002
- else if ([
1003
- "native-regexp",
1004
- "native-error-constructor",
1005
- "native-error",
1006
- "object",
1007
- "array",
1008
- ].includes(expected.kind))
1009
- kind = expected.kind;
1010
- else
1011
- return fail(e, "completion-error-matcher-value-unresolved");
1012
- matcher = { source: model.location(e.arguments[1]), kind };
1013
- }
1014
- let thrown;
1015
- try {
1016
- invoke(callback, [], e, action, depth + 1);
1017
- }
1018
- catch (error) {
1019
- if (!(error instanceof ProgramThrow))
1020
- throw error;
1021
- thrown = error;
1022
- }
1023
- let missingExceptionDiagnostic;
1024
- if (matcher) {
1025
- if (!trial.completion.omit && thrown) {
1026
- // Only the selected assertion can use its own archived passing witness.
1027
- // Do not execute or pretend to understand an arbitrary matcher body.
1028
- completionChecks.set(e, {
1029
- method: exceptionMethod,
1030
- callbackSource: model.location(callback.node),
1031
- completion: "throw",
1032
- throwSource: thrown.source,
1033
- targetEvaluations: completionTargetEvaluations,
1034
- outcome: "witnessed-pass",
1035
- matcher,
1036
- });
1037
- throw new ReachedAssertion();
1038
- }
1039
- if (thrown)
1040
- return fail(e, "completion-changed-error-matcher-unresolved");
1041
- // On normal completion, expectsError skips expectedException entirely.
1042
- // It still reads expected.name twice and formats a supplied message.
1043
- // Prove those operations safe; do not equate skipping the matcher body
1044
- // with skipping all application-observable work.
1045
- const message = matcher.kind === "message-overload" ? expected : args[2];
1046
- if (!primitive(message))
1047
- return fail(e, "completion-missing-exception-message-coercion-unresolved");
1048
- const diagnostic = { message: describe(message) };
1049
- switch (matcher.kind) {
1050
- case "none":
1051
- case "message-overload":
1052
- case "native-regexp":
1053
- case "array":
1054
- missingExceptionDiagnostic = { ...diagnostic, nameBasis: "absent" };
1055
- break;
1056
- case "source-function":
1057
- // Fresh source function names (including inferred names) are strings.
1058
- // No specific inferred spelling is fabricated or needed here.
1059
- missingExceptionDiagnostic = {
1060
- ...diagnostic,
1061
- nameBasis: "source-function-name",
1062
- };
1063
- break;
1064
- case "native-error":
1065
- case "native-error-constructor":
1066
- missingExceptionDiagnostic = {
1067
- ...diagnostic,
1068
- nameBasis: "native-error-name",
1069
- name: describe("Error"),
1070
- };
1071
- break;
1072
- case "object": {
1073
- if (primitive(expected) || expected.kind !== "object")
1074
- return fail(e, "completion-missing-exception-matcher-kind");
1075
- accessible(expected, e);
1076
- const name = expected.properties.get("name");
1077
- if (!primitive(name))
1078
- return fail(e, "completion-missing-exception-name-coercion-unresolved");
1079
- missingExceptionDiagnostic = expected.properties.has("name")
1080
- ? {
1081
- ...diagnostic,
1082
- nameBasis: "own-primitive-name",
1083
- name: describe(name),
1084
- }
1085
- : { ...diagnostic, nameBasis: "absent" };
1086
- }
1087
- }
1088
- }
1089
- let diagnostic;
1090
- if (exceptionMethod === "doesNotThrow" && thrown) {
1091
- // Native expectsNoError formats `${actual?.message}` before throwing its
1092
- // AssertionError. An object/function conversion here can run arbitrary
1093
- // application code, exit successfully or never return. Abrupt callback
1094
- // completion alone is therefore insufficient proof of rejection.
1095
- const value = thrown.value;
1096
- if (primitive(value))
1097
- diagnostic = {
1098
- basis: "primitive-thrown-value",
1099
- message: describe(undefined),
1100
- };
1101
- else if (value.kind === "native-error")
1102
- diagnostic = {
1103
- basis: "native-error-message",
1104
- message: describe(value.message),
1105
- };
1106
- else if (value.kind === "closure")
1107
- diagnostic = {
1108
- basis: "absent-message",
1109
- message: describe(undefined),
1110
- };
1111
- else if (value.kind === "object" || value.kind === "array") {
1112
- // Accepted aggregates are fresh own-data-property objects/arrays with
1113
- // pristine prototypes; accessors and prototype mutation are outside
1114
- // this source model. Do not generalize this to arbitrary JS objects.
1115
- accessible(value, e);
1116
- const message = value.properties.get("message");
1117
- if (!primitive(message))
1118
- return fail(e, "completion-diagnostic-message-coercion-unresolved");
1119
- diagnostic = {
1120
- basis: value.properties.has("message")
1121
- ? "own-primitive-message"
1122
- : "absent-message",
1123
- message: describe(message),
1124
- };
1125
- }
1126
- else
1127
- return fail(e, "completion-diagnostic-message-access-unresolved");
1128
- }
1129
- const rejected = exceptionMethod === "throws" ? !thrown : !!thrown;
1130
- if (trial.assertion === e) {
1131
- completionChecks.set(e, {
1132
- method: exceptionMethod,
1133
- callbackSource: model.location(callback.node),
1134
- completion: thrown ? "throw" : "normal",
1135
- ...(thrown ? { throwSource: thrown.source } : {}),
1136
- ...(diagnostic ? { diagnostic } : {}),
1137
- ...(matcher ? { matcher } : {}),
1138
- ...(missingExceptionDiagnostic ? { missingExceptionDiagnostic } : {}),
1139
- targetEvaluations: completionTargetEvaluations,
1140
- outcome: rejected ? "rejected" : "not-rejected",
1141
- });
1142
- throw new ReachedAssertion();
1143
- }
1144
- if (rejected)
1145
- return fail(e, "earlier-completion-assertion-rejects");
1146
- return undefined;
1147
- }
1148
- const predicate = model.nativePredicate(e);
1149
- if (predicate) {
1150
- if (initializing)
1151
- return fail(e, "effectful-module-initialization");
1152
- if (trial &&
1153
- predicate !== "node-same-value" &&
1154
- !((trial.payload &&
1155
- ["node-deep-strict-equality", "node-literal-regexp"].includes(predicate)) ||
1156
- (trial.directReturn && predicate === "node-deep-strict-equality")))
1157
- return fail(e, "unsupported-trial-predicate");
1158
- if (e.arguments.length < 2 || e.arguments.length > 3)
1159
- return fail(e, "unsupported-comparison-arity");
1160
- const values = e.arguments.map((arg) => evaluate(arg, action, depth + 1));
1161
- if (values.length === 3 && !primitive(values[2]))
1162
- return fail(e, "unsupported-assertion-message");
1163
- const [a, b] = values;
1164
- if (trial?.directReturn) {
1165
- if (!independentLiteral(e.arguments[1]))
1166
- return fail(e, "direct-return-expectation-not-independent");
1167
- const actual = describe(a), expected = describe(b);
1168
- const equal = equalPayload(actual, expected, predicate === "node-deep-strict-equality");
1169
- if (equal === undefined)
1170
- return fail(e, "direct-return-predicate-undecidable-in-model");
1171
- if (trial.assertion === e) {
1172
- if (directTargetEvaluations === 0)
1173
- return fail(e, "direct-return-target-not-evaluated-by-selected-call");
1174
- directReturnChecks.set(e, {
1175
- predicate,
1176
- actual,
1177
- expected,
1178
- callSource: model.location(trial.directReturn.call),
1179
- targetEvaluations: directTargetEvaluations,
1180
- outcome: equal ? "not-rejected" : "rejected",
1181
- });
1182
- throw new ReachedAssertion();
1183
- }
1184
- if (!equal)
1185
- return fail(e, "earlier-direct-return-assertion-rejects");
1186
- return undefined;
1187
- }
1188
- if (trial?.payload &&
1189
- !(!primitive(a) && a.kind === "count") &&
1190
- !(!primitive(b) && b.kind === "count")) {
1191
- const actualValue = describe(a), expectedValue = describe(b);
1192
- const equal = predicate === "node-literal-regexp"
1193
- ? expectedValue.kind === "substring-pattern" &&
1194
- actualValue.kind === "string"
1195
- ? actualValue.value.includes(expectedValue.value)
1196
- : undefined
1197
- : equalPayload(actualValue, expectedValue, predicate === "node-deep-strict-equality");
1198
- const witnessed = equal === undefined &&
1199
- trial.originalWitness &&
1200
- trial.assertion === e &&
1201
- (actualValue.kind === "opaque-string" ||
1202
- actualValue.kind === "quoted-string") &&
1203
- (expectedValue.kind === "string" ||
1204
- (predicate === "node-literal-regexp" &&
1205
- expectedValue.kind === "substring-pattern"));
1206
- if (equal === undefined && !witnessed)
1207
- return fail(e, "payload-predicate-undecidable-in-model");
1208
- if (trial.assertion === e) {
1209
- const projection = payloadReads.get(peel(e.arguments[0]));
1210
- if (!projection || !independentLiteral(e.arguments[1]))
1211
- return fail(e, "payload-expectation-or-projection-not-independent");
1212
- payloadChecks.set(e, {
1213
- predicate,
1214
- actual: actualValue,
1215
- expected: expectedValue,
1216
- projection,
1217
- outcome: witnessed
1218
- ? "witnessed-pass"
1219
- : equal
1220
- ? "not-rejected"
1221
- : "rejected",
1222
- });
1223
- throw new ReachedAssertion();
1224
- }
1225
- if (!equal)
1226
- return fail(e, "earlier-payload-assertion-rejects");
1227
- return undefined;
1228
- }
1229
- const actual = !primitive(a) && a.kind === "count"
1230
- ? a
1231
- : !primitive(b) && b.kind === "count"
1232
- ? b
1233
- : undefined;
1234
- const expected = actual === a ? b : a;
1235
- if (actual) {
1236
- if (typeof expected !== "number" ||
1237
- !Number.isSafeInteger(expected) ||
1238
- expected < 0)
1239
- return fail(e, "count-expectation-not-independent-integer");
1240
- if (actual.evidence.observedCount !== expected &&
1241
- trial?.assertion !== e)
1242
- return fail(e, "source-count-disagrees-with-passing-assertion");
1243
- checks.set(e, { ...actual.evidence, expectedCount: expected });
1244
- }
1245
- else if (!primitive(a) || !primitive(b))
1246
- return fail(e, "unsupported-comparison-operand");
1247
- else if (trial && !Object.is(a, b))
1248
- return fail(e, "earlier-primitive-assertion-rejects");
1249
- if (trial?.assertion === e)
1250
- throw new ReachedAssertion();
1251
- return undefined;
1252
- }
1253
- const declaration = model.declaration(callee);
1254
- const saved = declaration && locals.get(declaration);
1255
- if (saved && !primitive(saved) && saved.kind === "mock") {
1256
- const args = argumentsOf(e, action, depth + 1);
1257
- if (args.some((arg) => !sourceValue(arg)))
1258
- return fail(e, "nonprimitive-mock-argument");
1259
- record(saved, e, action, args);
1260
- return undefined;
1261
- }
1262
- if (declaration &&
1263
- ts.isVariableDeclaration(declaration) &&
1264
- !(declaration.parent.flags & ts.NodeFlags.Const))
1265
- return fail(e, "mutable-call-target");
1266
- let target;
1267
- // Namespace import access has a resolved source declaration rather than a
1268
- // modeled namespace object. Do not bypass a runtime receiver with a mere name.
1269
- if (declaration &&
1270
- model.production(declaration) &&
1271
- (ts.isFunctionDeclaration(declaration) ||
1272
- ts.isVariableDeclaration(declaration)) &&
1273
- !locals.has(declaration))
1274
- target = moduleEnvironment(declaration, depth).get(declaration);
1275
- else
1276
- target = evaluate(callee, action, depth + 1);
1277
- if (primitive(target) || target.kind !== "closure")
1278
- return fail(e, "unsupported-call-target");
1279
- const selectedDirectCall = trial?.directReturn?.call === e;
1280
- if (selectedDirectCall)
1281
- activeDirectCalls++;
1282
- try {
1283
- return accessible(invoke(target, argumentsOf(e, action, depth + 1), e, action, depth + 1), e);
1284
- }
1285
- finally {
1286
- if (selectedDirectCall)
1287
- activeDirectCalls--;
1288
- }
1289
- }
1290
- function execute(statement, action, depth) {
1291
- if (--budget < 0)
1292
- return fail(statement, "source-model-budget");
1293
- if (trial?.completion?.target === statement) {
1294
- completionTargetEvaluations++;
1295
- if (trial.completion.omit)
1296
- return;
1297
- }
1298
- if (ts.isVariableStatement(statement) &&
1299
- statement.declarationList.flags & ts.NodeFlags.Const) {
1300
- for (const d of statement.declarationList.declarations) {
1301
- if (!ts.isIdentifier(d.name) || !d.initializer)
1302
- return fail(d, "unsupported-local-declaration");
1303
- locals.set(d, evaluate(d.initializer, action, depth + 1));
1304
- }
1305
- }
1306
- else if (ts.isExpressionStatement(statement))
1307
- evaluate(statement.expression, action, depth + 1);
1308
- else if (ts.isReturnStatement(statement) &&
1309
- (model.production(statement) || trial?.completion))
1310
- return {
1311
- value: statement.expression
1312
- ? evaluate(statement.expression, action, depth + 1)
1313
- : undefined,
1314
- };
1315
- else if (ts.isIfStatement(statement) &&
1316
- (model.production(statement) || trial?.payload || trial?.completion)) {
1317
- const condition = evaluate(statement.expression, action, depth + 1);
1318
- if (!primitive(condition))
1319
- return fail(statement, "nonprimitive-condition");
1320
- const branch = condition
1321
- ? statement.thenStatement
1322
- : statement.elseStatement;
1323
- if (branch)
1324
- return execute(branch, action, depth + 1);
1325
- }
1326
- else if (ts.isBlock(statement) &&
1327
- (model.production(statement) || trial?.payload || trial?.completion)) {
1328
- for (const child of statement.statements) {
1329
- const returned = execute(child, action, depth + 1);
1330
- if (returned)
1331
- return returned;
1332
- }
1333
- }
1334
- else if (trial?.completion && ts.isThrowStatement(statement)) {
1335
- const value = evaluate(statement.expression, action, depth + 1);
1336
- if (!sourceValue(value))
1337
- return fail(statement, "unsupported-thrown-value");
1338
- throw new ProgramThrow(value, model.location(statement));
1339
- }
1340
- else if (trial?.completion && ts.isTryStatement(statement)) {
1341
- let result;
1342
- let thrown;
1343
- try {
1344
- result = execute(statement.tryBlock, action, depth + 1);
1345
- }
1346
- catch (error) {
1347
- if (!(error instanceof ProgramThrow))
1348
- throw error;
1349
- thrown = error;
1350
- }
1351
- if (thrown && statement.catchClause) {
1352
- const previous = locals;
1353
- locals = new Map(locals);
1354
- try {
1355
- const binding = statement.catchClause.variableDeclaration;
1356
- if (binding)
1357
- bind(binding.name, binding, thrown.value, action, depth + 1);
1358
- thrown = undefined;
1359
- result = execute(statement.catchClause.block, action, depth + 1);
1360
- }
1361
- catch (error) {
1362
- if (!(error instanceof ProgramThrow))
1363
- throw error;
1364
- thrown = error;
1365
- }
1366
- finally {
1367
- locals = previous;
1368
- }
1369
- }
1370
- // An evaluator limitation is NEVER a catchable JavaScript exception.
1371
- // Nor may a finally return conceal an unsupported operation in the try.
1372
- if (statement.finallyBlock) {
1373
- const final = execute(statement.finallyBlock, action, depth + 1);
1374
- if (final)
1375
- return final;
1376
- }
1377
- if (thrown)
1378
- throw thrown;
1379
- return result;
1380
- }
1381
- else if (!ts.isEmptyStatement(statement))
1382
- fail(statement, "unsupported-statement");
1383
- }
1384
- let limitation;
1385
- try {
1386
- if (!(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) ||
1387
- !ts.isBlock(fn.body) ||
1388
- fn.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword))
1389
- fail(fn, "unsupported-test-body");
1390
- // No test branching, loops, async suspension, escaped mocks or opaque calls are
1391
- // accepted before a checked assertion. A later unsupported statement does
1392
- // not invalidate an earlier captured count.
1393
- if ((ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) &&
1394
- ts.isBlock(fn.body))
1395
- for (const statement of fn.body.statements)
1396
- execute(statement, model.location(statement), 0);
1397
- }
1398
- catch (error) {
1399
- if (error instanceof Unsupported)
1400
- limitation = error.message;
1401
- else if (error instanceof ProgramThrow)
1402
- limitation = `uncaught-source-exception-before-selected-assertion at ${error.source}`;
1403
- else if (!(error instanceof ReachedAssertion))
1404
- throw error;
1405
- }
1406
- return {
1407
- checks,
1408
- payloadChecks,
1409
- directReturnChecks,
1410
- completionChecks,
1411
- limitation,
1412
- };
1413
- }
1414
- /** Shared environment gate for scoped source-prefix checks. No earlier test,
1415
- * opaque import or executable module setup can establish hidden state. */
1416
- function firstSynchronousPrefixIssue(ts, fn, production, model) {
1417
- const sf = fn.getSourceFile(), registration = fn.parent.parent;
1418
- if (!ts.isExpressionStatement(registration) || registration.parent !== sf)
1419
- return "registration-shape";
1420
- let found = false;
1421
- for (const statement of sf.statements) {
1422
- if (ts.isImportDeclaration(statement)) {
1423
- const c = statement.importClause;
1424
- if (!c || !ts.isStringLiteralLike(statement.moduleSpecifier))
1425
- return "import-outside-scope";
1426
- if (c.isTypeOnly)
1427
- continue;
1428
- if ([
1429
- "node:test",
1430
- "node:assert/strict",
1431
- "node:assert",
1432
- "assert/strict",
1433
- "assert",
1434
- ].includes(statement.moduleSpecifier.text))
1435
- continue;
1436
- const names = [
1437
- c.name,
1438
- ...(c.namedBindings && ts.isNamedImports(c.namedBindings)
1439
- ? c.namedBindings.elements
1440
- .filter((e) => !e.isTypeOnly)
1441
- .map((e) => e.name)
1442
- : []),
1443
- ].filter((n) => n !== undefined);
1444
- if (!names.length ||
1445
- (c.namedBindings && ts.isNamespaceImport(c.namedBindings)) ||
1446
- names.some((name) => model.declaration(name)?.getSourceFile() !== production))
1447
- return "import-outside-scope";
1448
- continue;
1449
- }
1450
- if (ts.isEmptyStatement(statement) ||
1451
- ts.isInterfaceDeclaration(statement) ||
1452
- ts.isTypeAliasDeclaration(statement))
1453
- continue;
1454
- if (statement === registration) {
1455
- found = true;
1456
- continue;
1457
- }
1458
- if (!found ||
1459
- !ts.isExpressionStatement(statement) ||
1460
- !ts.isCallExpression(statement.expression) ||
1461
- !model.nativeTest(statement.expression) ||
1462
- statement.expression.arguments.length !== 2 ||
1463
- !ts.isStringLiteralLike(statement.expression.arguments[0]) ||
1464
- !ts.isArrowFunction(statement.expression.arguments[1]))
1465
- return "setup-or-earlier-test";
1466
- }
1467
- if (!found)
1468
- return "registration-not-found";
1469
- const peel = (raw) => {
1470
- let e = raw;
1471
- while (ts.isParenthesizedExpression(e) ||
1472
- ts.isAsExpression(e) ||
1473
- ts.isTypeAssertionExpression(e) ||
1474
- ts.isNonNullExpression(e) ||
1475
- ts.isSatisfiesExpression(e))
1476
- e = e.expression;
1477
- return e;
1478
- };
1479
- for (const statement of production.statements) {
1480
- if (ts.isEmptyStatement(statement) ||
1481
- ts.isInterfaceDeclaration(statement) ||
1482
- ts.isTypeAliasDeclaration(statement))
1483
- continue;
1484
- if (ts.isFunctionDeclaration(statement) && statement.body)
1485
- continue;
1486
- if (!ts.isVariableStatement(statement) ||
1487
- !(statement.declarationList.flags & ts.NodeFlags.Const) ||
1488
- statement.declarationList.declarations.some((d) => !ts.isIdentifier(d.name) ||
1489
- !d.initializer ||
1490
- !ts.isArrowFunction(peel(d.initializer))))
1491
- return "production-initialization";
1492
- }
1493
- }
1494
- /** Omit one complete return/throw statement and compare the actual native
1495
- * synchronous completion predicate. Non-rejection is local to this prefix. */
1496
- export function analyzeCompletionSensitivity(ts, fn, assertion, target, model) {
1497
- const base = {
1498
- model: "node-first-test-completion-v1",
1499
- status: "unresolved",
1500
- };
1501
- const limit = (reason) => ({ ...base, reason });
1502
- if (!model.production(target) ||
1503
- !(ts.isThrowStatement(target) || ts.isReturnStatement(target)) ||
1504
- !ts.isArrowFunction(fn) ||
1505
- !ts.isBlock(fn.body) ||
1506
- fn.modifiers?.length ||
1507
- fn.parameters.length ||
1508
- !ts.isExpressionStatement(assertion.parent) ||
1509
- assertion.parent.parent !== fn.body ||
1510
- !ts.isCallExpression(fn.parent) ||
1511
- !model.nativeTest(fn.parent) ||
1512
- fn.parent.arguments.length !== 2 ||
1513
- fn.parent.arguments[1] !== fn ||
1514
- !ts.isStringLiteralLike(fn.parent.arguments[0]) ||
1515
- !model.nativeException?.(assertion) ||
1516
- !assertion.arguments.length ||
1517
- assertion.arguments.length > 3 ||
1518
- (model.nativeException?.(assertion) === "doesNotThrow" &&
1519
- assertion.arguments.length !== 1))
1520
- return limit("completion-assertion-or-target-shape");
1521
- const production = target.getSourceFile();
1522
- const issue = firstSynchronousPrefixIssue(ts, fn, production, model);
1523
- if (issue)
1524
- return limit(`completion-${issue}`);
1525
- const trial = {
1526
- assertion,
1527
- module: production,
1528
- completion: { target, omit: false },
1529
- };
1530
- const originalRun = runMockCounts(ts, fn, model, undefined, trial);
1531
- const original = originalRun.completionChecks.get(assertion);
1532
- if (originalRun.limitation ||
1533
- !original ||
1534
- !["not-rejected", "witnessed-pass"].includes(original.outcome) ||
1535
- !original.targetEvaluations)
1536
- return limit(originalRun.limitation ??
1537
- "completion-original-unavailable-or-target-not-executed");
1538
- const omittedRun = runMockCounts(ts, fn, model, undefined, {
1539
- ...trial,
1540
- completion: { target, omit: true },
1541
- });
1542
- const omitted = omittedRun.completionChecks.get(assertion);
1543
- if (omittedRun.limitation ||
1544
- !omitted ||
1545
- !omitted.targetEvaluations ||
1546
- omitted.method !== original.method)
1547
- return limit(omittedRun.limitation ?? "completion-omission-unavailable");
1548
- return {
1549
- ...base,
1550
- status: "source-checked",
1551
- scope: "first-synchronous-test-prefix",
1552
- assertionSource: model.location(assertion),
1553
- targetSource: model.location(target),
1554
- changeText: target.getText(),
1555
- change: "statement-omitted",
1556
- original,
1557
- omitted,
1558
- };
1559
- }
1560
- /** A direct value question in a declaration-only production module and the first
1561
- * synchronous test prefix. This shares the existing value evaluator; it does not
1562
- * execute source, infer a witness or generalize into whole-suite survival. */
1563
- export function analyzeDirectReturnSensitivity(ts, fn, assertion, target, model) {
1564
- const base = {
1565
- model: "node-first-test-direct-return-v1",
1566
- status: "unresolved",
1567
- };
1568
- const limit = (reason) => ({ ...base, reason });
1569
- const peel = (raw) => {
1570
- let e = raw;
1571
- while (ts.isParenthesizedExpression(e) ||
1572
- ts.isAsExpression(e) ||
1573
- ts.isTypeAssertionExpression(e) ||
1574
- ts.isNonNullExpression(e) ||
1575
- ts.isSatisfiesExpression(e))
1576
- e = e.expression;
1577
- return e;
1578
- };
1579
- if (!model.production(target) ||
1580
- !ts.isArrowFunction(fn) ||
1581
- !ts.isBlock(fn.body) ||
1582
- fn.modifiers?.length ||
1583
- fn.parameters.length ||
1584
- !ts.isExpressionStatement(assertion.parent) ||
1585
- assertion.parent.parent !== fn.body ||
1586
- !ts.isCallExpression(fn.parent) ||
1587
- !model.nativeTest(fn.parent) ||
1588
- fn.parent.arguments.length !== 2 ||
1589
- fn.parent.arguments[1] !== fn ||
1590
- !ts.isStringLiteralLike(fn.parent.arguments[0]) ||
1591
- !["node-same-value", "node-deep-strict-equality"].includes(model.nativePredicate(assertion) ?? "") ||
1592
- assertion.arguments.length !== 2)
1593
- return limit("direct-return-assertion-shape");
1594
- const sf = fn.getSourceFile(), production = target.getSourceFile();
1595
- const scopeIssue = firstSynchronousPrefixIssue(ts, fn, production, model);
1596
- if (scopeIssue)
1597
- return limit(`direct-return-${scopeIssue}`);
1598
- let actual = peel(assertion.arguments[0]);
1599
- const aliases = new Set();
1600
- while (ts.isIdentifier(actual)) {
1601
- const d = model.declaration(actual);
1602
- if (!d ||
1603
- !ts.isVariableDeclaration(d) ||
1604
- aliases.has(d) ||
1605
- !ts.isIdentifier(d.name) ||
1606
- !(d.parent.flags & ts.NodeFlags.Const) ||
1607
- !d.initializer ||
1608
- d.getSourceFile() !== sf ||
1609
- d.getStart() >= assertion.getStart())
1610
- break;
1611
- aliases.add(d);
1612
- actual = peel(d.initializer);
1613
- }
1614
- if (!ts.isCallExpression(actual) ||
1615
- actual.questionDotToken ||
1616
- !ts.isIdentifier(actual.expression) ||
1617
- model.declaration(actual.expression)?.getSourceFile() !== production)
1618
- return limit("direct-return-operand-not-source-call");
1619
- let node = target;
1620
- if (ts.isReturnStatement(node) && node.expression)
1621
- node = peel(node.expression);
1622
- if (ts.isConditionalExpression(node))
1623
- node = node.condition;
1624
- const edits = [];
1625
- if (ts.isBinaryExpression(node) &&
1626
- [
1627
- ts.SyntaxKind.EqualsEqualsEqualsToken,
1628
- ts.SyntaxKind.ExclamationEqualsEqualsToken,
1629
- ].includes(node.operatorToken.kind)) {
1630
- edits.push({ change: "condition-true", value: true }, { change: "condition-false", value: false }, { change: "condition-inverted", value: "invert" });
1631
- }
1632
- else if (node.kind === ts.SyntaxKind.TrueKeyword ||
1633
- node.kind === ts.SyntaxKind.FalseKeyword) {
1634
- edits.push({
1635
- change: "boolean-literal-inverted",
1636
- value: node.kind !== ts.SyntaxKind.TrueKeyword,
1637
- });
1638
- }
1639
- else
1640
- return limit("direct-return-target-shape");
1641
- const expression = node;
1642
- const trial = {
1643
- assertion,
1644
- module: production,
1645
- directReturn: { call: actual, target: expression },
1646
- };
1647
- const originalRun = runMockCounts(ts, fn, model, undefined, trial);
1648
- const original = originalRun.directReturnChecks.get(assertion);
1649
- if (originalRun.limitation ||
1650
- !original ||
1651
- original.outcome !== "not-rejected")
1652
- return limit(originalRun.limitation ?? "direct-return-original-unavailable");
1653
- const variants = edits.map((edit) => {
1654
- const changed = runMockCounts(ts, fn, model, undefined, {
1655
- ...trial,
1656
- condition: { node: expression, value: edit.value },
1657
- });
1658
- const check = changed.directReturnChecks.get(assertion);
1659
- if (changed.limitation ||
1660
- !check ||
1661
- check.predicate !== original.predicate ||
1662
- JSON.stringify(check.expected) !== JSON.stringify(original.expected) ||
1663
- check.callSource !== original.callSource)
1664
- return {
1665
- change: edit.change,
1666
- status: "unresolved",
1667
- reason: changed.limitation ?? "direct-return-changed-unavailable",
1668
- };
1669
- return { change: edit.change, status: "source-checked", check };
1670
- });
1671
- return {
1672
- ...base,
1673
- status: "source-checked",
1674
- scope: "first-synchronous-test-prefix",
1675
- assertionSource: model.location(assertion),
1676
- targetSource: model.location(target),
1677
- changeSource: model.location(expression),
1678
- changeText: expression.getText(),
1679
- original,
1680
- variants,
1681
- };
1682
- }
1683
- /** A hint selects this small proof recipe, never permission to assume shared state safe.
1684
- * The first synchronous callback has no earlier caller of its imported module in
1685
- * this closed test file. Normal isolated Node test loading/scheduling and unmodified
1686
- * built-ins are model assumptions, not facts proved by the source inspection.
1687
- */
1688
- export function analyzeFirstTestOmission(syntax, fn, assertion, call, model, row) {
1689
- const ts = syntax;
1690
- const base = {
1691
- model: "node-first-test-call-omission-v1",
1692
- status: "unresolved",
1693
- };
1694
- const limit = (reason) => ({ ...base, reason });
1695
- const sf = fn.getSourceFile(), production = call.getSourceFile();
1696
- const parent = call.parent;
1697
- if (!ts.isArrowFunction(parent) ||
1698
- parent.body !== call ||
1699
- parent.modifiers?.length ||
1700
- parent.parameters.some((p) => !ts.isIdentifier(p.name) || p.initializer) ||
1701
- !ts.isPropertyAccessExpression(call.expression) ||
1702
- !model.globalConsole(call.expression.expression) ||
1703
- !["log", "error"].includes(call.expression.name.text) ||
1704
- !model.production(call))
1705
- return limit("omission-target-not-direct-console-callback");
1706
- if (!ts.isArrowFunction(fn) ||
1707
- !ts.isBlock(fn.body) ||
1708
- fn.modifiers?.length ||
1709
- fn.parameters.length !== 1 ||
1710
- !ts.isIdentifier(fn.parameters[0].name) ||
1711
- fn.parameters[0].initializer ||
1712
- fn.parameters[0].dotDotDotToken ||
1713
- !ts.isExpressionStatement(assertion.parent) ||
1714
- assertion.parent.parent !== fn.body ||
1715
- !ts.isCallExpression(fn.parent) ||
1716
- !model.nativeTest(fn.parent) ||
1717
- fn.parent.arguments.length !== 2 ||
1718
- fn.parent.arguments[1] !== fn ||
1719
- model.nativePredicate(assertion) !== "node-same-value")
1720
- return limit("omission-requires-direct-synchronous-count-assertion");
1721
- const registration = fn.parent;
1722
- let registrationStatement = registration.parent;
1723
- while (registrationStatement.parent && registrationStatement.parent !== sf)
1724
- registrationStatement = registrationStatement.parent;
1725
- if (row && row.evidence.rowIndex !== 0)
1726
- return limit("omission-not-first-source-row");
1727
- if (!row &&
1728
- (!ts.isExpressionStatement(registrationStatement) ||
1729
- registrationStatement.expression !== registration ||
1730
- !ts.isStringLiteralLike(registration.arguments[0])))
1731
- return limit("omission-unsupported-registration");
1732
- // Only native imports and the selected production module may initialize before
1733
- // registration. A type-only import is erased; an unrelated import is not inert.
1734
- const erasedImport = (s) => {
1735
- const clause = s.importClause;
1736
- if (!clause)
1737
- return false;
1738
- if (clause.isTypeOnly)
1739
- return true;
1740
- const bindings = clause.namedBindings;
1741
- return (!clause.name &&
1742
- !!bindings &&
1743
- ts.isNamedImports(bindings) &&
1744
- bindings.elements.length > 0 &&
1745
- bindings.elements.every((e) => {
1746
- const d = model.declaration(e.name);
1747
- return (e.isTypeOnly ||
1748
- (!!d &&
1749
- (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d))));
1750
- }));
1751
- };
1752
- const nativeImports = new Set([
1753
- "node:test",
1754
- "node:assert/strict",
1755
- "node:assert",
1756
- "assert/strict",
1757
- "assert",
1758
- ]);
1759
- let found = false;
1760
- for (const s of sf.statements) {
1761
- if (ts.isImportDeclaration(s)) {
1762
- if (erasedImport(s))
1763
- continue;
1764
- if (!ts.isStringLiteralLike(s.moduleSpecifier) || !s.importClause)
1765
- return limit("omission-test-import-outside-scope");
1766
- if (nativeImports.has(s.moduleSpecifier.text))
1767
- continue;
1768
- const c = s.importClause;
1769
- const names = [
1770
- c.name,
1771
- ...(c.namedBindings && ts.isNamedImports(c.namedBindings)
1772
- ? c.namedBindings.elements.map((e) => e.name)
1773
- : []),
1774
- ].filter((n) => n !== undefined);
1775
- if (!names.length ||
1776
- (c.namedBindings && ts.isNamespaceImport(c.namedBindings)) ||
1777
- names.some((n) => model.declaration(n)?.getSourceFile() !== production))
1778
- return limit("omission-test-import-outside-scope");
1779
- continue;
1780
- }
1781
- if (ts.isEmptyStatement(s) ||
1782
- ts.isInterfaceDeclaration(s) ||
1783
- ts.isTypeAliasDeclaration(s))
1784
- continue;
1785
- if (row &&
1786
- ts.isVariableStatement(s) &&
1787
- ts.isForOfStatement(registrationStatement)) {
1788
- const iterable = registrationStatement.expression;
1789
- if (s.declarationList.declarations.length === 1 &&
1790
- model.declaration(iterable) === s.declarationList.declarations[0])
1791
- continue;
1792
- }
1793
- if (s === registrationStatement) {
1794
- if (found)
1795
- return limit("omission-not-first-test");
1796
- found = true;
1797
- continue;
1798
- }
1799
- // Later registrations cannot execute until this synchronous callback finishes.
1800
- // Hooks, options, top-level calls and arbitrary registration expressions are not accepted.
1801
- if (found &&
1802
- ts.isExpressionStatement(s) &&
1803
- ts.isCallExpression(s.expression) &&
1804
- model.nativeTest(s.expression) &&
1805
- s.expression.arguments.length === 2 &&
1806
- ts.isStringLiteralLike(s.expression.arguments[0]) &&
1807
- ts.isArrowFunction(s.expression.arguments[1]))
1808
- continue;
1809
- return limit("omission-test-setup-or-earlier-registration");
1810
- }
1811
- if (!found)
1812
- return limit("omission-registration-not-in-module");
1813
- for (const s of production.statements) {
1814
- if (ts.isExportDeclaration(s))
1815
- return limit("omission-production-reexport");
1816
- if (ts.isImportDeclaration(s) &&
1817
- !erasedImport(s) &&
1818
- !(ts.isStringLiteralLike(s.moduleSpecifier) &&
1819
- s.moduleSpecifier.text === "node:util"))
1820
- return limit("omission-production-import-outside-scope");
1821
- }
1822
- const original = runMockCounts(ts, fn, model, row, {
1823
- assertion,
1824
- module: production,
1825
- });
1826
- const before = original.checks.get(assertion);
1827
- if (!before ||
1828
- original.limitation ||
1829
- before.observedCount !== before.expectedCount)
1830
- return limit(original.limitation ?? "omission-original-count-not-established");
1831
- const calleeSource = model.location(call);
1832
- if (!before.calls?.some((c) => c.source === calleeSource))
1833
- return limit("omission-target-not-in-selected-history");
1834
- const changed = runMockCounts(ts, fn, model, row, {
1835
- assertion,
1836
- module: production,
1837
- omit: call,
1838
- });
1839
- const after = changed.checks.get(assertion);
1840
- if (!after ||
1841
- changed.limitation ||
1842
- after.instance !== before.instance ||
1843
- after.expectedCount !== before.expectedCount)
1844
- return limit(changed.limitation ?? "omission-changed-count-not-established");
1845
- return {
1846
- ...base,
1847
- status: "source-checked",
1848
- scope: "first-synchronous-test",
1849
- outcome: after.observedCount === after.expectedCount ? "not-rejected" : "rejected",
1850
- assertionSource: model.location(assertion),
1851
- callSource: calleeSource,
1852
- callbackSource: model.location(parent),
1853
- instance: before.instance,
1854
- expectedCount: before.expectedCount,
1855
- originalCount: before.observedCount,
1856
- omittedCount: after.observedCount,
1857
- };
1858
- }
1859
- /** Allocation-specific permission for a closed, synchronous, nonescaping test
1860
- * module. This is not a general readonly inference from `const`. Native APIs,
1861
- * isolated module loading and pristine prototypes remain model assumptions. */
1862
- function closedCountScope(ts, sf, prod, model) {
1863
- const reject = (why) => {
1864
- throw new Error(why);
1865
- };
1866
- let budget = 32768;
1867
- const walk = (n, f) => {
1868
- if (--budget < 0)
1869
- reject("scope-budget");
1870
- f(n);
1871
- ts.forEachChild(n, (c) => walk(c, f));
1872
- };
1873
- const peel = (raw) => {
1874
- let e = raw;
1875
- while (ts.isParenthesizedExpression(e) ||
1876
- ts.isAsExpression(e) ||
1877
- ts.isSatisfiesExpression(e) ||
1878
- ts.isNonNullExpression(e))
1879
- e = e.expression;
1880
- return e;
1881
- };
1882
- const local = (n) => {
1883
- const d = model.declaration(n);
1884
- return d?.getSourceFile().isDeclarationFile ? undefined : d;
1885
- };
1886
- const erased = (s) => {
1887
- const c = s.importClause;
1888
- return (!!c &&
1889
- (c.isTypeOnly ||
1890
- (!c.name &&
1891
- c.namedBindings &&
1892
- ts.isNamedImports(c.namedBindings) &&
1893
- c.namedBindings.elements.every((e) => {
1894
- const d = local(e.name);
1895
- return (e.isTypeOnly ||
1896
- (!!d &&
1897
- (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d))));
1898
- }))));
1899
- };
1900
- try {
1901
- const allocations = new Set();
1902
- const shared = new Set();
1903
- const calls = [];
1904
- let factory, factoryBinding;
1905
- for (const s of prod.statements) {
1906
- if (ts.isInterfaceDeclaration(s) ||
1907
- ts.isTypeAliasDeclaration(s) ||
1908
- ts.isEmptyStatement(s))
1909
- continue;
1910
- if (ts.isImportDeclaration(s)) {
1911
- if (erased(s))
1912
- continue;
1913
- if (!ts.isStringLiteralLike(s.moduleSpecifier) ||
1914
- s.moduleSpecifier.text !== "node:util" ||
1915
- !s.importClause?.name ||
1916
- s.importClause.namedBindings)
1917
- reject("scope-production-import");
1918
- continue;
1919
- }
1920
- if (!ts.isVariableStatement(s) ||
1921
- !(s.declarationList.flags & ts.NodeFlags.Const))
1922
- return { reason: "scope-production-binding" };
1923
- for (const d of s.declarationList.declarations) {
1924
- if (!ts.isIdentifier(d.name) || !d.initializer)
1925
- return { reason: "scope-production-binding" };
1926
- const e = peel(d.initializer);
1927
- if (ts.isObjectLiteralExpression(e)) {
1928
- allocations.add(e);
1929
- shared.add(d);
1930
- }
1931
- if (s.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
1932
- if (factory || !ts.isArrowFunction(e))
1933
- return { reason: "scope-single-factory" };
1934
- factory = e;
1935
- factoryBinding = d;
1936
- }
1937
- }
1938
- }
1939
- if (!factory || !shared.size)
1940
- return { reason: "scope-private-allocations" };
1941
- const bannedNames = new Set([
1942
- "eval",
1943
- "Function",
1944
- "require",
1945
- "globalThis",
1946
- "global",
1947
- "__proto__",
1948
- "constructor",
1949
- "prototype",
1950
- "toString",
1951
- "valueOf",
1952
- "Symbol",
1953
- ]);
1954
- const noMutation = (n) => {
1955
- if ((ts.isBinaryExpression(n) &&
1956
- n.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
1957
- n.operatorToken.kind <= ts.SyntaxKind.LastAssignment) ||
1958
- ts.isDeleteExpression(n) ||
1959
- ts.isPostfixUnaryExpression(n) ||
1960
- (ts.isPrefixUnaryExpression(n) &&
1961
- [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(n.operator)) ||
1962
- ts.isNewExpression(n) ||
1963
- ts.isAwaitExpression(n) ||
1964
- ts.isYieldExpression(n) ||
1965
- ts.isTaggedTemplateExpression(n) ||
1966
- n.kind === ts.SyntaxKind.ThisKeyword ||
1967
- ts.isGetAccessorDeclaration(n) ||
1968
- ts.isSetAccessorDeclaration(n) ||
1969
- ts.isComputedPropertyName(n) ||
1970
- (ts.isIdentifier(n) && bannedNames.has(n.text)) ||
1971
- ((ts.isArrowFunction(n) || ts.isFunctionExpression(n)) &&
1972
- (!!n.modifiers?.length ||
1973
- ("asteriskToken" in n && !!n.asteriskToken))))
1974
- reject("scope-effect-or-dynamic-escape");
1975
- };
1976
- walk(prod, (n) => {
1977
- noMutation(n);
1978
- if (ts.isCallExpression(n))
1979
- calls.push(n);
1980
- if (ts.isElementAccessExpression(n))
1981
- reject("scope-computed-production-read");
1982
- });
1983
- walk(sf, noMutation);
1984
- const origins = (raw, active = new Set()) => {
1985
- if (!raw || --budget < 0 || active.has(raw))
1986
- return;
1987
- const e = peel(raw), next = new Set(active).add(raw);
1988
- if (ts.isArrowFunction(e))
1989
- return [e];
1990
- if (ts.isIdentifier(e)) {
1991
- const d = local(e);
1992
- if (d &&
1993
- ts.isVariableDeclaration(d) &&
1994
- d.initializer &&
1995
- d.parent.flags & ts.NodeFlags.Const)
1996
- return origins(d.initializer, next);
1997
- if (d &&
1998
- ts.isBindingElement(d) &&
1999
- ts.isObjectBindingPattern(d.parent) &&
2000
- ts.isParameter(d.parent.parent)) {
2001
- const p = d.parent.parent, owner = p.parent, binding = owner.parent;
2002
- if (!ts.isArrowFunction(owner) ||
2003
- owner === factory ||
2004
- !ts.isVariableDeclaration(binding) ||
2005
- binding.initializer !== owner)
2006
- return;
2007
- let direct = true;
2008
- walk(prod, (n) => {
2009
- if (ts.isIdentifier(n) &&
2010
- local(n) === binding &&
2011
- n !== binding.name &&
2012
- !(ts.isCallExpression(n.parent) && n.parent.expression === n))
2013
- direct = false;
2014
- });
2015
- if (!direct)
2016
- return;
2017
- const sites = calls.filter((c) => local(c.expression) === binding), found = [];
2018
- if (!sites.length)
2019
- return;
2020
- const key = d.propertyName ?? d.name;
2021
- if (!(ts.isIdentifier(key) || ts.isStringLiteralLike(key)))
2022
- return;
2023
- for (const c of sites) {
2024
- const arg = c.arguments[owner.parameters.indexOf(p)] ?? p.initializer;
2025
- if (!arg)
2026
- return;
2027
- const obj = peel(arg);
2028
- if (!ts.isObjectLiteralExpression(obj))
2029
- return;
2030
- let value = d.initializer;
2031
- const names = new Set();
2032
- for (const member of obj.properties) {
2033
- if (!ts.isPropertyAssignment(member) ||
2034
- !(ts.isIdentifier(member.name) ||
2035
- ts.isStringLiteralLike(member.name)) ||
2036
- names.has(member.name.text))
2037
- return;
2038
- names.add(member.name.text);
2039
- if (member.name.text === key.text)
2040
- value = member.initializer;
2041
- }
2042
- const targets = origins(value, next);
2043
- if (!targets)
2044
- return;
2045
- found.push(...targets);
2046
- }
2047
- return found;
2048
- }
2049
- }
2050
- if (ts.isCallExpression(e)) {
2051
- const targets = origins(e.expression, next), result = [];
2052
- if (!targets)
2053
- return;
2054
- for (const f of targets) {
2055
- let body;
2056
- if (!ts.isBlock(f.body))
2057
- body = f.body;
2058
- else if (f.body.statements.length === 1 &&
2059
- ts.isReturnStatement(f.body.statements[0]))
2060
- body = f.body.statements[0].expression;
2061
- const target = origins(body, next);
2062
- if (!target)
2063
- return;
2064
- result.push(...target);
2065
- }
2066
- return result;
2067
- }
2068
- return;
2069
- };
2070
- const methods = new Set(), sharedMethods = new Set();
2071
- for (const obj of allocations)
2072
- for (const prop of obj.properties) {
2073
- if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
2074
- return { reason: "scope-shared-member" };
2075
- const targets = origins(prop.initializer);
2076
- if (!targets?.length)
2077
- return { reason: "scope-method-origin" };
2078
- methods.add(prop.name.text);
2079
- targets.forEach((t) => sharedMethods.add(t));
2080
- }
2081
- const callOrigins = new Map(calls.map((c) => [c, origins(c.expression)]));
2082
- const nativeMap = (c) => {
2083
- if (!ts.isPropertyAccessExpression(c.expression) ||
2084
- c.expression.name.text !== "map" ||
2085
- c.arguments.length !== 1 ||
2086
- !ts.isArrowFunction(c.arguments[0]))
2087
- return false;
2088
- const p = local(c.expression.expression);
2089
- if (!p ||
2090
- !ts.isParameter(p) ||
2091
- !ts.isIdentifier(p.name) ||
2092
- !ts.isArrowFunction(p.parent))
2093
- return false;
2094
- const owner = p.parent;
2095
- if (owner === factory || sharedMethods.has(owner))
2096
- return false;
2097
- const sites = calls.filter((call) => callOrigins.get(call)?.includes(owner));
2098
- return (sites.length > 0 &&
2099
- sites.every((call) => {
2100
- const arg = call.arguments[owner.parameters.indexOf(p)];
2101
- if (!arg)
2102
- return false;
2103
- const e = peel(arg), d = local(e);
2104
- return (ts.isArrayLiteralExpression(e) ||
2105
- (!!d &&
2106
- ts.isParameter(d) &&
2107
- !!d.dotDotDotToken &&
2108
- ts.isIdentifier(d.name)));
2109
- }));
2110
- };
2111
- walk(prod, (n) => {
2112
- if (ts.isIdentifier(n) &&
2113
- shared.has(local(n)) &&
2114
- n !== local(n).name) {
2115
- let p = n;
2116
- while (ts.isParenthesizedExpression(p.parent) ||
2117
- ts.isConditionalExpression(p.parent))
2118
- p = p.parent;
2119
- if (!ts.isReturnStatement(p.parent))
2120
- reject("scope-shared-object-escape");
2121
- let owner = p.parent;
2122
- while (owner.parent && !ts.isArrowFunction(owner))
2123
- owner = owner.parent;
2124
- if (owner !== factory)
2125
- reject("scope-shared-object-escape");
2126
- }
2127
- if (ts.isIdentifier(n) &&
2128
- local(n) === factoryBinding &&
2129
- n !== factoryBinding?.name)
2130
- reject("scope-factory-reentry");
2131
- if (ts.isCallExpression(n)) {
2132
- const e = n.expression;
2133
- const console = ts.isPropertyAccessExpression(e) &&
2134
- model.globalConsole(e.expression) &&
2135
- ["log", "error"].includes(e.name.text);
2136
- if (!callOrigins.get(n)?.length &&
2137
- !console &&
2138
- !model.nativeInspect?.(n) &&
2139
- !nativeMap(n))
2140
- reject("scope-call-origin");
2141
- }
2142
- });
2143
- const callbacks = [], tables = new Set();
2144
- const register = (s) => {
2145
- if (!ts.isExpressionStatement(s) || !ts.isCallExpression(s.expression))
2146
- return reject("scope-registration");
2147
- const c = s.expression, f = c.arguments[1];
2148
- if (!model.nativeTest(c) ||
2149
- c.arguments.length !== 2 ||
2150
- !ts.isArrowFunction(f) ||
2151
- !ts.isBlock(f.body) ||
2152
- f.parameters.length !== 1 ||
2153
- !ts.isIdentifier(f.parameters[0].name) ||
2154
- f.parameters[0].initializer ||
2155
- f.parameters[0].dotDotDotToken)
2156
- return reject("scope-registration");
2157
- const rows = sourceTestRows(ts, f, model);
2158
- if (rows?.reason || (!rows && !ts.isStringLiteralLike(c.arguments[0])))
2159
- return reject("scope-row-registration");
2160
- if (rows) {
2161
- const loop = c.parent.parent.parent;
2162
- if (!ts.isForOfStatement(loop))
2163
- return reject("scope-row-registration");
2164
- const table = local(loop.expression);
2165
- if (!table || !ts.isVariableDeclaration(table))
2166
- return reject("scope-row-registration");
2167
- tables.add(table);
2168
- }
2169
- callbacks.push(f);
2170
- };
2171
- // Check registrations first so only their proven exclusive literal tables are allowed.
2172
- for (const s of sf.statements) {
2173
- if (ts.isExpressionStatement(s))
2174
- register(s);
2175
- else if (ts.isForOfStatement(s)) {
2176
- if (!ts.isBlock(s.statement) || s.statement.statements.length !== 1)
2177
- return { reason: "scope-row-registration" };
2178
- register(s.statement.statements[0]);
2179
- }
2180
- }
2181
- for (const s of sf.statements) {
2182
- if (ts.isImportDeclaration(s)) {
2183
- if (erased(s))
2184
- continue;
2185
- if (!ts.isStringLiteralLike(s.moduleSpecifier) || !s.importClause)
2186
- return { reason: "scope-test-import" };
2187
- if ([
2188
- "node:test",
2189
- "node:assert/strict",
2190
- "node:assert",
2191
- "assert/strict",
2192
- "assert",
2193
- ].includes(s.moduleSpecifier.text))
2194
- continue;
2195
- const names = s.importClause.namedBindings;
2196
- if (s.importClause.name ||
2197
- !names ||
2198
- !ts.isNamedImports(names) ||
2199
- names.elements.some((e) => local(e.name) !== factoryBinding))
2200
- return { reason: "scope-test-import" };
2201
- }
2202
- else if (ts.isVariableStatement(s)) {
2203
- if (s.declarationList.declarations.some((d) => !tables.has(d)))
2204
- return { reason: "scope-test-setup" };
2205
- }
2206
- else if (!ts.isExpressionStatement(s) &&
2207
- !ts.isForOfStatement(s) &&
2208
- !ts.isEmptyStatement(s))
2209
- return { reason: "scope-test-setup" };
2210
- }
2211
- const literal = (raw) => {
2212
- if (--budget < 0)
2213
- return false;
2214
- const e = peel(raw);
2215
- if (ts.isStringLiteralLike(e) ||
2216
- ts.isNumericLiteral(e) ||
2217
- [
2218
- ts.SyntaxKind.TrueKeyword,
2219
- ts.SyntaxKind.FalseKeyword,
2220
- ts.SyntaxKind.NullKeyword,
2221
- ].includes(e.kind))
2222
- return true;
2223
- if (ts.isArrayLiteralExpression(e))
2224
- return e.elements.every(literal);
2225
- return (ts.isObjectLiteralExpression(e) &&
2226
- e.properties.every((p) => ts.isPropertyAssignment(p) &&
2227
- ts.isIdentifier(p.name) &&
2228
- literal(p.initializer)));
2229
- };
2230
- for (const fn of callbacks) {
2231
- const receivers = new Set(), mocks = new Set();
2232
- const row = sourceTestRows(ts, fn, model)?.rows[0];
2233
- walk(fn.body, (n) => {
2234
- if (!ts.isCallExpression(n))
2235
- return;
2236
- if (local(n.expression) === factoryBinding) {
2237
- const arg = n.arguments[0];
2238
- if (n.arguments.length !== 1 ||
2239
- !ts.isObjectLiteralExpression(arg) ||
2240
- !arg.properties.every((p) => (ts.isPropertyAssignment(p) &&
2241
- ts.isIdentifier(p.name) &&
2242
- literal(p.initializer)) ||
2243
- (ts.isShorthandPropertyAssignment(p) &&
2244
- !p.objectAssignmentInitializer &&
2245
- !!row?.bindings.has(local(p.name)))))
2246
- reject("scope-factory-input");
2247
- if (!ts.isVariableDeclaration(n.parent) ||
2248
- !ts.isIdentifier(n.parent.name) ||
2249
- !(n.parent.parent.flags & ts.NodeFlags.Const))
2250
- return reject("scope-receiver-binding");
2251
- receivers.add(n.parent);
2252
- }
2253
- if (model.nativeMock(n)) {
2254
- const [receiver, method, replacement] = n.arguments;
2255
- if (n.arguments.length !== 3 ||
2256
- !model.globalConsole(receiver) ||
2257
- !ts.isStringLiteralLike(method) ||
2258
- !["log", "error"].includes(method.text) ||
2259
- !ts.isArrowFunction(replacement) ||
2260
- replacement.parameters.length ||
2261
- !ts.isBlock(replacement.body) ||
2262
- replacement.body.statements.length ||
2263
- !ts.isVariableDeclaration(n.parent))
2264
- reject("scope-mock-installation");
2265
- if (ts.isVariableDeclaration(n.parent))
2266
- mocks.add(n.parent);
2267
- }
2268
- });
2269
- walk(fn.body, (n) => {
2270
- if (ts.isIdentifier(n) &&
2271
- receivers.has(local(n)) &&
2272
- n !== local(n).name) {
2273
- const prop = n.parent, call = prop.parent;
2274
- if (!ts.isPropertyAccessExpression(prop) ||
2275
- prop.expression !== n ||
2276
- !methods.has(prop.name.text) ||
2277
- !ts.isCallExpression(call) ||
2278
- call.expression !== prop ||
2279
- !call.arguments.every(literal))
2280
- reject("scope-receiver-escape");
2281
- }
2282
- if (ts.isIdentifier(n) &&
2283
- local(n) === factoryBinding &&
2284
- !(ts.isCallExpression(n.parent) && n.parent.expression === n))
2285
- reject("scope-factory-escape");
2286
- if (!ts.isCallExpression(n))
2287
- return;
2288
- const e = n.expression;
2289
- const source = ts.isPropertyAccessExpression(e) &&
2290
- receivers.has(local(e.expression));
2291
- let root = e;
2292
- while (ts.isPropertyAccessExpression(root))
2293
- root = root.expression;
2294
- const mockRead = mocks.has(local(root)) &&
2295
- ts.isPropertyAccessExpression(e) &&
2296
- ["callCount", "slice"].includes(e.name.text);
2297
- if (!source &&
2298
- !mockRead &&
2299
- !model.nativeMock(n) &&
2300
- !model.nativeAssertion?.(n) &&
2301
- !model.globalString?.(e) &&
2302
- local(e) !== factoryBinding)
2303
- reject("scope-test-call-origin");
2304
- });
2305
- }
2306
- return { allocations, callbacks };
2307
- }
2308
- catch (error) {
2309
- return {
2310
- reason: error instanceof Error ? error.message : "scope-unavailable",
2311
- };
2312
- }
2313
- }
2314
- /** A finite, explicit control-change question, not protection against arbitrary edits. */
2315
- export function analyzeCountSensitivity(ts, fn, assertion, target, model, row) {
2316
- const base = {
2317
- model: "node-closed-count-sensitivity-v1",
2318
- status: "unresolved",
2319
- };
2320
- const limit = (reason) => ({ ...base, reason });
2321
- const production = target.getSourceFile();
2322
- if (!model.production(target) ||
2323
- !ts.isArrowFunction(fn) ||
2324
- !ts.isBlock(fn.body) ||
2325
- assertion.parent.parent !== fn.body ||
2326
- model.nativePredicate(assertion) !== "node-same-value")
2327
- return limit("count-sensitivity-assertion-shape");
2328
- let condition = target;
2329
- if (ts.isReturnStatement(condition) && condition.expression)
2330
- condition = condition.expression;
2331
- if (ts.isConditionalExpression(condition))
2332
- condition = condition.condition;
2333
- if (ts.isIfStatement(condition))
2334
- condition = condition.expression;
2335
- // Exact primitive equality, without getters, calls, overloaded coercion or side effects.
2336
- if (!ts.isBinaryExpression(condition) ||
2337
- ![
2338
- ts.SyntaxKind.EqualsEqualsEqualsToken,
2339
- ts.SyntaxKind.ExclamationEqualsEqualsToken,
2340
- ].includes(condition.operatorToken.kind) ||
2341
- ![condition.left, condition.right].every((e) => ts.isIdentifier(e) ||
2342
- ts.isStringLiteralLike(e) ||
2343
- ts.isNumericLiteral(e)))
2344
- return limit("count-sensitivity-condition-shape");
2345
- const scope = closedCountScope(ts, fn.getSourceFile(), production, model);
2346
- if (!scope.allocations || !scope.callbacks?.includes(fn))
2347
- return limit(scope.reason ?? "count-sensitivity-scope");
2348
- const trial = {
2349
- assertion,
2350
- module: production,
2351
- allocations: scope.allocations,
2352
- };
2353
- const original = runMockCounts(ts, fn, model, row, trial), before = original.checks.get(assertion);
2354
- if (!before ||
2355
- original.limitation ||
2356
- before.observedCount !== before.expectedCount)
2357
- return limit(original.limitation ?? "count-sensitivity-original-unavailable");
2358
- const variants = [];
2359
- for (const value of [true, false, "invert"]) {
2360
- const changed = runMockCounts(ts, fn, model, row, {
2361
- ...trial,
2362
- condition: { node: condition, value },
2363
- });
2364
- const after = changed.checks.get(assertion), change = value === true
2365
- ? "condition-true"
2366
- : value === false
2367
- ? "condition-false"
2368
- : "condition-inverted";
2369
- if (changed.limitation ||
2370
- !after ||
2371
- after.instance !== before.instance ||
2372
- after.expectedCount !== before.expectedCount)
2373
- variants.push({
2374
- change,
2375
- status: "unresolved",
2376
- reason: changed.limitation ?? "count-sensitivity-changed-unavailable",
2377
- });
2378
- else
2379
- variants.push({
2380
- change,
2381
- status: "source-checked",
2382
- count: after.observedCount,
2383
- outcome: after.observedCount === after.expectedCount
2384
- ? "not-rejected"
2385
- : "rejected",
2386
- });
2387
- }
2388
- return {
2389
- ...base,
2390
- status: "source-checked",
2391
- scope: "closed-synchronous-test-module",
2392
- assertionSource: model.location(assertion),
2393
- targetSource: model.location(target),
2394
- conditionSource: model.location(condition),
2395
- conditionText: condition.getText(),
2396
- allocations: [...scope.allocations].map(model.location),
2397
- instance: before.instance,
2398
- expectedCount: before.expectedCount,
2399
- originalCount: before.observedCount,
2400
- variants,
2401
- };
2402
- }
2403
- /** Specific control or native-map callback edits, evaluated only up to an
2404
- * existing selected-argument predicate. No runtime values or oracle labels enter. */
2405
- export function analyzePayloadSensitivity(ts, fn, assertion, target, model, row) {
2406
- const base = {
2407
- model: "node-closed-payload-sensitivity-v2",
2408
- status: "unresolved",
2409
- };
2410
- const limit = (reason) => ({ ...base, reason });
2411
- if (!model.production(target) ||
2412
- !ts.isArrowFunction(fn) ||
2413
- !ts.isBlock(fn.body) ||
2414
- ![
2415
- "node-same-value",
2416
- "node-deep-strict-equality",
2417
- "node-literal-regexp",
2418
- ].includes(model.nativePredicate(assertion) ?? ""))
2419
- return limit("payload-sensitivity-assertion-shape");
2420
- let ancestor = assertion.parent;
2421
- while (ancestor !== fn && !ts.isSourceFile(ancestor)) {
2422
- if (ts.isArrowFunction(ancestor) || ts.isFunctionExpression(ancestor))
2423
- return limit("payload-nested-assertion");
2424
- ancestor = ancestor.parent;
2425
- }
2426
- if (ancestor !== fn)
2427
- return limit("payload-assertion-outside-test");
2428
- const scope = closedCountScope(ts, fn.getSourceFile(), target.getSourceFile(), model);
2429
- if (!scope.allocations || !scope.callbacks?.includes(fn))
2430
- return limit(scope.reason ?? "payload-sensitivity-scope");
2431
- let node = target;
2432
- if (ts.isReturnStatement(node) && node.expression)
2433
- node = node.expression;
2434
- if (ts.isConditionalExpression(node))
2435
- node = node.condition;
2436
- const edits = [];
2437
- let changeNode = node;
2438
- if (ts.isBinaryExpression(node) &&
2439
- [
2440
- ts.SyntaxKind.EqualsEqualsEqualsToken,
2441
- ts.SyntaxKind.ExclamationEqualsEqualsToken,
2442
- ].includes(node.operatorToken.kind) &&
2443
- [node.left, node.right].every((e) => ts.isIdentifier(e) ||
2444
- ts.isStringLiteralLike(e) ||
2445
- ts.isNumericLiteral(e) ||
2446
- (ts.isTypeOfExpression(e) && ts.isIdentifier(e.expression)))) {
2447
- for (const value of [true, false, "invert"])
2448
- edits.push({
2449
- change: value === true
2450
- ? "condition-true"
2451
- : value === false
2452
- ? "condition-false"
2453
- : "condition-inverted",
2454
- trial: { condition: { node, value } },
2455
- });
2456
- }
2457
- else if (ts.isCallExpression(node) &&
2458
- ts.isPropertyAccessExpression(node.expression) &&
2459
- node.expression.name.text === "map" &&
2460
- node.arguments.length === 1 &&
2461
- ts.isArrowFunction(node.arguments[0]) &&
2462
- ts.isBlock(node.arguments[0].body)) {
2463
- // Native array origin is established by scope and the interpreter, not spelling.
2464
- changeNode = node.arguments[0].body;
2465
- edits.push({
2466
- change: "map-callback-empty",
2467
- trial: { emptyMapCallback: node.arguments[0] },
2468
- });
2469
- }
2470
- else
2471
- return limit("payload-sensitivity-target-shape");
2472
- const trial = {
2473
- assertion,
2474
- module: target.getSourceFile(),
2475
- allocations: scope.allocations,
2476
- payload: true,
2477
- };
2478
- const originalRun = runMockCounts(ts, fn, model, row, {
2479
- ...trial,
2480
- originalWitness: true,
2481
- }), original = originalRun.payloadChecks.get(assertion);
2482
- if (originalRun.limitation ||
2483
- !original ||
2484
- !["not-rejected", "witnessed-pass"].includes(original.outcome))
2485
- return limit(originalRun.limitation ?? "payload-original-predicate-unavailable");
2486
- const variants = edits.map((edit) => {
2487
- const run = runMockCounts(ts, fn, model, row, {
2488
- ...trial,
2489
- ...edit.trial,
2490
- }), check = run.payloadChecks.get(assertion);
2491
- if (run.limitation ||
2492
- !check ||
2493
- check.predicate !== original.predicate ||
2494
- JSON.stringify(check.expected) !== JSON.stringify(original.expected) ||
2495
- check.projection.instance !== original.projection.instance ||
2496
- check.projection.argumentIndex !== original.projection.argumentIndex ||
2497
- check.projection.readAt !== original.projection.readAt)
2498
- return {
2499
- change: edit.change,
2500
- status: "unresolved",
2501
- reason: run.limitation ?? "payload-changed-predicate-unavailable",
2502
- };
2503
- return { change: edit.change, status: "source-checked", check };
2504
- });
2505
- return {
2506
- ...base,
2507
- status: "source-checked",
2508
- scope: "closed-synchronous-test-module",
2509
- assertionSource: model.location(assertion),
2510
- targetSource: model.location(target),
2511
- changeSource: model.location(changeNode),
2512
- changeText: changeNode.getText(),
2513
- allocations: [...scope.allocations].map(model.location),
2514
- original,
2515
- variants,
2516
- };
2517
- }