@orangepro/orangepro-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/local/agentWorkflow.js +81 -0
  4. package/dist/local/aiGraph/links.js +635 -0
  5. package/dist/local/analyze/analyzer.js +2129 -0
  6. package/dist/local/analyze/behaviorContracts.js +169 -0
  7. package/dist/local/analyze/boilerplate.js +42 -0
  8. package/dist/local/analyze/callGraph.js +458 -0
  9. package/dist/local/analyze/classify.js +219 -0
  10. package/dist/local/analyze/clustering.js +357 -0
  11. package/dist/local/analyze/confirm.js +2422 -0
  12. package/dist/local/analyze/coverage.js +518 -0
  13. package/dist/local/analyze/coverageArtifacts.js +607 -0
  14. package/dist/local/analyze/frameworks.js +115 -0
  15. package/dist/local/analyze/linkage/conventions.js +160 -0
  16. package/dist/local/analyze/parseCache.js +164 -0
  17. package/dist/local/analyze/selfAssert.js +53 -0
  18. package/dist/local/analyze/symbols.js +430 -0
  19. package/dist/local/analyze/testLayer.js +135 -0
  20. package/dist/local/analyze/treeSitter/engine.js +1253 -0
  21. package/dist/local/analyze/treeSitter/languages.js +101 -0
  22. package/dist/local/autoProve.js +620 -0
  23. package/dist/local/cli.js +1468 -0
  24. package/dist/local/cliArgs.js +112 -0
  25. package/dist/local/corpusScope.js +162 -0
  26. package/dist/local/enrich/csv.js +348 -0
  27. package/dist/local/enrich/index.js +43 -0
  28. package/dist/local/enrich/markdown.js +193 -0
  29. package/dist/local/explain/explain.js +91 -0
  30. package/dist/local/exportCli.js +26 -0
  31. package/dist/local/flows/flowWalker.js +215 -0
  32. package/dist/local/flows/llmFlowDiscovery.js +567 -0
  33. package/dist/local/freshness/changed.js +280 -0
  34. package/dist/local/freshness/manifest.js +35 -0
  35. package/dist/local/freshness/status.js +30 -0
  36. package/dist/local/gaps/gaps.js +114 -0
  37. package/dist/local/generate/buckets.js +73 -0
  38. package/dist/local/generate/compareJudge.js +124 -0
  39. package/dist/local/generate/compareReport.js +538 -0
  40. package/dist/local/generate/compareScore.js +105 -0
  41. package/dist/local/generate/deriveImports.js +91 -0
  42. package/dist/local/generate/generator.js +2586 -0
  43. package/dist/local/generate/prompt.js +144 -0
  44. package/dist/local/generate/promptV5.js +438 -0
  45. package/dist/local/generate/providers.js +400 -0
  46. package/dist/local/generate/runHints.js +304 -0
  47. package/dist/local/graph/citations.js +73 -0
  48. package/dist/local/graph/confirmable.js +72 -0
  49. package/dist/local/graph/factories.js +210 -0
  50. package/dist/local/graph/ontology.js +18 -0
  51. package/dist/local/interactive.js +53 -0
  52. package/dist/local/jobs/jobStore.js +80 -0
  53. package/dist/local/jobs/notify.js +29 -0
  54. package/dist/local/jobs/runner.js +75 -0
  55. package/dist/local/ledger.js +117 -0
  56. package/dist/local/localConfig.js +112 -0
  57. package/dist/local/mcp.js +548 -0
  58. package/dist/local/operations.js +1749 -0
  59. package/dist/local/pack/coverageReport.js +192 -0
  60. package/dist/local/pack/exporter.js +195 -0
  61. package/dist/local/pack/schema.js +128 -0
  62. package/dist/local/pack/summary.js +127 -0
  63. package/dist/local/pack/validate.js +25 -0
  64. package/dist/local/proofRunnability.js +366 -0
  65. package/dist/local/recipe/dbSqljs.js +255 -0
  66. package/dist/local/reprove/paths.js +13 -0
  67. package/dist/local/reprove/scoped.js +136 -0
  68. package/dist/local/resolve/barrelWalker.js +178 -0
  69. package/dist/local/resolve/exportIndex.js +270 -0
  70. package/dist/local/resolve/importGraph.js +347 -0
  71. package/dist/local/resolve/resolver.js +122 -0
  72. package/dist/local/resolve/resolverCache.js +117 -0
  73. package/dist/local/rtm.js +413 -0
  74. package/dist/local/score/coverage.js +99 -0
  75. package/dist/local/score/doctor.js +67 -0
  76. package/dist/local/score/risk.js +362 -0
  77. package/dist/local/score/score.js +182 -0
  78. package/dist/local/types.js +1 -0
  79. package/dist/local/util/hash.js +16 -0
  80. package/dist/local/util/ids.js +16 -0
  81. package/dist/local/util/progress.js +8 -0
  82. package/dist/local/util/redact.js +39 -0
  83. package/dist/local/util/time.js +1 -0
  84. package/dist/local/util/walk.js +174 -0
  85. package/dist/local/viz/behaviorReportData.js +367 -0
  86. package/dist/local/viz/behaviorReportHtml.js +664 -0
  87. package/dist/local/viz/d3.bundle.js +3 -0
  88. package/dist/local/viz/html.js +1152 -0
  89. package/dist/local/viz/payload.js +525 -0
  90. package/dist/local/workspace.js +99 -0
  91. package/docs/agent-workflow.md +167 -0
  92. package/docs/agents/claude-code.md +43 -0
  93. package/docs/agents/codex.md +52 -0
  94. package/docs/agents/cursor.md +39 -0
  95. package/docs/agents/opencode.md +43 -0
  96. package/docs/agents/vscode.md +34 -0
  97. package/docs/local-proof-kit.md +269 -0
  98. package/package.json +92 -0
  99. package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
  100. package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
  101. package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
  102. package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
  103. package/scripts/spikes/failure-summary.mjs +29 -0
@@ -0,0 +1,2422 @@
1
+ // Confirmed-coverage confirmer (Phase 4.2 / Gate 2). False-confirm-safe by
2
+ // construction: a (test, behavior) pair is CONFIRMED only when the TypeScript
3
+ // TypeChecker proves the test exercises + asserts the behavior's REAL exported
4
+ // binding. Every ambiguity, heuristic, or unresolved hop downgrades to INFERRED
5
+ // (or NONE). Under-confirming is the desired failure mode.
6
+ //
7
+ // The five conjuncts (ALL must hold — see private/spikes/gate-specs-digest.md
8
+ // §confirmed-semantics):
9
+ // 1. Terminal-binding resolution — a runtime import in the test resolves
10
+ // (through renames + barrels, via getSymbolAtLocation+getAliasedSymbol) to
11
+ // the terminal file that DEFINES the behavior's exported binding.
12
+ // 2. Runtime import, not type-only.
13
+ // 3. Runtime reference of that binding (call callee / new / JSX render /
14
+ // render(...) arg) — a bare mention is not a use.
15
+ // 4. The reference is in a live position — NOT a mock factory, string/snapshot,
16
+ // type position, import line, or skipped/disabled block.
17
+ // 5. An assertion (or an allow-listed self-asserting helper) accompanies the
18
+ // runtime use in the same test block.
19
+ //
20
+ // Confirmation NEVER originates from symbols.ts regex, basename-stem matching, or
21
+ // describe-name matching — those can only ever yield INFERRED/NONE. This module
22
+ // is the SOLE producer of hard TESTED_BY/COVERS edges.
23
+ import ts from "typescript";
24
+ import path from "node:path";
25
+ import { loadTsConfigFor, resolveImport } from "../resolve/resolver.js";
26
+ import { walkBarrel } from "../resolve/barrelWalker.js";
27
+ import { isSelfAssertingCallee } from "./selfAssert.js";
28
+ const norm = (p) => path.resolve(p);
29
+ /**
30
+ * Build a TypeChecker Program over a bounded file set (PR-diff-scoped at PR time;
31
+ * the test+source closure for a full analyze — never silently whole-repo). Honors
32
+ * the anchor file's nearest tsconfig (NodeNext OR bundler) so resolution matches
33
+ * the target repo. JSX is preserved and emit/lib-checks are off — we only read
34
+ * symbols, never compile.
35
+ */
36
+ export function buildConfirmProgram(absFiles, anchorFile) {
37
+ const base = loadTsConfigFor(anchorFile).options;
38
+ const options = {
39
+ ...base,
40
+ noEmit: true,
41
+ allowJs: true,
42
+ checkJs: false,
43
+ skipLibCheck: true,
44
+ // Preserve needs no jsx-runtime resolution; tag identities still bind.
45
+ jsx: ts.JsxEmit.Preserve,
46
+ module: base.module ?? ts.ModuleKind.NodeNext,
47
+ moduleResolution: base.moduleResolution ?? ts.ModuleResolutionKind.NodeNext,
48
+ // Don't require @types to be resolvable from the scanned tree.
49
+ types: []
50
+ };
51
+ const rootNames = [...new Set(absFiles.map(norm))];
52
+ const program = ts.createProgram(rootNames, options);
53
+ return { program, checker: program.getTypeChecker() };
54
+ }
55
+ /** Callee text: "expect", "cy.contains", or just "should" for a chained base.
56
+ * Element access with a string-literal key (`describe["skipIf"]`) is normalized to
57
+ * the dot form so it cannot evade the marker sets purely by bracket notation. */
58
+ function calleeText(call) {
59
+ const e = call.expression;
60
+ if (ts.isIdentifier(e))
61
+ return e.text;
62
+ if (ts.isPropertyAccessExpression(e)) {
63
+ const base = ts.isIdentifier(e.expression) ? e.expression.text + "." : "";
64
+ return base + e.name.text;
65
+ }
66
+ if (ts.isElementAccessExpression(e) && ts.isStringLiteralLike(e.argumentExpression)) {
67
+ const base = ts.isIdentifier(e.expression) ? e.expression.text + "." : "";
68
+ return base + e.argumentExpression.text;
69
+ }
70
+ return "";
71
+ }
72
+ const MOCK_CALLEES = new Set([
73
+ "vi.mock",
74
+ "jest.mock",
75
+ "vi.doMock",
76
+ "jest.doMock",
77
+ "vi.mocked",
78
+ "jest.mocked",
79
+ "vi.importActual",
80
+ "jest.requireActual"
81
+ ]);
82
+ const MODULE_MOCK_CALLEES = new Set(["vi.mock", "jest.mock", "vi.doMock", "jest.doMock", "jest.unstable_mockModule"]);
83
+ const RENDER_CALLEES = new Set(["render", "mount", "renderHook", "create"]);
84
+ // Runner markers that make a test/suite block NON-live: skipped, todo, or expected to
85
+ // FAIL (`test.fails`/`it.failing` passing means the behavior is still WRONG), plus the
86
+ // conditional `skipIf`/`runIf` (which we cannot evaluate). Detected ANYWHERE in the
87
+ // callee chain so chained/parametrized forms (`describe.skip.each(...)`) are caught.
88
+ const DISABLED_RUNNER_MARKERS = new Set(["skip", "todo", "fails", "failing", "skipIf", "runIf"]);
89
+ const RUNNER_ROOTS = new Set(["it", "test", "specify", "describe", "context", "fit", "fdescribe"]);
90
+ /** Root identifier + member names along a (possibly chained/parametrized) call's callee:
91
+ * `describe.skip.each([...])(...)` -> {root:"describe", members:{skip,each}}. */
92
+ function runnerCalleeChain(call) {
93
+ const members = new Set();
94
+ let e = call.expression;
95
+ for (;;) {
96
+ if (ts.isPropertyAccessExpression(e)) {
97
+ members.add(e.name.text);
98
+ e = e.expression;
99
+ }
100
+ else if (ts.isElementAccessExpression(e) && ts.isStringLiteralLike(e.argumentExpression)) {
101
+ members.add(e.argumentExpression.text);
102
+ e = e.expression;
103
+ }
104
+ else if (ts.isCallExpression(e)) {
105
+ e = e.expression;
106
+ }
107
+ else
108
+ break;
109
+ }
110
+ return { root: ts.isIdentifier(e) ? e.text : "", members };
111
+ }
112
+ /** True when `call` is a test/suite runner that is skipped / todo / expected-to-fail. */
113
+ function isDisabledRunnerCall(call) {
114
+ const { root, members } = runnerCalleeChain(call);
115
+ if (root === "xit" || root === "xdescribe" || root === "xtest")
116
+ return true;
117
+ if (!RUNNER_ROOTS.has(root))
118
+ return false;
119
+ for (const m of members)
120
+ if (DISABLED_RUNNER_MARKERS.has(m))
121
+ return true;
122
+ return false;
123
+ }
124
+ /** The module specifier of a `vi.mock(...)`/`jest.mock(...)` first arg: a string literal,
125
+ * OR a dynamic `import("./x")` call (`vi.mock(import("./x"), factory)`). */
126
+ function mockSpecifier(arg) {
127
+ if (!arg)
128
+ return null;
129
+ if (ts.isStringLiteralLike(arg))
130
+ return arg.text;
131
+ if (ts.isCallExpression(arg) && arg.expression.kind === ts.SyntaxKind.ImportKeyword) {
132
+ const s = arg.arguments[0];
133
+ if (s && ts.isStringLiteralLike(s))
134
+ return s.text;
135
+ }
136
+ return null;
137
+ }
138
+ // A Jest/Vitest/jest-dom/jest-extended matcher METHOD name. An ALLOW-list of the
139
+ // matcher naming conventions, NOT a denylist of `to*` built-ins: a denylist is
140
+ // provably open-ended (ES keeps adding `to*` value methods — toWellFormed/toSorted/
141
+ // toHex/toBase64/toArray/Temporal.to*), so an unknown `to*` would over-confirm
142
+ // (UNSAFE). These prefixes cover essentially every real matcher (toBe*, toHave*,
143
+ // toEqual*, toContain*, toMatch*, toThrow*, toReturn*, toResolve/Reject, toInclude*,
144
+ // toStartWith/toEndWith, toSatisfy, toStrictEqual) and collide with NO ECMAScript
145
+ // built-in `to*` method, so impostors (.toString/.toFixed/.toHex/.toArray/…)
146
+ // UNDER-confirm — the safe failure mode. A custom matcher with a novel non-`to`
147
+ // name also under-confirms (rare, safe).
148
+ const MATCHER_NAME = /^to(Be|Have|Equal|StrictEqual|Contain|Match|Throw|Return|Resolve|Reject|Include|StartWith|EndWith|Satisfy)/;
149
+ // Matchers whose 2nd+ arguments are MEANINGFUL comparands and must all be observed:
150
+ // the spy call/return families (variadic expected args), toHaveProperty(path, value),
151
+ // and the jest-dom toHaveAttribute(name, value). Every other matcher is single-comparand
152
+ // — a 2nd+ arg is ignored, so a use there is not asserted (`expect(x).toBe(x, use())`);
153
+ // see observedArgs. (An unmodelled custom multi-comparand matcher UNDER-confirms a use
154
+ // in its 2nd+ arg — the safe failure mode.)
155
+ const VARIADIC_MATCHER = /^to(HaveBeenCalledWith|HaveBeenLastCalledWith|HaveBeenNthCalledWith|HaveReturnedWith|HaveLastReturnedWith|HaveNthReturnedWith|ReturnWith|LastReturnedWith|NthReturnedWith|HaveProperty|HaveAttribute)/;
156
+ /**
157
+ * A bare `expect(x)` is NOT an assertion — in Jest/Vitest it records nothing until
158
+ * a matcher runs. Only a complete matcher CALL chain asserts: `expect(x).toBe(y)`,
159
+ * `await expect(p).resolves.toEqual(z)`, `expect(() => f()).toThrow()`,
160
+ * `expect(x).not.toBeNull()`, `render(...); expect(el).toBeVisible()`. Walk up the
161
+ * property-access chain rooted at the `expect(...)` call and return the INVOKED
162
+ * matcher CALL (the CallExpression whose callee property is a {@link MATCHER_NAME}),
163
+ * or null. A bare reference, an un-invoked `.toBe` getter, or a non-matcher call
164
+ * (`.toString()`, `.then()`, `.toHex()`) does NOT qualify.
165
+ *
166
+ * Deliberately Jest/Vitest-shaped (the product's TS/JS target): chai property-style
167
+ * (`expect(x).to.be.true`) and custom non-convention matchers UNDER-confirm — the
168
+ * safe failure mode. The expect() call stays the assertion site (so relatedness
169
+ * reads its argument); the matcher call's args are also observed (it carries the
170
+ * expected/comparand value).
171
+ */
172
+ function expectMatcherCall(expectCall) {
173
+ let n = expectCall;
174
+ while (n.parent) {
175
+ const acc = n.parent;
176
+ let member;
177
+ if (ts.isPropertyAccessExpression(acc) && acc.expression === n)
178
+ member = acc.name.text;
179
+ else if (ts.isElementAccessExpression(acc) && acc.expression === n && ts.isStringLiteralLike(acc.argumentExpression)) {
180
+ member = acc.argumentExpression.text;
181
+ }
182
+ else
183
+ break;
184
+ if (member !== undefined && acc.parent && ts.isCallExpression(acc.parent) && acc.parent.expression === acc && MATCHER_NAME.test(member)) {
185
+ return acc.parent;
186
+ }
187
+ n = acc;
188
+ }
189
+ return null;
190
+ }
191
+ // Modules whose `assert` IS a real assertion. A LOCAL function named `assert`
192
+ // (or any non-imported / non-trusted `assert*`) asserts nothing structurally and
193
+ // must not fake evidence — mirrors the FRAMEWORK trust applied to render/screen.
194
+ // (`node:assert` covers `node:assert/strict`; `assert` covers `assert/strict`.)
195
+ const TRUSTED_ASSERT_PKGS = ["node:assert", "assert", "chai"];
196
+ // How many leading arguments of a node:assert/chai assert call are COMPARANDS (the
197
+ // rest — a trailing failure MESSAGE — is never compared, so a use smuggled there is
198
+ // not asserted). Bare `assert(v[,msg])` / unknown methods default to 1.
199
+ const ASSERT_COMPARANDS = {
200
+ ok: 1,
201
+ equal: 2,
202
+ notEqual: 2,
203
+ strictEqual: 2,
204
+ notStrictEqual: 2,
205
+ deepEqual: 2,
206
+ notDeepEqual: 2,
207
+ deepStrictEqual: 2,
208
+ notDeepStrictEqual: 2,
209
+ match: 2,
210
+ doesNotMatch: 2,
211
+ throws: 1,
212
+ doesNotThrow: 1,
213
+ rejects: 1,
214
+ doesNotReject: 1,
215
+ ifError: 1,
216
+ fail: 0 // assert.fail([message]) — its sole arg is the failure message, never a comparand
217
+ };
218
+ // Matchers / assert methods that INVOKE a callback argument (so a use inside that
219
+ // callback DOES execute and is asserted): `expect(() => f()).toThrow()`,
220
+ // `assert.throws(() => f())`. For all other matchers a function-expression argument
221
+ // is inspected as a value (the function object), not executed.
222
+ const INVOKING_EXPECT_MATCHER = /^(toThrow|toThrowError)/;
223
+ const INVOKING_ASSERT_METHODS = new Set(["throws", "doesNotThrow", "rejects", "doesNotReject"]);
224
+ // Test-runtime wrappers that SYNCHRONOUSLY invoke their callback argument, so a render
225
+ // inside their callback genuinely executes — `act(() => render(<X/>))`, `waitFor(...)`.
226
+ const INVOKING_WRAPPER_CALLEES = new Set(["act", "waitFor", "waitForElementToBeRemoved"]);
227
+ /** The root identifier of a call's callee chain: `assert.equal(...)` / `assert["equal"](...)`
228
+ * -> `assert`. Descends both dot and string-keyed element access (symmetric with calleeText). */
229
+ function calleeRootId(call) {
230
+ let e = call.expression;
231
+ while (ts.isPropertyAccessExpression(e) || (ts.isElementAccessExpression(e) && ts.isStringLiteralLike(e.argumentExpression))) {
232
+ e = e.expression;
233
+ }
234
+ return ts.isIdentifier(e) ? e : null;
235
+ }
236
+ /**
237
+ * The syntactic shape of an assertion call (BEFORE trust/import gating, which the
238
+ * walk applies):
239
+ * - "expect": a complete `expect(...)` matcher CALL chain (bare expect rejected).
240
+ * - "assert": an `assert*(...)` call (gated to a trusted module in the walk).
241
+ * - "selfassert": a self-asserting framework query (getBy…/findBy…/should/…) —
242
+ * observes a RENDER effect only, never a value call (enforced in observesUse).
243
+ */
244
+ function assertionKind(call) {
245
+ const c = calleeText(call);
246
+ if (!c)
247
+ return null;
248
+ if (/(^|\.)expect(\.(soft|poll))?$/.test(c))
249
+ return expectMatcherCall(call) ? "expect" : null;
250
+ if (/(^|\.)assert([.A-Z]|$)/.test(c))
251
+ return "assert";
252
+ return isSelfAssertingCallee(c) ? "selfassert" : null;
253
+ }
254
+ // Test-LEAF runner roots — the callback passed to one of these is a test block, incl.
255
+ // parametrized/chained forms (`it.each(rows)(name, cb)`, `it.only`, `it.skip`). NOT
256
+ // describe/context (grouping). Disabled markers (skip/fails/…) still downgrade via
257
+ // isDisabledRunnerCall in classifyAncestors.
258
+ const TEST_LEAF_ROOTS = new Set(["it", "test", "specify", "fit", "xit"]);
259
+ /**
260
+ * Nearest enclosing TEST CALLBACK — the arrow/function passed to an it()/test()
261
+ * leaf. Returns null when the node is NOT inside any test callback (e.g. module
262
+ * top-level, or a beforeEach). This is the conjunct-5 scope: scoping to the
263
+ * test-runner callback (rather than any arrow, or the whole source file) (a)
264
+ * closes the top-level false-confirm — an unrelated top-level assertion can no
265
+ * longer satisfy a top-level use — and (b) lets a runtime use nested in a
266
+ * `.map(...)` / `waitFor(...)` callback still match an assertion in the SAME
267
+ * it() block (recall), while keeping sibling it() blocks isolated.
268
+ */
269
+ function enclosingTestBlock(node) {
270
+ let n = node.parent;
271
+ while (n) {
272
+ if ((ts.isArrowFunction(n) || ts.isFunctionExpression(n)) && n.parent && ts.isCallExpression(n.parent)) {
273
+ const call = n.parent;
274
+ if (call.arguments.includes(n) && TEST_LEAF_ROOTS.has(runnerCalleeChain(call).root))
275
+ return n;
276
+ }
277
+ n = n.parent;
278
+ }
279
+ return null;
280
+ }
281
+ function classifyAncestors(node) {
282
+ let inImportOrExport = false;
283
+ let inTypeContext = false;
284
+ let inMockFactory = false;
285
+ let inSkipped = false;
286
+ let n = node.parent;
287
+ while (n) {
288
+ if (ts.isImportDeclaration(n) ||
289
+ ts.isImportClause(n) ||
290
+ ts.isImportSpecifier(n) ||
291
+ ts.isNamespaceImport(n) ||
292
+ ts.isExportDeclaration(n) ||
293
+ ts.isExportSpecifier(n)) {
294
+ inImportOrExport = true;
295
+ }
296
+ // Only a TYPE NODE is type context. An `as`/`satisfies`/`<T>` cast is NOT — its
297
+ // EXPRESSION side runs at runtime (`saveUser() as string` calls saveUser); only the
298
+ // cast's `.type` node (a TypeNode, caught here) is type-only.
299
+ if (ts.isTypeNode(n) ||
300
+ ts.isTypeQueryNode(n) ||
301
+ ts.isTypeReferenceNode(n) ||
302
+ ts.isTypeAliasDeclaration(n)) {
303
+ inTypeContext = true;
304
+ }
305
+ if (ts.isCallExpression(n)) {
306
+ if (MOCK_CALLEES.has(calleeText(n)))
307
+ inMockFactory = true;
308
+ if (isDisabledRunnerCall(n))
309
+ inSkipped = true;
310
+ }
311
+ n = n.parent;
312
+ }
313
+ return { inImportOrExport, inTypeContext, inMockFactory, inSkipped };
314
+ }
315
+ /**
316
+ * The runtime-use kind of `id`, or null. `value` = a call/new whose RESULT is a
317
+ * value (`X(...)`, `new X()`); `render` = a JSX instantiation (`<X/>`) whose
318
+ * EFFECT is the rendered DOM. A bare `render(X)` arg is NOT classified here — it
319
+ * is only a render use when the render helper resolves to a real test framework
320
+ * (handled in the walk), since a local `render` may never call its argument.
321
+ */
322
+ function runtimeUseKind(id) {
323
+ const p = id.parent;
324
+ if (!p)
325
+ return null;
326
+ if (ts.isCallExpression(p) && p.expression === id)
327
+ return "value"; // X(...)
328
+ if (ts.isNewExpression(p) && p.expression === id)
329
+ return "value"; // new X(...)
330
+ if ((ts.isJsxOpeningElement(p) || ts.isJsxSelfClosingElement(p)) && p.tagName === id)
331
+ return "render"; // <X/>
332
+ return null;
333
+ }
334
+ function isRuntimeUse(id) {
335
+ return runtimeUseKind(id) !== null;
336
+ }
337
+ /** The call/new/JSX expression that `id` is the head of (the "use expression"). */
338
+ function useExpressionOf(id) {
339
+ return id.parent;
340
+ }
341
+ /** True if `target` is `root` or anywhere in its subtree. */
342
+ function nodeContains(root, target) {
343
+ if (root === target)
344
+ return true;
345
+ let found = false;
346
+ const walk = (n) => {
347
+ if (found)
348
+ return;
349
+ if (n === target) {
350
+ found = true;
351
+ return;
352
+ }
353
+ ts.forEachChild(n, walk);
354
+ };
355
+ ts.forEachChild(root, walk);
356
+ return found;
357
+ }
358
+ // Higher-order methods whose callback's RETURN value flows into the method's result (so
359
+ // a use inside the callback genuinely contributes to the asserted value). ONLY these are
360
+ // descended. EXCLUDED on purpose: forEach (returns undefined), filter/find/some/every
361
+ // (the callback is a PREDICATE — the result holds the ORIGINAL elements, not the
362
+ // callback's return), sort (comparator), finally (return ignored) — descending those
363
+ // would equate "executed" with "asserted" (the use runs but its value is discarded).
364
+ const HOF_CALLBACK_METHODS = new Set(["map", "flatMap", "reduce", "reduceRight", "then", "catch"]);
365
+ // then/catch run their callback only on a real thenable; the rest are array methods.
366
+ const PROMISE_HOF_METHODS = new Set(["then", "catch"]);
367
+ /** Whether `recv.<method>(cb)` actually INVOKES `cb`: only when the receiver is provably a
368
+ * built-in Array/ReadonlyArray/tuple (array methods) or a Promise (then/catch). A custom
369
+ * object with a method merely NAMED `map`/`then` can ignore its callback, so it must not
370
+ * count as invoking (Codex round-6). */
371
+ function hofReceiverInvokes(receiver, method, checker) {
372
+ const t = checker.getNonNullableType(checker.getTypeAtLocation(receiver));
373
+ const constituents = t.isUnion() ? t.types : [t];
374
+ const names = new Set();
375
+ for (const ty of constituents) {
376
+ const sym = ty.getSymbol() ?? ty.aliasSymbol;
377
+ if (sym)
378
+ names.add(sym.getName());
379
+ }
380
+ if (PROMISE_HOF_METHODS.has(method))
381
+ return names.has("Promise");
382
+ if (names.has("Array") || names.has("ReadonlyArray"))
383
+ return true;
384
+ const ic = checker;
385
+ return constituents.some((ty) => Boolean(ic.isArrayType?.(ty) || ic.isTupleType?.(ty)));
386
+ }
387
+ /** True when `fn` is a function-expression passed as a callback to a known invoking
388
+ * higher-order method (`arr.map(fn)` / `promise.then(fn)` / `Array.from(xs, fn)`). When a
389
+ * `checker` is supplied, `arr.map`-style calls require the receiver to be a built-in
390
+ * array/promise — a method merely NAMED map/then on a custom object does NOT invoke. */
391
+ function isInvokedCallback(fn, checker) {
392
+ const p = fn.parent;
393
+ if (!p || !ts.isCallExpression(p) || !p.arguments.includes(fn))
394
+ return false;
395
+ const callee = p.expression;
396
+ if (ts.isPropertyAccessExpression(callee)) {
397
+ if (HOF_CALLBACK_METHODS.has(callee.name.text)) {
398
+ return checker ? hofReceiverInvokes(callee.expression, callee.name.text, checker) : true;
399
+ }
400
+ if (ts.isIdentifier(callee.expression) && callee.expression.text === "Array" && callee.name.text === "from") {
401
+ // `Array.from(source, mapFn)` invokes ONLY arg1 (the map callback); arg0 is the
402
+ // array-like SOURCE, never called. And the `Array` root must be the global, not a
403
+ // local `const Array = { from: … }` spoof.
404
+ if (p.arguments[1] !== fn)
405
+ return false;
406
+ return checker ? isGlobalBinding(callee.expression, checker) : true;
407
+ }
408
+ }
409
+ return false;
410
+ }
411
+ /** True when identifier `id` resolves to a GLOBAL declared only in lib `.d.ts` files (e.g.
412
+ * the built-in `Array`) — a user-declared shadow (`const Array = …`) has a declaration in a
413
+ * real source file and is rejected; an unresolved binding is also rejected (no trust). */
414
+ function isGlobalBinding(id, checker) {
415
+ const sym = checker.getSymbolAtLocation(id);
416
+ const decls = sym?.getDeclarations() ?? [];
417
+ return decls.length > 0 && decls.every((d) => d.getSourceFile().isDeclarationFile);
418
+ }
419
+ /**
420
+ * Does `target`'s VALUE flow to the value of `arg`? Structural containment, with two
421
+ * value-discarding boundaries excluded:
422
+ * - a non-final operand of a comma/sequence expression (`(use(), x)` → only x flows);
423
+ * - a function expression whose body is NOT executed for `arg`'s value. A function is
424
+ * descended only when the matcher invokes it (`intoFns`: toThrow/poll/assert.throws)
425
+ * or it is a callback to a known invoking higher-order method ({@link isInvokedCallback}:
426
+ * `[...].map(u => use(u))`). A function merely STORED — expect's arg
427
+ * (`expect(() => use()).toBeInstanceOf(Function)`), a ternary/logical operand
428
+ * (`expect(cond ? (() => use()) : null)…`), an array/object literal — is inspected as
429
+ * a VALUE (the function object), never run, so its body use is NOT observed.
430
+ * This is the WRAPPED relatedness signal: the use is observed only when its result can
431
+ * reach the asserted value.
432
+ *
433
+ * Known heuristic limit (conjunct 5): still structural, not full dataflow / reachability.
434
+ * Residual over-confirms that need value/return/reachability tracking remain and are
435
+ * deliberately NOT closed — a never-taken branch (`false && use()`, dead ternary arm); a
436
+ * wrapping literal whose matcher asserts a value-INDEPENDENT property
437
+ * (`expect([use(), x]).toHaveLength(2)`, a negated `expect(x).not.toBe(use())`); an IIFE
438
+ * whose return discards the use (`expect((() => { use(); return 1 })()).toBe(1)`); a use in
439
+ * a NESTED un-invoked function under an invoking matcher (`expect(() => () => use()).toThrow()`);
440
+ * a tagged template that drops substitutions. Each is CONSTRUCTED nonsense never seen in
441
+ * real code (the 10-repo sweep is unaffected); closing them would cost recall on genuine
442
+ * invoked callbacks (`[1,2].map(x => use(x))`, `expect(() => use()).toThrow()`).
443
+ */
444
+ function flowsInto(arg, target, intoFns, invoked) {
445
+ if (arg === target)
446
+ return true;
447
+ if (ts.isParenthesizedExpression(arg))
448
+ return flowsInto(arg.expression, target, intoFns, invoked);
449
+ if (ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.CommaToken) {
450
+ return flowsInto(arg.right, target, intoFns, invoked);
451
+ }
452
+ if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
453
+ // A function is executed for arg's value ONLY when it is an invoked HOF callback, or
454
+ // the matcher invokes it (intoFns) AND it is a concise-body arrow `() => use(...)`.
455
+ // A block body (extra statements / an explicit `throw`) is ambiguous — the asserted
456
+ // throw may not be the use (`expect(() => { use(); throw e }).toThrow()`) — so
457
+ // under-confirm. Otherwise the function is a STORED value, never run.
458
+ const conciseInvoked = intoFns && ts.isArrowFunction(arg) && !ts.isBlock(arg.body);
459
+ if (!invoked(arg) && !conciseInvoked)
460
+ return false;
461
+ // Descend ONLY the value-producing nodes (concise body / `return` expressions): a
462
+ // side-effect statement in a block body is DISCARDED, not asserted
463
+ // (`map(() => { use(); return 1 })` asserts 1, not use()).
464
+ return functionValueExpressions(arg).some((v) => flowsInto(v, target, intoFns, invoked));
465
+ }
466
+ let found = false;
467
+ ts.forEachChild(arg, (c) => {
468
+ if (!found && flowsInto(c, target, intoFns, invoked))
469
+ found = true;
470
+ });
471
+ return found;
472
+ }
473
+ /** The expression(s) whose value becomes a function's RETURN value: a concise arrow body,
474
+ * or the block body's top-level `return` expressions (NOT those in nested functions). A
475
+ * side-effect statement's value is discarded and is not included. */
476
+ function functionValueExpressions(fn) {
477
+ if (ts.isArrowFunction(fn) && !ts.isBlock(fn.body))
478
+ return [fn.body];
479
+ const out = [];
480
+ const walk = (n) => {
481
+ if (ts.isReturnStatement(n)) {
482
+ if (n.expression)
483
+ out.push(n.expression);
484
+ return;
485
+ }
486
+ if (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n))
487
+ return;
488
+ ts.forEachChild(n, walk);
489
+ };
490
+ walk(fn.body);
491
+ return out;
492
+ }
493
+ const DOM_QUERY_RE = /^(get|find|query)(All)?By[A-Z]/;
494
+ // Render helpers / DOM-query receivers are trusted ONLY when imported from these.
495
+ const FRAMEWORK_RENDER_PKGS = ["@testing-library/", "enzyme", "@vue/test-utils", "@solidjs/testing-library"];
496
+ const FRAMEWORK_DOM_PKGS = ["@testing-library/", "@vue/test-utils"];
497
+ const NEST_TESTING_PKGS = ["@nestjs/testing"];
498
+ // Packages that export act/waitFor (callback-INVOKING wrappers). A locally-defined `act`
499
+ // that ignores its callback must not make a dead render look live (Codex round-6).
500
+ const INVOKING_WRAPPER_PKGS = ["@testing-library/", "@vue/test-utils", "react-dom", "react", "preact/test-utils"];
501
+ function newState() {
502
+ return {
503
+ importedRuntime: false,
504
+ importedType: false,
505
+ collision: false,
506
+ importSources: [],
507
+ runtimeUses: [],
508
+ nonRuntimeReasons: new Set()
509
+ };
510
+ }
511
+ /**
512
+ * Classify every (behaviorName -> verdict) for one (test, impl) pair in a single
513
+ * AST walk. `behaviorNames` are the exported binding names in `implAbsPath` that
514
+ * are candidate behaviors (the impl file's denominator-eligible CodeSymbols).
515
+ */
516
+ export function confirmBehaviors(ctx, testAbsPath, implAbsPath, behaviorNames) {
517
+ const { program, checker } = ctx;
518
+ const out = new Map();
519
+ const nameSet = new Set(behaviorNames);
520
+ const memberTargetsByClass = new Map();
521
+ for (const name of behaviorNames) {
522
+ const dot = name.indexOf(".");
523
+ if (dot <= 0)
524
+ continue;
525
+ const cls = name.slice(0, dot);
526
+ const list = memberTargetsByClass.get(cls);
527
+ if (list)
528
+ list.push(name);
529
+ else
530
+ memberTargetsByClass.set(cls, [name]);
531
+ }
532
+ const testSf = program.getSourceFile(norm(testAbsPath));
533
+ if (!testSf) {
534
+ for (const n of behaviorNames)
535
+ out.set(n, { verdict: "none", reason: "test file not in the confirmer program" });
536
+ return out;
537
+ }
538
+ const implNorm = norm(implAbsPath);
539
+ // Resolve an identifier's binding to a terminal export name in implAbsPath, or
540
+ // null. Follows aliases (renames, re-export barrels) to the defining file.
541
+ const resolvedTargetName = (id) => {
542
+ let sym = checker.getSymbolAtLocation(id);
543
+ if (!sym)
544
+ return null;
545
+ if (sym.flags & ts.SymbolFlags.Alias) {
546
+ try {
547
+ sym = checker.getAliasedSymbol(sym);
548
+ }
549
+ catch {
550
+ return null; // unresolvable/ambiguous alias — not a single terminal
551
+ }
552
+ }
553
+ const decls = sym.getDeclarations() ?? [];
554
+ if (decls.length === 0)
555
+ return null;
556
+ // Every value declaration must live in the terminal impl file. A symbol whose
557
+ // declarations span multiple files (ambiguous re-export) is NOT a single terminal.
558
+ const declFiles = new Set(decls.map((d) => norm(d.getSourceFile().fileName)));
559
+ if (declFiles.size !== 1 || !declFiles.has(implNorm))
560
+ return null;
561
+ let name = sym.getName();
562
+ if (name === "default") {
563
+ // `export default function Foo(){}` / `class Foo{}` is recorded in the
564
+ // denominator under its DECLARED name (symbols.ts), but the symbol's name is
565
+ // "default". Recover the declared name so the confirmer matches the behavior
566
+ // (otherwise every default export is systematically un-confirmable).
567
+ const valueDecl = sym.valueDeclaration ?? decls[0];
568
+ if (valueDecl && (ts.isFunctionDeclaration(valueDecl) || ts.isClassDeclaration(valueDecl)) && valueDecl.name) {
569
+ name = valueDecl.name.text;
570
+ }
571
+ }
572
+ return nameSet.has(name) || memberTargetsByClass.has(name) ? name : null;
573
+ };
574
+ const states = new Map();
575
+ for (const n of behaviorNames)
576
+ states.set(n, newState());
577
+ const stateFor = (name) => states.get(name);
578
+ const targetNamesForResolved = (name) => {
579
+ const out = [];
580
+ if (nameSet.has(name))
581
+ out.push(name);
582
+ for (const member of memberTargetsByClass.get(name) ?? [])
583
+ out.push(member);
584
+ return out;
585
+ };
586
+ // The ImportDeclaration that declared a local binding identifier (or null).
587
+ const importDeclOf = (id) => {
588
+ const sym = checker.getSymbolAtLocation(id);
589
+ const spec = (sym?.getDeclarations() ?? []).find((d) => ts.isImportSpecifier(d) || ts.isImportClause(d) || ts.isNamespaceImport(d));
590
+ if (!spec)
591
+ return null;
592
+ let n = spec;
593
+ while (n && !ts.isImportDeclaration(n))
594
+ n = n.parent;
595
+ return n && ts.isImportDeclaration(n) ? n : null;
596
+ };
597
+ // The import source of the binding `id` ACTUALLY refers to (per-binding).
598
+ const importSourceOf = (id) => {
599
+ const sym = checker.getSymbolAtLocation(id);
600
+ const spec = (sym?.getDeclarations() ?? []).find((d) => ts.isImportSpecifier(d) || ts.isImportClause(d) || ts.isNamespaceImport(d));
601
+ const decl = importDeclOf(id);
602
+ if (!spec || !decl || !ts.isStringLiteralLike(decl.moduleSpecifier))
603
+ return null;
604
+ const r = resolveImport(decl.moduleSpecifier.text, testAbsPath);
605
+ if (!r.resolvedFileName)
606
+ return null;
607
+ const clauseTypeOnly = !!decl.importClause?.isTypeOnly;
608
+ let importedName = "default";
609
+ let elTypeOnly = false;
610
+ if (ts.isImportSpecifier(spec)) {
611
+ importedName = (spec.propertyName ?? spec.name).text;
612
+ elTypeOnly = !!spec.isTypeOnly;
613
+ }
614
+ else if (ts.isNamespaceImport(spec)) {
615
+ importedName = "*";
616
+ }
617
+ return { file: norm(r.resolvedFileName), importedName, typeOnly: clauseTypeOnly || elTypeOnly };
618
+ };
619
+ // Was the local binding `id` imported from one of `pkgs`? (Trust gate for render
620
+ // helpers / DOM-query receivers so a LOCAL `render`/`screen` cannot fake evidence.)
621
+ const importedFromPkg = (id, pkgs) => {
622
+ const decl = importDeclOf(id);
623
+ if (!decl || !ts.isStringLiteralLike(decl.moduleSpecifier))
624
+ return false;
625
+ const s = decl.moduleSpecifier.text;
626
+ return pkgs.some((p) => (p.endsWith("/") ? s.startsWith(p) : s === p || s.startsWith(p + "/")));
627
+ };
628
+ // An `assert*` call is a real assertion only when its callee root identifier is
629
+ // imported from a trusted assert module — a local/non-imported `assert` is a no-op
630
+ // structurally and must not confirm. (node:assert/strict & assert/strict covered
631
+ // by the prefix match in importedFromPkg.)
632
+ const isTrustedAssert = (call) => {
633
+ const root = calleeRootId(call);
634
+ return !!root && importedFromPkg(root, TRUSTED_ASSERT_PKGS);
635
+ };
636
+ // Conjunct 1 (terminal-binding), per binding: a direct import to impl, or a
637
+ // re-export that the barrel walker follows to exactly ONE covered terminal that
638
+ // IS the impl. Type-only and ambiguous/namespace/default barrels are NOT clean.
639
+ const cleanBindingSource = (src) => {
640
+ if (src.typeOnly)
641
+ return false;
642
+ if (src.file === implNorm)
643
+ return true;
644
+ if (src.importedName === "*" || src.importedName === "default")
645
+ return false;
646
+ const w = walkBarrel(src.file, src.importedName);
647
+ return w.status === "terminal" && w.covered && norm(w.terminalFile ?? "") === implNorm;
648
+ };
649
+ // Conjunct 4: ALL modules resolved from a vi.mock/jest.mock specifier — a
650
+ // binding is mocked if its impl OR its own import-source module (e.g. a
651
+ // re-export barrel the test imports through) is mocked.
652
+ const mockedFiles = new Set();
653
+ // Raw mocked module specifier STRINGS (`vi.mock("@testing-library/react")`) — used to
654
+ // detect a mocked FRAMEWORK package without relying on node_modules resolution.
655
+ const mockedSpecifiers = new Set();
656
+ const assertions = [];
657
+ // Number of framework render()/mount() calls per test block (render-effect ambiguity).
658
+ const renderCallsByBlock = new Map();
659
+ // ---- imports: record runtime/type binding to the terminal impl + collisions ----
660
+ const visitImports = (node) => {
661
+ if (ts.isImportDeclaration(node) && node.importClause && ts.isStringLiteralLike(node.moduleSpecifier)) {
662
+ const clause = node.importClause;
663
+ const clauseTypeOnly = !!clause.isTypeOnly;
664
+ const r = resolveImport(node.moduleSpecifier.text, testAbsPath);
665
+ const specifierFile = r.resolvedFileName ? norm(r.resolvedFileName) : null;
666
+ const consider = (localId, elTypeOnly, importedName) => {
667
+ const directMemberClass = specifierFile === implNorm && memberTargetsByClass.has(importedName) ? importedName : null;
668
+ const resolved = resolvedTargetName(localId) ?? directMemberClass;
669
+ const typeOnly = clauseTypeOnly || elTypeOnly;
670
+ if (resolved) {
671
+ const targetNames = targetNamesForResolved(resolved);
672
+ for (const targetName of targetNames) {
673
+ const st = stateFor(targetName);
674
+ if (typeOnly)
675
+ st.importedType = true;
676
+ else
677
+ st.importedRuntime = true;
678
+ if (specifierFile)
679
+ st.importSources.push({ file: specifierFile, importedName, typeOnly });
680
+ }
681
+ }
682
+ else if (nameSet.has(localId.text)) {
683
+ // A binding whose LOCAL name matches a behavior but resolves elsewhere
684
+ // (helper file) or nowhere (ambiguous barrel): name matches, identity does not.
685
+ stateFor(localId.text).collision = true;
686
+ }
687
+ };
688
+ if (clause.name)
689
+ consider(clause.name, false, "default");
690
+ const named = clause.namedBindings;
691
+ if (named && ts.isNamespaceImport(named))
692
+ consider(named.name, false, "*");
693
+ else if (named && ts.isNamedImports(named)) {
694
+ for (const el of named.elements)
695
+ consider(el.name, !!el.isTypeOnly, (el.propertyName ?? el.name).text);
696
+ }
697
+ }
698
+ ts.forEachChild(node, visitImports);
699
+ };
700
+ visitImports(testSf);
701
+ // The PRE-ALIAS imported name of a binding (`import { screen as scr }` -> "screen"),
702
+ // or undefined for a non-import. Read straight off the import specifier so it works
703
+ // even when the framework package does not resolve in the scanned tree.
704
+ const importedSourceName = (id) => {
705
+ const sym = checker.getSymbolAtLocation(id);
706
+ const spec = (sym?.getDeclarations() ?? []).find((d) => ts.isImportSpecifier(d) || ts.isImportClause(d) || ts.isNamespaceImport(d));
707
+ if (!spec)
708
+ return undefined;
709
+ if (ts.isImportSpecifier(spec))
710
+ return (spec.propertyName ?? spec.name).text;
711
+ if (ts.isNamespaceImport(spec))
712
+ return "*";
713
+ return "default"; // import clause (default import)
714
+ };
715
+ // A render exercise observed by an EFFECT assertion: a DOM query (get/find/queryBy*)
716
+ // whose receiver is a FRAMEWORK-imported `screen`/`within` (a local `screen` object
717
+ // cannot fake this). Match the IMPORTED name, not the local text, so an aliased
718
+ // `import { screen as scr }` (idiomatic testing-library) still counts.
719
+ const isFrameworkDomBinding = (id) => {
720
+ const n = importedSourceName(id);
721
+ return (n === "screen" || n === "within") && importedFromPkg(id, FRAMEWORK_DOM_PKGS);
722
+ };
723
+ const receiverIsFrameworkDom = (expr) => {
724
+ if (ts.isIdentifier(expr))
725
+ return isFrameworkDomBinding(expr);
726
+ if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression))
727
+ return isFrameworkDomBinding(expr.expression);
728
+ return false;
729
+ };
730
+ const isFrameworkQuery = (call) => ts.isPropertyAccessExpression(call.expression) &&
731
+ DOM_QUERY_RE.test(call.expression.name.text) &&
732
+ receiverIsFrameworkDom(call.expression.expression);
733
+ const isFrameworkEffect = (call) => {
734
+ if (isFrameworkQuery(call))
735
+ return true; // self-assert: screen.findByRole(...)
736
+ if (!/(^|\.)expect$|(^|\.)assert([.A-Z]|$)/.test(calleeText(call)))
737
+ return false;
738
+ let ok = false;
739
+ const walk = (n) => {
740
+ if (ok)
741
+ return;
742
+ if (ts.isCallExpression(n) && isFrameworkQuery(n)) {
743
+ ok = true;
744
+ return;
745
+ }
746
+ ts.forEachChild(n, walk);
747
+ };
748
+ for (const arg of call.arguments)
749
+ walk(arg);
750
+ return ok;
751
+ };
752
+ const isNestTestingModuleCall = (call) => {
753
+ const e = call.expression;
754
+ return (ts.isPropertyAccessExpression(e) &&
755
+ e.name.text === "createTestingModule" &&
756
+ ts.isIdentifier(e.expression) &&
757
+ importedFromPkg(e.expression, NEST_TESTING_PKGS));
758
+ };
759
+ // A JSX <X/> tag is a REAL render use only when the element is passed to a framework
760
+ // render()/mount() — a bare `const el = <X/>` just constructs an element object (the
761
+ // component function is never invoked), so it is a value, not a rendered component.
762
+ //
763
+ // Crucially the element must be the DIRECT rendered element. `render(<Shell><X/></Shell>)`
764
+ // does NOT mount <X/>: a wrapper may ignore its children, so a nested tag under another
765
+ // JSX element under-confirms (we cannot prove the wrapper renders it). We confirm only
766
+ // when <X/> is a direct argument of the trusted render call, or when <X/> is the
767
+ // initializer of a `const` that itself flows directly into a trusted render call.
768
+ const elementOfTag = (id) => {
769
+ const p = id.parent;
770
+ if (ts.isJsxSelfClosingElement(p) && p.tagName === id)
771
+ return p;
772
+ if (ts.isJsxOpeningElement(p) && p.tagName === id && ts.isJsxElement(p.parent))
773
+ return p.parent;
774
+ return null;
775
+ };
776
+ const isFrameworkRenderCall = (call) => ts.isIdentifier(call.expression) &&
777
+ RENDER_CALLEES.has(call.expression.text) &&
778
+ importedFromPkg(call.expression, FRAMEWORK_RENDER_PKGS);
779
+ // A function boundary that actually RUNS when the test executes: a callback to a known
780
+ // invoking HOF (.map/.forEach/… via isInvokedCallback), an IIFE, or a test-runtime
781
+ // wrapper that synchronously calls its callback (act/waitFor). A function merely STORED
782
+ // (`const doRender = () => render(<X/>)`) does NOT run, so anything inside it is dead.
783
+ const isInvokedFunctionBoundary = (fn) => {
784
+ if (isInvokedCallback(fn, checker))
785
+ return true;
786
+ const par = fn.parent;
787
+ if (!par)
788
+ return false;
789
+ if (ts.isCallExpression(par) && par.expression === fn)
790
+ return true; // (fn)()
791
+ if (ts.isParenthesizedExpression(par) && par.parent && ts.isCallExpression(par.parent) && par.parent.expression === par)
792
+ return true; // (fn)()
793
+ // A test-runtime wrapper (act/waitFor) invokes its callback ONLY when it is the real
794
+ // imported helper — a local no-op `act` that drops its callback must not count.
795
+ if (ts.isCallExpression(par) && par.arguments.includes(fn)) {
796
+ const e = par.expression;
797
+ if (ts.isIdentifier(e) && INVOKING_WRAPPER_CALLEES.has(e.text) && importedFromPkg(e, INVOKING_WRAPPER_PKGS))
798
+ return true;
799
+ if (ts.isPropertyAccessExpression(e) &&
800
+ INVOKING_WRAPPER_CALLEES.has(e.name.text) &&
801
+ ts.isIdentifier(e.expression) &&
802
+ importedFromPkg(e.expression, INVOKING_WRAPPER_PKGS))
803
+ return true; // React.act(...)
804
+ }
805
+ return false;
806
+ };
807
+ // Whether `node` executes when its enclosing test block runs — the path up to `block`
808
+ // crosses no uninvoked function boundary. A render inside a stored thunk is NOT live.
809
+ const isLivePosition = (node, block) => {
810
+ let n = node.parent;
811
+ while (n && n !== block) {
812
+ if ((ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n)) &&
813
+ !isInvokedFunctionBoundary(n)) {
814
+ return false;
815
+ }
816
+ n = n.parent;
817
+ }
818
+ return n === block;
819
+ };
820
+ // A framework render() call counts only when it actually executes in its test block.
821
+ const renderCallIsLive = (call) => {
822
+ const blk = enclosingTestBlock(call);
823
+ return blk != null && isLivePosition(call, blk);
824
+ };
825
+ // The trusted render call that takes `el` as a DIRECT argument — `render(<X/>)`.
826
+ const frameworkRenderArgCall = (el) => {
827
+ const p = el.parent;
828
+ return p && ts.isCallExpression(p) && p.arguments.includes(el) && isFrameworkRenderCall(p) ? p : null;
829
+ };
830
+ // The trusted render call that passes `sym` (a const bound to a JSX element) as a DIRECT
831
+ // argument somewhere in scope — `const ui = <X/>; render(ui)`. A nested use
832
+ // (`render(<Shell>{ui}</Shell>)`) is NOT direct and stays ambiguous.
833
+ const constElementRenderCall = (sym, scope) => {
834
+ let hit = null;
835
+ const walk = (n) => {
836
+ if (hit)
837
+ return;
838
+ if (ts.isCallExpression(n) &&
839
+ isFrameworkRenderCall(n) &&
840
+ n.arguments.some((a) => ts.isIdentifier(a) && checker.getSymbolAtLocation(a) === sym)) {
841
+ hit = n;
842
+ return;
843
+ }
844
+ ts.forEachChild(n, walk);
845
+ };
846
+ walk(scope);
847
+ return hit;
848
+ };
849
+ // Returns the framework render()/mount() call that actually mounts the JSX tag `id`,
850
+ // or null when the element is never directly rendered (a bare element object, or a tag
851
+ // nested under another JSX element whose parent may ignore children).
852
+ const jsxRenderCall = (id) => {
853
+ const el = elementOfTag(id);
854
+ if (!el)
855
+ return null;
856
+ const direct = frameworkRenderArgCall(el); // render(<X/>)
857
+ if (direct)
858
+ return renderCallIsLive(direct) ? direct : null;
859
+ const p = el.parent;
860
+ if (ts.isVariableDeclaration(p) && ts.isIdentifier(p.name) && p.initializer === el) {
861
+ const list = p.parent;
862
+ if (list && ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0) {
863
+ const sym = checker.getSymbolAtLocation(p.name);
864
+ if (sym) {
865
+ const call = constElementRenderCall(sym, enclosingTestBlock(el) ?? testSf);
866
+ if (call && renderCallIsLive(call))
867
+ return call;
868
+ }
869
+ }
870
+ }
871
+ return null;
872
+ };
873
+ // ---- module mocks + assertions + runtime refs ----
874
+ let hasNestTestingModuleHarness = false;
875
+ const unsafeMemberTargets = new Set();
876
+ const NEST_DI_ASSOCIATED_REASON = "NestJS TestingModule DI target proof is associated-only until alias-closure verification lands";
877
+ const instanceBindings = new Map();
878
+ const objectPropertyBindings = new Map();
879
+ const arrayElementBindings = new Map();
880
+ const mapEntryBindings = new Map();
881
+ const wrapperInstanceBindings = new Map();
882
+ const constructorBindings = new Map();
883
+ const prototypeBindings = new Map();
884
+ const mutatorAliases = new Map();
885
+ const unwrapExpression = (expr) => {
886
+ let current = expr;
887
+ while (ts.isParenthesizedExpression(current) ||
888
+ ts.isAsExpression(current) ||
889
+ ts.isTypeAssertionExpression(current) ||
890
+ ts.isNonNullExpression(current) ||
891
+ ts.isSatisfiesExpression(current) ||
892
+ ts.isAwaitExpression(current)) {
893
+ current = current.expression;
894
+ }
895
+ return current;
896
+ };
897
+ const numericIndex = (expr) => {
898
+ const index = unwrapExpression(expr);
899
+ if (ts.isNumericLiteral(index))
900
+ return Number(index.text);
901
+ if (ts.isStringLiteralLike(index) && /^\d+$/.test(index.text))
902
+ return Number(index.text);
903
+ return null;
904
+ };
905
+ const constArrayElement = (expr, index, seen = new Set()) => {
906
+ const source = unwrapExpression(expr);
907
+ if (ts.isArrayLiteralExpression(source)) {
908
+ const el = source.elements[index];
909
+ return el && ts.isExpression(el) ? el : null;
910
+ }
911
+ if (!ts.isIdentifier(source))
912
+ return null;
913
+ const sym = checker.getSymbolAtLocation(source);
914
+ if (!sym || seen.has(sym))
915
+ return null;
916
+ seen.add(sym);
917
+ for (const decl of sym.getDeclarations() ?? []) {
918
+ if (ts.isVariableDeclaration(decl) &&
919
+ ts.isIdentifier(decl.name) &&
920
+ decl.initializer &&
921
+ checker.getSymbolAtLocation(decl.name) === sym) {
922
+ const list = decl.parent;
923
+ if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0)
924
+ continue;
925
+ const initializer = unwrapExpression(decl.initializer);
926
+ if (ts.isArrayLiteralExpression(initializer)) {
927
+ const el = initializer.elements[index];
928
+ return el && ts.isExpression(el) ? el : null;
929
+ }
930
+ return constArrayElement(initializer, index, seen);
931
+ }
932
+ }
933
+ return null;
934
+ };
935
+ const classNameFromElementToken = (expr) => {
936
+ const index = numericIndex(expr.argumentExpression);
937
+ if (index === null)
938
+ return null;
939
+ const el = constArrayElement(expr.expression, index);
940
+ return el ? classNameFromToken(el) : null;
941
+ };
942
+ const classNameFromToken = (expr) => {
943
+ const token = unwrapExpression(expr);
944
+ if (ts.isElementAccessExpression(token))
945
+ return classNameFromElementToken(token);
946
+ if (ts.isIdentifier(token)) {
947
+ const src = importSourceOf(token);
948
+ return (resolvedTargetName(token) ??
949
+ (src && cleanBindingSource(src) && memberTargetsByClass.has(src.importedName) ? src.importedName : null) ??
950
+ classNameFromLocalAlias(token));
951
+ }
952
+ return null;
953
+ };
954
+ const mentionedTargetClasses = (node) => {
955
+ const out = new Set();
956
+ const walk = (n) => {
957
+ if (ts.isIdentifier(n)) {
958
+ const className = classNameFromToken(n);
959
+ if (className && memberTargetsByClass.has(className))
960
+ out.add(className);
961
+ }
962
+ ts.forEachChild(n, walk);
963
+ };
964
+ walk(node);
965
+ return out;
966
+ };
967
+ const markMentionedTargetClassesUnsafe = (node) => {
968
+ for (const className of mentionedTargetClasses(node))
969
+ markClassUnsafe(className);
970
+ };
971
+ const insideNestTestingModuleCall = (node) => {
972
+ let current = node.parent;
973
+ while (current) {
974
+ if (ts.isCallExpression(current) && isNestTestingModuleCall(current))
975
+ return true;
976
+ current = current.parent;
977
+ }
978
+ return false;
979
+ };
980
+ function classNameFromLocalAlias(id, seen = new Set()) {
981
+ const sym = checker.getSymbolAtLocation(id);
982
+ if (!sym || seen.has(sym))
983
+ return null;
984
+ seen.add(sym);
985
+ for (const decl of sym.getDeclarations() ?? []) {
986
+ if (ts.isVariableDeclaration(decl) &&
987
+ ts.isIdentifier(decl.name) &&
988
+ decl.initializer &&
989
+ checker.getSymbolAtLocation(decl.name) === sym) {
990
+ const target = classNameFromToken(decl.initializer);
991
+ if (target)
992
+ return target;
993
+ }
994
+ }
995
+ return null;
996
+ }
997
+ ;
998
+ const markClassUnsafe = (className) => {
999
+ if (!className)
1000
+ return;
1001
+ for (const targetName of memberTargetsByClass.get(className) ?? [])
1002
+ unsafeMemberTargets.add(targetName);
1003
+ };
1004
+ const propName = (name) => {
1005
+ if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isNumericLiteral(name))
1006
+ return name.text;
1007
+ return null;
1008
+ };
1009
+ const propertyAccessName = (expr) => {
1010
+ if (ts.isPropertyAccessExpression(expr))
1011
+ return expr.name.text;
1012
+ const arg = unwrapExpression(expr.argumentExpression);
1013
+ return ts.isStringLiteralLike(arg) || ts.isNumericLiteral(arg) ? arg.text : null;
1014
+ };
1015
+ const literalKey = (expr) => {
1016
+ const value = unwrapExpression(expr);
1017
+ if (ts.isStringLiteralLike(value) || ts.isNumericLiteral(value))
1018
+ return value.text;
1019
+ if (!ts.isIdentifier(value))
1020
+ return null;
1021
+ const sym = checker.getSymbolAtLocation(value);
1022
+ for (const decl of sym?.getDeclarations() ?? []) {
1023
+ if (!ts.isVariableDeclaration(decl) || !decl.initializer)
1024
+ continue;
1025
+ const list = decl.parent;
1026
+ if (!list || !ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0)
1027
+ continue;
1028
+ const init = unwrapExpression(decl.initializer);
1029
+ if (ts.isStringLiteralLike(init) || ts.isNumericLiteral(init))
1030
+ return init.text;
1031
+ }
1032
+ return null;
1033
+ };
1034
+ const markTargetMemberUnsafe = (receiver, method) => {
1035
+ const className = classNameForMutableReceiver(receiver);
1036
+ if (!className)
1037
+ return;
1038
+ if (!method || method === "__proto__") {
1039
+ markClassUnsafe(className);
1040
+ return;
1041
+ }
1042
+ if (nameSet.has(`${className}.${method}`))
1043
+ unsafeMemberTargets.add(`${className}.${method}`);
1044
+ };
1045
+ const classNameFromNestGetCall = (expr) => {
1046
+ return bindingFromNestGetCall(expr)?.className ?? null;
1047
+ };
1048
+ function bindingFromNestGetCall(expr) {
1049
+ const call = unwrapExpression(expr);
1050
+ if (!ts.isCallExpression(call) ||
1051
+ !ts.isPropertyAccessExpression(call.expression) ||
1052
+ call.expression.name.text !== "get" ||
1053
+ call.arguments.length === 0) {
1054
+ return null;
1055
+ }
1056
+ const className = classNameFromToken(call.arguments[0]);
1057
+ if (!className || !memberTargetsByClass.has(className))
1058
+ return null;
1059
+ const token = unwrapExpression(call.arguments[0]);
1060
+ const src = ts.isIdentifier(token) ? importSourceOf(token) : null;
1061
+ return { className, src, fromNest: true };
1062
+ }
1063
+ function sameClassBinding(bindings) {
1064
+ const concrete = bindings.filter((b) => !!b);
1065
+ if (concrete.length === 0)
1066
+ return null;
1067
+ const first = concrete[0];
1068
+ return concrete.every((b) => b.className === first.className) ? first : null;
1069
+ }
1070
+ function bindingFromObjectProperty(owner, name) {
1071
+ const value = unwrapExpression(owner);
1072
+ if (ts.isIdentifier(value)) {
1073
+ const sym = checker.getSymbolAtLocation(value);
1074
+ const wrapper = sym ? wrapperInstanceBindings.get(sym) : undefined;
1075
+ const wrapperField = wrapper?.fields.get(name);
1076
+ if (wrapperField)
1077
+ return wrapperField;
1078
+ return (sym ? objectPropertyBindings.get(sym)?.get(name) : null) ?? null;
1079
+ }
1080
+ if (ts.isObjectLiteralExpression(value)) {
1081
+ for (const prop of value.properties) {
1082
+ if (ts.isPropertyAssignment(prop) && propName(prop.name) === name)
1083
+ return bindingFromNestInstanceExpr(prop.initializer);
1084
+ if (ts.isGetAccessor(prop) && propName(prop.name) === name && prop.body)
1085
+ return bindingReturnedByBody(prop.body, []);
1086
+ }
1087
+ }
1088
+ return null;
1089
+ }
1090
+ function bindingFromArrayElement(owner, index) {
1091
+ const value = unwrapExpression(owner);
1092
+ if (ts.isIdentifier(value)) {
1093
+ const sym = checker.getSymbolAtLocation(value);
1094
+ return (sym ? arrayElementBindings.get(sym)?.get(index) : null) ?? null;
1095
+ }
1096
+ if (ts.isArrayLiteralExpression(value)) {
1097
+ const el = value.elements[index];
1098
+ return el && ts.isExpression(el) ? bindingFromNestInstanceExpr(el) : null;
1099
+ }
1100
+ if (ts.isCallExpression(value) &&
1101
+ (calleeText(value) === "Object.values" || mutatorNameFromExpr(value.expression) === "Object.values") &&
1102
+ value.arguments.length > 0) {
1103
+ const bindings = objectLiteralValueBindings(value.arguments[0]);
1104
+ return bindings[index] ?? sameClassBinding(bindings) ?? null;
1105
+ }
1106
+ return null;
1107
+ }
1108
+ function bindingFromMapGet(call) {
1109
+ if (!ts.isPropertyAccessExpression(unwrapExpression(call.expression)))
1110
+ return null;
1111
+ const callee = unwrapExpression(call.expression);
1112
+ if (callee.name.text !== "get" || call.arguments.length === 0)
1113
+ return null;
1114
+ const key = literalKey(call.arguments[0]);
1115
+ const receiver = unwrapExpression(callee.expression);
1116
+ if (!key || !ts.isIdentifier(receiver))
1117
+ return null;
1118
+ const sym = checker.getSymbolAtLocation(receiver);
1119
+ return (sym ? mapEntryBindings.get(sym)?.get(key) : null) ?? null;
1120
+ }
1121
+ function bindingFromNestInstanceExpr(expr) {
1122
+ const receiver = unwrapExpression(expr);
1123
+ if (ts.isIdentifier(receiver)) {
1124
+ const sym = checker.getSymbolAtLocation(receiver);
1125
+ const binding = sym ? instanceBindings.get(sym) : undefined;
1126
+ return binding ?? null;
1127
+ }
1128
+ if (ts.isConditionalExpression(receiver)) {
1129
+ return sameClassBinding([bindingFromNestInstanceExpr(receiver.whenTrue), bindingFromNestInstanceExpr(receiver.whenFalse)]);
1130
+ }
1131
+ if (ts.isBinaryExpression(receiver) &&
1132
+ (receiver.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
1133
+ receiver.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ||
1134
+ receiver.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)) {
1135
+ return sameClassBinding([bindingFromNestInstanceExpr(receiver.left), bindingFromNestInstanceExpr(receiver.right)]);
1136
+ }
1137
+ if (ts.isPropertyAccessExpression(receiver))
1138
+ return bindingFromObjectProperty(receiver.expression, receiver.name.text);
1139
+ if (ts.isElementAccessExpression(receiver)) {
1140
+ const index = numericIndex(receiver.argumentExpression);
1141
+ if (index !== null)
1142
+ return bindingFromArrayElement(receiver.expression, index);
1143
+ const key = literalKey(receiver.argumentExpression);
1144
+ return key ? bindingFromObjectProperty(receiver.expression, key) : null;
1145
+ }
1146
+ if (ts.isCallExpression(receiver)) {
1147
+ const nest = bindingFromNestGetCall(receiver);
1148
+ if (nest)
1149
+ return nest;
1150
+ const map = bindingFromMapGet(receiver);
1151
+ if (map)
1152
+ return map;
1153
+ if (calleeText(receiver) === "Promise.resolve" && receiver.arguments.length > 0)
1154
+ return bindingFromNestInstanceExpr(receiver.arguments[0]);
1155
+ return bindingFromPassThroughCall(receiver);
1156
+ }
1157
+ return bindingFromNestGetCall(receiver);
1158
+ }
1159
+ const classNameFromNestInstanceExpr = (expr) => bindingFromNestInstanceExpr(expr)?.className ?? null;
1160
+ const classNameFromConstructorExpr = (expr) => {
1161
+ const receiver = unwrapExpression(expr);
1162
+ if (ts.isIdentifier(receiver)) {
1163
+ const sym = checker.getSymbolAtLocation(receiver);
1164
+ return (sym ? constructorBindings.get(sym) : null) ?? classNameFromToken(receiver);
1165
+ }
1166
+ if (ts.isPropertyAccessExpression(receiver) && receiver.name.text === "constructor") {
1167
+ return classNameFromNestInstanceExpr(receiver.expression);
1168
+ }
1169
+ return null;
1170
+ };
1171
+ const classNameFromPrototypeExpr = (expr) => {
1172
+ const receiver = unwrapExpression(expr);
1173
+ if (ts.isIdentifier(receiver)) {
1174
+ const sym = checker.getSymbolAtLocation(receiver);
1175
+ return sym ? prototypeBindings.get(sym) ?? null : null;
1176
+ }
1177
+ if (ts.isPropertyAccessExpression(receiver)) {
1178
+ if (receiver.name.text === "prototype")
1179
+ return classNameFromConstructorExpr(receiver.expression);
1180
+ if (receiver.name.text === "__proto__")
1181
+ return classNameFromNestInstanceExpr(receiver.expression);
1182
+ }
1183
+ if (ts.isCallExpression(receiver) &&
1184
+ (mutatorNameFromExpr(receiver.expression) === "Object.getPrototypeOf" ||
1185
+ mutatorNameFromExpr(receiver.expression) === "Reflect.getPrototypeOf" ||
1186
+ calleeText(receiver) === "Object.getPrototypeOf" ||
1187
+ calleeText(receiver) === "Reflect.getPrototypeOf") &&
1188
+ receiver.arguments.length > 0) {
1189
+ return classNameFromNestInstanceExpr(receiver.arguments[0]);
1190
+ }
1191
+ return null;
1192
+ };
1193
+ function bindingReturnedByBody(body, args) {
1194
+ const bindingFromReturnExpr = (expr) => {
1195
+ const ret = unwrapExpression(expr);
1196
+ if (ts.isIdentifier(ret)) {
1197
+ const sym = checker.getSymbolAtLocation(ret);
1198
+ const paramIndex = sym
1199
+ ? args.findIndex((_, index) => {
1200
+ const fn = body.parent;
1201
+ return ((ts.isFunctionExpression(fn) || ts.isArrowFunction(fn) || ts.isFunctionDeclaration(fn) || ts.isMethodDeclaration(fn)) &&
1202
+ !!fn.parameters[index] &&
1203
+ ts.isIdentifier(fn.parameters[index].name) &&
1204
+ checker.getSymbolAtLocation(fn.parameters[index].name) === sym);
1205
+ })
1206
+ : -1;
1207
+ if (paramIndex >= 0)
1208
+ return bindingFromNestInstanceExpr(args[paramIndex]);
1209
+ }
1210
+ return bindingFromNestInstanceExpr(ret);
1211
+ };
1212
+ if (ts.isExpression(body))
1213
+ return bindingFromReturnExpr(body);
1214
+ let out = null;
1215
+ const walk = (n) => {
1216
+ if (out)
1217
+ return;
1218
+ if (ts.isReturnStatement(n) && n.expression) {
1219
+ out = bindingFromReturnExpr(n.expression);
1220
+ return;
1221
+ }
1222
+ ts.forEachChild(n, walk);
1223
+ };
1224
+ walk(body);
1225
+ return out;
1226
+ }
1227
+ const bindingFromPassThroughCall = (expr) => {
1228
+ const call = unwrapExpression(expr);
1229
+ if (!ts.isCallExpression(call))
1230
+ return null;
1231
+ const fn = functionDeclForCallee(call.expression);
1232
+ if (!fn?.body)
1233
+ return null;
1234
+ return bindingReturnedByBody(fn.body, call.arguments);
1235
+ };
1236
+ const classNameForMutableReceiver = (expr) => classNameFromNestInstanceExpr(expr) ?? classNameFromPrototypeExpr(expr);
1237
+ const objectProp = (obj, name) => {
1238
+ for (const p of obj.properties) {
1239
+ if (!ts.isPropertyAssignment(p) || propName(p.name) !== name)
1240
+ continue;
1241
+ return p.initializer;
1242
+ }
1243
+ return null;
1244
+ };
1245
+ function objectLiteralValueBindings(expr) {
1246
+ const value = unwrapExpression(expr);
1247
+ if (!ts.isObjectLiteralExpression(value))
1248
+ return [];
1249
+ const out = [];
1250
+ for (const prop of value.properties) {
1251
+ if (ts.isPropertyAssignment(prop))
1252
+ out.push(bindingFromNestInstanceExpr(prop.initializer));
1253
+ else if (ts.isShorthandPropertyAssignment(prop)) {
1254
+ const valueSym = checker.getShorthandAssignmentValueSymbol(prop);
1255
+ out.push(valueSym ? instanceBindings.get(valueSym) ?? null : bindingFromNestInstanceExpr(prop.name));
1256
+ }
1257
+ else if (ts.isSpreadAssignment(prop))
1258
+ out.push(...objectLiteralValueBindings(prop.expression));
1259
+ else if (ts.isGetAccessor(prop) && prop.body)
1260
+ out.push(bindingReturnedByBody(prop.body, []));
1261
+ }
1262
+ return out;
1263
+ }
1264
+ function classDeclarationForNewExpression(expr) {
1265
+ if (!ts.isIdentifier(expr.expression))
1266
+ return null;
1267
+ const sym = checker.getSymbolAtLocation(expr.expression);
1268
+ for (const decl of sym?.getDeclarations() ?? []) {
1269
+ if (ts.isClassDeclaration(decl))
1270
+ return decl;
1271
+ }
1272
+ return null;
1273
+ }
1274
+ function wrapperFieldsForNewExpression(expr) {
1275
+ const classDecl = classDeclarationForNewExpression(expr);
1276
+ if (!classDecl)
1277
+ return null;
1278
+ const ctor = classDecl.members.find(ts.isConstructorDeclaration);
1279
+ if (!ctor?.body)
1280
+ return null;
1281
+ const paramSymbols = ctor.parameters.map((param) => (ts.isIdentifier(param.name) ? checker.getSymbolAtLocation(param.name) ?? null : null));
1282
+ const fields = new Map();
1283
+ const walk = (n) => {
1284
+ if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
1285
+ const left = unwrapExpression(n.left);
1286
+ const right = unwrapExpression(n.right);
1287
+ if (ts.isPropertyAccessExpression(left) &&
1288
+ left.expression.kind === ts.SyntaxKind.ThisKeyword &&
1289
+ ts.isIdentifier(right)) {
1290
+ const paramIndex = paramSymbols.findIndex((sym) => sym && checker.getSymbolAtLocation(right) === sym);
1291
+ const arg = paramIndex >= 0 ? expr.arguments?.[paramIndex] : undefined;
1292
+ const binding = arg ? bindingFromNestInstanceExpr(arg) : null;
1293
+ if (binding)
1294
+ fields.set(left.name.text, binding);
1295
+ }
1296
+ }
1297
+ ts.forEachChild(n, walk);
1298
+ };
1299
+ walk(ctor.body);
1300
+ return fields.size > 0 ? { classDecl, fields } : null;
1301
+ }
1302
+ function recordObjectPropertyBindings(id, expr) {
1303
+ const sym = checker.getSymbolAtLocation(id);
1304
+ if (!sym)
1305
+ return;
1306
+ const value = unwrapExpression(expr);
1307
+ if (!ts.isObjectLiteralExpression(value))
1308
+ return;
1309
+ const props = new Map();
1310
+ for (const prop of value.properties) {
1311
+ if (ts.isPropertyAssignment(prop)) {
1312
+ const name = propName(prop.name);
1313
+ const binding = bindingFromNestInstanceExpr(prop.initializer);
1314
+ if (name && binding)
1315
+ props.set(name, binding);
1316
+ }
1317
+ else if (ts.isShorthandPropertyAssignment(prop)) {
1318
+ const valueSym = checker.getShorthandAssignmentValueSymbol(prop);
1319
+ const binding = valueSym ? instanceBindings.get(valueSym) ?? null : bindingFromNestInstanceExpr(prop.name);
1320
+ if (binding)
1321
+ props.set(prop.name.text, binding);
1322
+ }
1323
+ else if (ts.isGetAccessor(prop) && prop.body) {
1324
+ const name = propName(prop.name);
1325
+ const binding = bindingReturnedByBody(prop.body, []);
1326
+ if (name && binding)
1327
+ props.set(name, binding);
1328
+ }
1329
+ else if (ts.isSpreadAssignment(prop)) {
1330
+ const spread = unwrapExpression(prop.expression);
1331
+ if (ts.isIdentifier(spread)) {
1332
+ const spreadSym = checker.getSymbolAtLocation(spread);
1333
+ for (const [key, binding] of objectPropertyBindings.get(spreadSym) ?? [])
1334
+ props.set(key, binding);
1335
+ }
1336
+ }
1337
+ }
1338
+ if (props.size > 0)
1339
+ objectPropertyBindings.set(sym, props);
1340
+ }
1341
+ function recordArrayElementBindings(id, expr) {
1342
+ const sym = checker.getSymbolAtLocation(id);
1343
+ if (!sym)
1344
+ return;
1345
+ const value = unwrapExpression(expr);
1346
+ if (!ts.isArrayLiteralExpression(value))
1347
+ return;
1348
+ const elements = new Map();
1349
+ value.elements.forEach((element, index) => {
1350
+ if (!ts.isExpression(element))
1351
+ return;
1352
+ const binding = bindingFromNestInstanceExpr(element);
1353
+ if (binding)
1354
+ elements.set(index, binding);
1355
+ });
1356
+ if (elements.size > 0)
1357
+ arrayElementBindings.set(sym, elements);
1358
+ }
1359
+ function recordMapEntryBindings(id, expr) {
1360
+ const sym = checker.getSymbolAtLocation(id);
1361
+ if (!sym)
1362
+ return;
1363
+ const value = unwrapExpression(expr);
1364
+ if (!ts.isNewExpression(value) || !ts.isIdentifier(value.expression) || value.expression.text !== "Map" || !value.arguments?.length)
1365
+ return;
1366
+ const entries = unwrapExpression(value.arguments[0]);
1367
+ if (!ts.isArrayLiteralExpression(entries))
1368
+ return;
1369
+ const map = new Map();
1370
+ for (const entry of entries.elements) {
1371
+ const tuple = ts.isExpression(entry) ? unwrapExpression(entry) : null;
1372
+ if (!tuple || !ts.isArrayLiteralExpression(tuple) || tuple.elements.length < 2)
1373
+ continue;
1374
+ const keyExpr = tuple.elements[0];
1375
+ const valueExpr = tuple.elements[1];
1376
+ if (!ts.isExpression(keyExpr) || !ts.isExpression(valueExpr))
1377
+ continue;
1378
+ const key = literalKey(keyExpr);
1379
+ const binding = bindingFromNestInstanceExpr(valueExpr);
1380
+ if (key && binding)
1381
+ map.set(key, binding);
1382
+ }
1383
+ if (map.size > 0)
1384
+ mapEntryBindings.set(sym, map);
1385
+ }
1386
+ function recordWrapperInstanceBinding(id, expr) {
1387
+ const sym = checker.getSymbolAtLocation(id);
1388
+ const value = unwrapExpression(expr);
1389
+ if (!sym || !ts.isNewExpression(value))
1390
+ return;
1391
+ const binding = wrapperFieldsForNewExpression(value);
1392
+ if (binding)
1393
+ wrapperInstanceBindings.set(sym, binding);
1394
+ }
1395
+ function recordContainerBindings(id, expr) {
1396
+ recordObjectPropertyBindings(id, expr);
1397
+ recordArrayElementBindings(id, expr);
1398
+ recordMapEntryBindings(id, expr);
1399
+ recordWrapperInstanceBinding(id, expr);
1400
+ }
1401
+ const markObjectLiteralMembersUnsafe = (className, obj) => {
1402
+ for (const p of obj.properties) {
1403
+ if (ts.isSpreadAssignment(p)) {
1404
+ markClassUnsafe(className);
1405
+ continue;
1406
+ }
1407
+ const name = "name" in p ? propName(p.name) : null;
1408
+ if (!name) {
1409
+ markClassUnsafe(className);
1410
+ continue;
1411
+ }
1412
+ if (nameSet.has(`${className}.${name}`))
1413
+ unsafeMemberTargets.add(`${className}.${name}`);
1414
+ }
1415
+ };
1416
+ function mutatorNameFromExpr(expr) {
1417
+ const callee = unwrapExpression(expr);
1418
+ if (ts.isIdentifier(callee)) {
1419
+ const sym = checker.getSymbolAtLocation(callee);
1420
+ return sym ? mutatorAliases.get(sym) ?? null : null;
1421
+ }
1422
+ if (ts.isPropertyAccessExpression(callee)) {
1423
+ const root = unwrapExpression(callee.expression);
1424
+ if (ts.isIdentifier(root) && (root.text === "Object" || root.text === "Reflect" || root.text === "jest")) {
1425
+ return `${root.text}.${callee.name.text}`;
1426
+ }
1427
+ if (callee.name.text === "__defineGetter__" || callee.name.text === "__defineSetter__")
1428
+ return callee.name.text;
1429
+ }
1430
+ if (ts.isElementAccessExpression(callee)) {
1431
+ const root = unwrapExpression(callee.expression);
1432
+ const key = unwrapExpression(callee.argumentExpression);
1433
+ if (ts.isIdentifier(root) && ts.isStringLiteralLike(key) && (root.text === "Object" || root.text === "Reflect" || root.text === "jest")) {
1434
+ return `${root.text}.${key.text}`;
1435
+ }
1436
+ }
1437
+ return null;
1438
+ }
1439
+ const bindMutatorAlias = (id, expr) => {
1440
+ const name = mutatorNameFromExpr(expr);
1441
+ if (!name)
1442
+ return;
1443
+ const sym = checker.getSymbolAtLocation(id);
1444
+ if (sym)
1445
+ mutatorAliases.set(sym, name);
1446
+ };
1447
+ const isAssignmentOperatorKind = (kind) => kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
1448
+ const hasNestSubstitution = (obj) => ["useValue", "useClass", "useFactory", "useExisting"].some((name) => objectProp(obj, name) !== null);
1449
+ const markNestProviderSubstitution = (node) => {
1450
+ if (ts.isObjectLiteralExpression(node) && hasNestSubstitution(node)) {
1451
+ const provide = objectProp(node, "provide");
1452
+ const className = provide ? classNameFromToken(provide) : null;
1453
+ if (className)
1454
+ markClassUnsafe(className);
1455
+ else
1456
+ markMentionedTargetClassesUnsafe(provide ?? node);
1457
+ }
1458
+ };
1459
+ function functionDeclForCallee(expr) {
1460
+ const callee = unwrapExpression(expr);
1461
+ if (!ts.isIdentifier(callee))
1462
+ return null;
1463
+ const sym = checker.getSymbolAtLocation(callee);
1464
+ for (const decl of sym?.getDeclarations() ?? []) {
1465
+ if (ts.isFunctionDeclaration(decl) || ts.isFunctionExpression(decl) || ts.isArrowFunction(decl) || ts.isMethodDeclaration(decl)) {
1466
+ return decl;
1467
+ }
1468
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
1469
+ const init = unwrapExpression(decl.initializer);
1470
+ if (ts.isFunctionExpression(init) || ts.isArrowFunction(init))
1471
+ return init;
1472
+ }
1473
+ }
1474
+ return null;
1475
+ }
1476
+ const classNamesContainedInExpression = (expr, out = new Set()) => {
1477
+ const value = unwrapExpression(expr);
1478
+ const direct = classNameForMutableReceiver(value);
1479
+ if (direct)
1480
+ out.add(direct);
1481
+ if (ts.isObjectLiteralExpression(value)) {
1482
+ for (const prop of value.properties) {
1483
+ if (ts.isPropertyAssignment(prop))
1484
+ classNamesContainedInExpression(prop.initializer, out);
1485
+ else if (ts.isSpreadAssignment(prop))
1486
+ classNamesContainedInExpression(prop.expression, out);
1487
+ }
1488
+ }
1489
+ else if (ts.isArrayLiteralExpression(value)) {
1490
+ for (const el of value.elements)
1491
+ if (ts.isExpression(el))
1492
+ classNamesContainedInExpression(el, out);
1493
+ }
1494
+ return out;
1495
+ };
1496
+ const functionMutatesParameter = (fn, index, seen = new Set()) => {
1497
+ if (seen.has(fn))
1498
+ return false;
1499
+ seen.add(fn);
1500
+ const param = fn.parameters[index];
1501
+ if (!param || !ts.isIdentifier(param.name))
1502
+ return false;
1503
+ const paramSym = checker.getSymbolAtLocation(param.name);
1504
+ if (!paramSym || !fn.body)
1505
+ return false;
1506
+ let mutates = false;
1507
+ const isParamRef = (expr) => {
1508
+ const receiver = unwrapExpression(expr);
1509
+ return ts.isIdentifier(receiver) && checker.getSymbolAtLocation(receiver) === paramSym;
1510
+ };
1511
+ const isParamDerived = (expr) => {
1512
+ const value = unwrapExpression(expr);
1513
+ if (isParamRef(value))
1514
+ return true;
1515
+ if (ts.isPropertyAccessExpression(value) || ts.isElementAccessExpression(value))
1516
+ return isParamDerived(value.expression);
1517
+ if (ts.isCallExpression(value) &&
1518
+ (mutatorNameFromExpr(value.expression) === "Object.getPrototypeOf" || mutatorNameFromExpr(value.expression) === "Reflect.getPrototypeOf") &&
1519
+ value.arguments.length > 0) {
1520
+ return isParamDerived(value.arguments[0]);
1521
+ }
1522
+ return false;
1523
+ };
1524
+ const walk = (n) => {
1525
+ if (mutates)
1526
+ return;
1527
+ if (ts.isBinaryExpression(n) && isAssignmentOperatorKind(n.operatorToken.kind)) {
1528
+ const left = unwrapExpression(n.left);
1529
+ const right = unwrapExpression(n.right);
1530
+ if (ts.isIdentifier(left) && isParamDerived(right)) {
1531
+ mutates = true;
1532
+ return;
1533
+ }
1534
+ if ((ts.isPropertyAccessExpression(left) || ts.isElementAccessExpression(left)) && isParamRef(left.expression)) {
1535
+ mutates = true;
1536
+ return;
1537
+ }
1538
+ if ((ts.isPropertyAccessExpression(left) || ts.isElementAccessExpression(left)) && isParamDerived(left.expression)) {
1539
+ mutates = true;
1540
+ return;
1541
+ }
1542
+ }
1543
+ if (ts.isCallExpression(n)) {
1544
+ const name = mutatorNameFromExpr(n.expression);
1545
+ if (name &&
1546
+ n.arguments.length > 0 &&
1547
+ isParamRef(n.arguments[0]) &&
1548
+ (name === "Object.assign" ||
1549
+ name === "Object.defineProperty" ||
1550
+ name === "Reflect.defineProperty" ||
1551
+ name === "Reflect.set" ||
1552
+ name === "Object.defineProperties" ||
1553
+ name === "Object.setPrototypeOf" ||
1554
+ name === "Reflect.setPrototypeOf" ||
1555
+ /(^|\.)replaceProperty$/.test(name))) {
1556
+ mutates = true;
1557
+ return;
1558
+ }
1559
+ const called = functionDeclForCallee(n.expression);
1560
+ if (called) {
1561
+ for (let argIndex = 0; argIndex < n.arguments.length; argIndex += 1) {
1562
+ if (isParamDerived(n.arguments[argIndex]) && functionMutatesParameter(called, argIndex, seen)) {
1563
+ mutates = true;
1564
+ return;
1565
+ }
1566
+ }
1567
+ }
1568
+ }
1569
+ ts.forEachChild(n, walk);
1570
+ };
1571
+ walk(fn.body);
1572
+ return mutates;
1573
+ };
1574
+ const markHelperMutation = (node) => {
1575
+ const fn = functionDeclForCallee(node.expression);
1576
+ if (!fn)
1577
+ return;
1578
+ node.arguments.forEach((arg, index) => {
1579
+ if (!functionMutatesParameter(fn, index))
1580
+ return;
1581
+ const classes = classNamesContainedInExpression(arg);
1582
+ const directCtor = classNameFromConstructorExpr(arg);
1583
+ if (directCtor)
1584
+ classes.add(directCtor);
1585
+ for (const className of classes)
1586
+ markClassUnsafe(className);
1587
+ });
1588
+ };
1589
+ const markWrapperMethodMutation = (node) => {
1590
+ const callee = unwrapExpression(node.expression);
1591
+ if (!ts.isPropertyAccessExpression(callee))
1592
+ return;
1593
+ const receiver = unwrapExpression(callee.expression);
1594
+ if (!ts.isIdentifier(receiver))
1595
+ return;
1596
+ const receiverSym = checker.getSymbolAtLocation(receiver);
1597
+ const wrapper = receiverSym ? wrapperInstanceBindings.get(receiverSym) : undefined;
1598
+ if (!wrapper)
1599
+ return;
1600
+ const method = wrapper.classDecl.members.find((member) => {
1601
+ return ts.isMethodDeclaration(member) && !!member.name && propName(member.name) === callee.name.text;
1602
+ });
1603
+ if (!method?.body)
1604
+ return;
1605
+ const classForThisProp = (expr) => {
1606
+ const value = unwrapExpression(expr);
1607
+ if (ts.isPropertyAccessExpression(value) &&
1608
+ value.expression.kind === ts.SyntaxKind.ThisKeyword) {
1609
+ return wrapper.fields.get(value.name.text)?.className ?? null;
1610
+ }
1611
+ if (ts.isPropertyAccessExpression(value) || ts.isElementAccessExpression(value))
1612
+ return classForThisProp(value.expression);
1613
+ return null;
1614
+ };
1615
+ const walk = (n) => {
1616
+ if (ts.isBinaryExpression(n) && isAssignmentOperatorKind(n.operatorToken.kind)) {
1617
+ const left = unwrapExpression(n.left);
1618
+ if (ts.isPropertyAccessExpression(left) || ts.isElementAccessExpression(left)) {
1619
+ const className = classForThisProp(left.expression);
1620
+ const memberName = propertyAccessName(left);
1621
+ if (className && memberName) {
1622
+ if (nameSet.has(`${className}.${memberName}`))
1623
+ unsafeMemberTargets.add(`${className}.${memberName}`);
1624
+ else
1625
+ markClassUnsafe(className);
1626
+ }
1627
+ }
1628
+ }
1629
+ ts.forEachChild(n, walk);
1630
+ };
1631
+ walk(method.body);
1632
+ };
1633
+ const markTupleMutationSeed = (expr) => {
1634
+ const root = unwrapExpression(expr);
1635
+ if (!ts.isArrayLiteralExpression(root))
1636
+ return;
1637
+ const scan = (arr) => {
1638
+ for (const element of arr.elements) {
1639
+ if (!ts.isExpression(element))
1640
+ continue;
1641
+ const e = unwrapExpression(element);
1642
+ if (ts.isArrayLiteralExpression(e)) {
1643
+ const className = e.elements.find((el) => ts.isExpression(el) && !!classNameForMutableReceiver(el));
1644
+ const method = e.elements
1645
+ .filter((el) => ts.isExpression(el))
1646
+ .map((el) => {
1647
+ const v = unwrapExpression(el);
1648
+ return ts.isStringLiteralLike(v) || ts.isNumericLiteral(v) ? v.text : null;
1649
+ })
1650
+ .find((v) => !!v);
1651
+ const hasFn = e.elements.some((el) => ts.isExpression(el) && (ts.isArrowFunction(unwrapExpression(el)) || ts.isFunctionExpression(unwrapExpression(el))));
1652
+ const resolvedClass = className ? classNameForMutableReceiver(className) : null;
1653
+ if (resolvedClass && hasFn) {
1654
+ if (method && nameSet.has(`${resolvedClass}.${method}`))
1655
+ unsafeMemberTargets.add(`${resolvedClass}.${method}`);
1656
+ else
1657
+ markClassUnsafe(resolvedClass);
1658
+ }
1659
+ scan(e);
1660
+ }
1661
+ }
1662
+ };
1663
+ scan(root);
1664
+ };
1665
+ const markSpyOrPatch = (node) => {
1666
+ const c = mutatorNameFromExpr(node.expression) ?? calleeText(node);
1667
+ if (c === "overrideProvider" && node.arguments.length > 0) {
1668
+ const className = classNameFromToken(node.arguments[0]);
1669
+ if (className)
1670
+ markClassUnsafe(className);
1671
+ else
1672
+ markMentionedTargetClassesUnsafe(node.arguments[0]);
1673
+ }
1674
+ if (!isNestTestingModuleCall(node) && insideNestTestingModuleCall(node)) {
1675
+ markMentionedTargetClassesUnsafe(node);
1676
+ }
1677
+ markHelperMutation(node);
1678
+ markWrapperMethodMutation(node);
1679
+ const calleeExpr = unwrapExpression(node.expression);
1680
+ if (ts.isPropertyAccessExpression(calleeExpr) && calleeExpr.name.text === "push" && node.arguments.length > 0) {
1681
+ const receiver = unwrapExpression(calleeExpr.expression);
1682
+ const binding = bindingFromNestInstanceExpr(node.arguments[0]);
1683
+ if (binding && ts.isIdentifier(receiver)) {
1684
+ const sym = checker.getSymbolAtLocation(receiver);
1685
+ if (sym) {
1686
+ const existing = arrayElementBindings.get(sym) ?? new Map();
1687
+ const next = existing.size === 0 ? 0 : Math.max(...existing.keys()) + 1;
1688
+ existing.set(next, binding);
1689
+ arrayElementBindings.set(sym, existing);
1690
+ }
1691
+ }
1692
+ }
1693
+ if (ts.isPropertyAccessExpression(unwrapExpression(node.expression)) && unwrapExpression(node.expression).name.text === "set") {
1694
+ const receiver = unwrapExpression(unwrapExpression(node.expression).expression);
1695
+ const key = node.arguments.length > 0 ? literalKey(node.arguments[0]) : null;
1696
+ const value = node.arguments[1] ? bindingFromNestInstanceExpr(node.arguments[1]) : null;
1697
+ if (key && value && ts.isIdentifier(receiver)) {
1698
+ const sym = checker.getSymbolAtLocation(receiver);
1699
+ if (sym) {
1700
+ const existing = mapEntryBindings.get(sym) ?? new Map();
1701
+ existing.set(key, value);
1702
+ mapEntryBindings.set(sym, existing);
1703
+ }
1704
+ }
1705
+ }
1706
+ if (c === "Object.assign" && node.arguments.length > 1) {
1707
+ const className = classNameForMutableReceiver(node.arguments[0]);
1708
+ if (className) {
1709
+ for (const arg of node.arguments.slice(1)) {
1710
+ const source = unwrapExpression(arg);
1711
+ if (!ts.isObjectLiteralExpression(source)) {
1712
+ markClassUnsafe(className);
1713
+ continue;
1714
+ }
1715
+ markObjectLiteralMembersUnsafe(className, source);
1716
+ }
1717
+ }
1718
+ }
1719
+ if (c === "Object.defineProperties" && node.arguments.length > 1) {
1720
+ const className = classNameForMutableReceiver(node.arguments[0]);
1721
+ const descriptors = unwrapExpression(node.arguments[1]);
1722
+ if (className && ts.isObjectLiteralExpression(descriptors))
1723
+ markObjectLiteralMembersUnsafe(className, descriptors);
1724
+ else if (className)
1725
+ markClassUnsafe(className);
1726
+ }
1727
+ if ((c === "Object.defineProperty" || c === "Reflect.defineProperty" || c === "Reflect.set" || /(^|\.)replaceProperty$/.test(c)) &&
1728
+ node.arguments.length > 1) {
1729
+ const methodArg = unwrapExpression(node.arguments[1]);
1730
+ const method = ts.isStringLiteralLike(methodArg) || ts.isNumericLiteral(methodArg) ? methodArg.text : null;
1731
+ markTargetMemberUnsafe(node.arguments[0], method);
1732
+ }
1733
+ if ((c === "Object.setPrototypeOf" || c === "Reflect.setPrototypeOf") && node.arguments.length > 0) {
1734
+ markClassUnsafe(classNameForMutableReceiver(node.arguments[0]));
1735
+ }
1736
+ if ((c === "__defineGetter__" || c === "__defineSetter__") && ts.isPropertyAccessExpression(unwrapExpression(node.expression)) && node.arguments.length > 0) {
1737
+ const receiver = unwrapExpression(node.expression);
1738
+ const methodArg = unwrapExpression(node.arguments[0]);
1739
+ const method = ts.isStringLiteralLike(methodArg) || ts.isNumericLiteral(methodArg) ? methodArg.text : null;
1740
+ markTargetMemberUnsafe(receiver.expression, method);
1741
+ }
1742
+ if (/(^|\.)spyOn$/.test(c) && node.arguments.length >= 2 && ts.isStringLiteralLike(node.arguments[1])) {
1743
+ markTargetMemberUnsafe(node.arguments[0], node.arguments[1].text);
1744
+ }
1745
+ };
1746
+ const markMemberAssignment = (node) => {
1747
+ if (!isAssignmentOperatorKind(node.operatorToken.kind))
1748
+ return;
1749
+ const left = unwrapExpression(node.left);
1750
+ if (!ts.isPropertyAccessExpression(left) && !ts.isElementAccessExpression(left))
1751
+ return;
1752
+ const memberName = propertyAccessName(left);
1753
+ markTargetMemberUnsafe(left.expression, memberName);
1754
+ };
1755
+ const recordMemberAliasAssignment = (node) => {
1756
+ if (node.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
1757
+ return;
1758
+ const left = unwrapExpression(node.left);
1759
+ if (!ts.isPropertyAccessExpression(left) && !ts.isElementAccessExpression(left))
1760
+ return;
1761
+ const binding = bindingFromNestInstanceExpr(node.right);
1762
+ if (!binding)
1763
+ return;
1764
+ const receiver = unwrapExpression(left.expression);
1765
+ if (!ts.isIdentifier(receiver))
1766
+ return;
1767
+ const sym = checker.getSymbolAtLocation(receiver);
1768
+ if (!sym)
1769
+ return;
1770
+ if (ts.isPropertyAccessExpression(left)) {
1771
+ const existing = objectPropertyBindings.get(sym) ?? new Map();
1772
+ existing.set(left.name.text, binding);
1773
+ objectPropertyBindings.set(sym, existing);
1774
+ return;
1775
+ }
1776
+ const index = numericIndex(left.argumentExpression);
1777
+ const key = index === null ? literalKey(left.argumentExpression) : null;
1778
+ if (index !== null) {
1779
+ const existing = arrayElementBindings.get(sym) ?? new Map();
1780
+ existing.set(index, binding);
1781
+ arrayElementBindings.set(sym, existing);
1782
+ }
1783
+ else if (key) {
1784
+ const existing = objectPropertyBindings.get(sym) ?? new Map();
1785
+ existing.set(key, binding);
1786
+ objectPropertyBindings.set(sym, existing);
1787
+ }
1788
+ };
1789
+ const classBindingFromExpr = (expr, requireNest) => {
1790
+ const value = unwrapExpression(expr);
1791
+ if (ts.isIdentifier(value)) {
1792
+ const sym = checker.getSymbolAtLocation(value);
1793
+ const binding = sym ? instanceBindings.get(sym) : undefined;
1794
+ if (binding)
1795
+ return binding;
1796
+ }
1797
+ if (ts.isNewExpression(value) && ts.isIdentifier(value.expression)) {
1798
+ const src = importSourceOf(value.expression);
1799
+ const resolved = classNameFromToken(value.expression);
1800
+ if (!resolved || !memberTargetsByClass.has(resolved))
1801
+ return null;
1802
+ return { className: resolved, src, fromNest: false };
1803
+ }
1804
+ const nestClass = classNameFromNestGetCall(value);
1805
+ if (nestClass) {
1806
+ if (requireNest && !hasNestTestingModuleHarness)
1807
+ return null;
1808
+ const call = unwrapExpression(value);
1809
+ const token = unwrapExpression(call.arguments[0]);
1810
+ const src = ts.isIdentifier(token) ? importSourceOf(token) : null;
1811
+ return { className: nestClass, src, fromNest: true };
1812
+ }
1813
+ return bindingFromNestInstanceExpr(value);
1814
+ };
1815
+ const maybeBindInstance = (name, expr) => {
1816
+ const binding = classBindingFromExpr(expr, true);
1817
+ if (!binding)
1818
+ return;
1819
+ const sym = checker.getSymbolAtLocation(name);
1820
+ if (sym)
1821
+ instanceBindings.set(sym, binding);
1822
+ };
1823
+ const maybeBindDerivedIdentity = (name, expr) => {
1824
+ bindMutatorAlias(name, expr);
1825
+ const sym = checker.getSymbolAtLocation(name);
1826
+ if (!sym)
1827
+ return;
1828
+ const ctorClass = classNameFromConstructorExpr(expr);
1829
+ if (ctorClass)
1830
+ constructorBindings.set(sym, ctorClass);
1831
+ const protoClass = classNameFromPrototypeExpr(expr);
1832
+ if (protoClass)
1833
+ prototypeBindings.set(sym, protoClass);
1834
+ };
1835
+ const bindArrayPatternElements = (pattern, initializer) => {
1836
+ const source = unwrapExpression(initializer);
1837
+ if (!ts.isArrayLiteralExpression(source))
1838
+ return;
1839
+ pattern.elements.forEach((element, index) => {
1840
+ if (!ts.isBindingElement(element) || !ts.isIdentifier(element.name))
1841
+ return;
1842
+ const value = source.elements[index];
1843
+ if (value && ts.isExpression(value)) {
1844
+ maybeBindInstance(element.name, value);
1845
+ maybeBindDerivedIdentity(element.name, value);
1846
+ }
1847
+ });
1848
+ };
1849
+ const memberUseForReceiver = (id) => {
1850
+ const p = id.parent;
1851
+ if (!p || !ts.isPropertyAccessExpression(p) || p.expression !== id || !ts.isIdentifier(p.name))
1852
+ return null;
1853
+ const call = p.parent;
1854
+ if (!call || !ts.isCallExpression(call) || call.expression !== p)
1855
+ return null;
1856
+ const sym = checker.getSymbolAtLocation(id);
1857
+ const binding = sym ? instanceBindings.get(sym) : undefined;
1858
+ if (!binding)
1859
+ return null;
1860
+ const targetName = `${binding.className}.${p.name.text}`;
1861
+ if (!nameSet.has(targetName))
1862
+ return null;
1863
+ if (binding.fromNest) {
1864
+ stateFor(targetName).nonRuntimeReasons.add(NEST_DI_ASSOCIATED_REASON);
1865
+ return null;
1866
+ }
1867
+ if (unsafeMemberTargets.has(targetName)) {
1868
+ stateFor(targetName).nonRuntimeReasons.add("NestJS provider or target method is substituted/mocked");
1869
+ return null;
1870
+ }
1871
+ const tb = enclosingTestBlock(call);
1872
+ if (!tb) {
1873
+ stateFor(targetName).nonRuntimeReasons.add("runtime use is outside any test block");
1874
+ return null;
1875
+ }
1876
+ return {
1877
+ targetName,
1878
+ use: {
1879
+ node: p.name,
1880
+ useExpr: call,
1881
+ kind: "value",
1882
+ block: tb,
1883
+ src: binding.src,
1884
+ }
1885
+ };
1886
+ };
1887
+ const memberUseForCall = (call) => {
1888
+ const callee = unwrapExpression(call.expression);
1889
+ if (!ts.isPropertyAccessExpression(callee))
1890
+ return null;
1891
+ if (!ts.isIdentifier(callee.name))
1892
+ return null;
1893
+ const method = callee.name.text;
1894
+ const binding = bindingFromNestInstanceExpr(callee.expression);
1895
+ if (!binding)
1896
+ return null;
1897
+ const targetName = `${binding.className}.${method}`;
1898
+ if (!nameSet.has(targetName))
1899
+ return null;
1900
+ if (binding.fromNest) {
1901
+ stateFor(targetName).nonRuntimeReasons.add(NEST_DI_ASSOCIATED_REASON);
1902
+ return null;
1903
+ }
1904
+ if (unsafeMemberTargets.has(targetName)) {
1905
+ stateFor(targetName).nonRuntimeReasons.add("NestJS provider or target method is substituted/mocked");
1906
+ return null;
1907
+ }
1908
+ const tb = enclosingTestBlock(call);
1909
+ if (!tb) {
1910
+ stateFor(targetName).nonRuntimeReasons.add("runtime use is outside any test block");
1911
+ return null;
1912
+ }
1913
+ return {
1914
+ targetName,
1915
+ use: {
1916
+ node: callee.name,
1917
+ useExpr: call,
1918
+ kind: "value",
1919
+ block: tb,
1920
+ src: binding.src,
1921
+ }
1922
+ };
1923
+ };
1924
+ const helperMemberUsesForCall = (call) => {
1925
+ const fn = functionDeclForCallee(call.expression);
1926
+ if (!fn?.body)
1927
+ return [];
1928
+ const body = fn.body;
1929
+ const out = [];
1930
+ fn.parameters.forEach((param, index) => {
1931
+ if (!ts.isIdentifier(param.name))
1932
+ return;
1933
+ const arg = call.arguments[index];
1934
+ const binding = arg ? bindingFromNestInstanceExpr(arg) : null;
1935
+ if (!binding)
1936
+ return;
1937
+ const paramSym = checker.getSymbolAtLocation(param.name);
1938
+ if (!paramSym)
1939
+ return;
1940
+ if (binding.fromNest) {
1941
+ const markNestDiUse = (n) => {
1942
+ if (ts.isCallExpression(n)) {
1943
+ const callee = unwrapExpression(n.expression);
1944
+ if (ts.isPropertyAccessExpression(callee) &&
1945
+ ts.isIdentifier(callee.name) &&
1946
+ ts.isIdentifier(unwrapExpression(callee.expression)) &&
1947
+ checker.getSymbolAtLocation(unwrapExpression(callee.expression)) === paramSym) {
1948
+ const targetName = `${binding.className}.${callee.name.text}`;
1949
+ if (nameSet.has(targetName))
1950
+ stateFor(targetName).nonRuntimeReasons.add(NEST_DI_ASSOCIATED_REASON);
1951
+ }
1952
+ }
1953
+ ts.forEachChild(n, markNestDiUse);
1954
+ };
1955
+ markNestDiUse(body);
1956
+ return;
1957
+ }
1958
+ const walk = (n) => {
1959
+ if (ts.isCallExpression(n)) {
1960
+ const callee = unwrapExpression(n.expression);
1961
+ if (ts.isPropertyAccessExpression(callee) &&
1962
+ ts.isIdentifier(callee.name) &&
1963
+ ts.isIdentifier(unwrapExpression(callee.expression)) &&
1964
+ checker.getSymbolAtLocation(unwrapExpression(callee.expression)) === paramSym) {
1965
+ const targetName = `${binding.className}.${callee.name.text}`;
1966
+ if (nameSet.has(targetName) && !unsafeMemberTargets.has(targetName)) {
1967
+ const tb = enclosingTestBlock(call);
1968
+ if (tb) {
1969
+ out.push({
1970
+ targetName,
1971
+ use: {
1972
+ node: ts.isIdentifier(unwrapExpression(call.expression)) ? unwrapExpression(call.expression) : callee.name,
1973
+ useExpr: call,
1974
+ kind: "value",
1975
+ block: tb,
1976
+ src: binding.src,
1977
+ }
1978
+ });
1979
+ }
1980
+ }
1981
+ }
1982
+ }
1983
+ ts.forEachChild(n, walk);
1984
+ };
1985
+ walk(body);
1986
+ });
1987
+ return out;
1988
+ };
1989
+ const visit = (node) => {
1990
+ if (ts.isCallExpression(node)) {
1991
+ const c = calleeText(node);
1992
+ if (isNestTestingModuleCall(node))
1993
+ hasNestTestingModuleHarness = true;
1994
+ markSpyOrPatch(node);
1995
+ const callMemberUse = memberUseForCall(node);
1996
+ if (callMemberUse)
1997
+ stateFor(callMemberUse.targetName).runtimeUses.push(callMemberUse.use);
1998
+ for (const helperUse of helperMemberUsesForCall(node))
1999
+ stateFor(helperUse.targetName).runtimeUses.push(helperUse.use);
2000
+ if (MODULE_MOCK_CALLEES.has(c)) {
2001
+ const spec = mockSpecifier(node.arguments[0]);
2002
+ if (spec) {
2003
+ mockedSpecifiers.add(spec);
2004
+ const r = resolveImport(spec, testAbsPath);
2005
+ if (r.resolvedFileName)
2006
+ mockedFiles.add(norm(r.resolvedFileName));
2007
+ }
2008
+ }
2009
+ // Count framework render()/mount() calls per test block — a render effect can only
2010
+ // be tied to a SINGLE render in the block (multiple distinct renders are ambiguous).
2011
+ if (isFrameworkRenderCall(node)) {
2012
+ const rb = enclosingTestBlock(node);
2013
+ // Only LIVE renders count — an uninvoked nested render must not create the use AND
2014
+ // satisfy the "exactly one render" check (Codex round-5).
2015
+ if (rb && isLivePosition(node, rb))
2016
+ renderCallsByBlock.set(rb, (renderCallsByBlock.get(rb) ?? 0) + 1);
2017
+ }
2018
+ const aKind = assertionKind(node);
2019
+ // An `assert*` call counts ONLY when its callee resolves to a trusted assert
2020
+ // module (node:assert/chai); a LOCAL no-op `assert` cannot fake evidence.
2021
+ if (aKind && (aKind !== "assert" || isTrustedAssert(node))) {
2022
+ const tb = enclosingTestBlock(node);
2023
+ if (tb)
2024
+ assertions.push({ call: node, block: tb, kind: aKind });
2025
+ }
2026
+ }
2027
+ markNestProviderSubstitution(node);
2028
+ if (ts.isVariableDeclaration(node) && node.initializer) {
2029
+ markTupleMutationSeed(node.initializer);
2030
+ if (ts.isIdentifier(node.name)) {
2031
+ recordContainerBindings(node.name, node.initializer);
2032
+ maybeBindInstance(node.name, node.initializer);
2033
+ maybeBindDerivedIdentity(node.name, node.initializer);
2034
+ }
2035
+ else if (ts.isArrayBindingPattern(node.name)) {
2036
+ bindArrayPatternElements(node.name, node.initializer);
2037
+ }
2038
+ }
2039
+ else if (ts.isBinaryExpression(node)) {
2040
+ markMemberAssignment(node);
2041
+ recordMemberAliasAssignment(node);
2042
+ if (node.operatorToken.kind !== ts.SyntaxKind.EqualsToken &&
2043
+ isAssignmentOperatorKind(node.operatorToken.kind) &&
2044
+ ts.isIdentifier(node.left)) {
2045
+ const sym = checker.getSymbolAtLocation(node.left);
2046
+ const binding = sym ? instanceBindings.get(sym) : undefined;
2047
+ if (sym && binding) {
2048
+ markClassUnsafe(binding.className);
2049
+ instanceBindings.delete(sym);
2050
+ }
2051
+ }
2052
+ if (node.operatorToken.kind !== ts.SyntaxKind.EqualsToken) {
2053
+ ts.forEachChild(node, visit);
2054
+ return;
2055
+ }
2056
+ if (!ts.isIdentifier(node.left)) {
2057
+ ts.forEachChild(node, visit);
2058
+ return;
2059
+ }
2060
+ const sym = checker.getSymbolAtLocation(node.left);
2061
+ const binding = classBindingFromExpr(node.right, true);
2062
+ if (binding && sym)
2063
+ instanceBindings.set(sym, binding);
2064
+ else if (sym && instanceBindings.has(sym))
2065
+ instanceBindings.delete(sym);
2066
+ if (ts.isIdentifier(node.left))
2067
+ maybeBindDerivedIdentity(node.left, node.right);
2068
+ }
2069
+ if (ts.isIdentifier(node)) {
2070
+ const anc = classifyAncestors(node);
2071
+ const memberUse = !anc.inImportOrExport && !anc.inTypeContext && !anc.inSkipped && !anc.inMockFactory ? memberUseForReceiver(node) : null;
2072
+ if (memberUse) {
2073
+ stateFor(memberUse.targetName).runtimeUses.push(memberUse.use);
2074
+ return;
2075
+ }
2076
+ const target = resolvedTargetName(node);
2077
+ if (target && !anc.inImportOrExport) {
2078
+ const targetNames = targetNamesForResolved(target);
2079
+ let kind = runtimeUseKind(node);
2080
+ // A bare JSX <X/> only counts as a render use when actually framework-rendered;
2081
+ // remember WHICH render call mounts it (its position drives the ordering check).
2082
+ let renderCall = null;
2083
+ if (kind === "render") {
2084
+ renderCall = jsxRenderCall(node);
2085
+ if (!renderCall)
2086
+ kind = null;
2087
+ }
2088
+ // render-arg (`render(X)`) is a render use ONLY when the render helper
2089
+ // resolves to a real test framework — a local `render` may ignore its arg.
2090
+ if (!kind) {
2091
+ const p = node.parent;
2092
+ if (p &&
2093
+ ts.isCallExpression(p) &&
2094
+ p.arguments.includes(node) &&
2095
+ isFrameworkRenderCall(p) &&
2096
+ renderCallIsLive(p)) {
2097
+ kind = "render";
2098
+ renderCall = p;
2099
+ }
2100
+ }
2101
+ for (const targetName of targetNames) {
2102
+ const st = stateFor(targetName);
2103
+ if (anc.inTypeContext)
2104
+ st.nonRuntimeReasons.add("used only in a type position");
2105
+ else if (anc.inMockFactory)
2106
+ st.nonRuntimeReasons.add("used only inside a mock factory");
2107
+ else if (anc.inSkipped)
2108
+ st.nonRuntimeReasons.add("used only inside a skipped/disabled test");
2109
+ else if (kind) {
2110
+ const tb = enclosingTestBlock(node);
2111
+ if (tb)
2112
+ st.runtimeUses.push({ node, useExpr: useExpressionOf(node), kind, block: tb, src: importSourceOf(node), renderCall });
2113
+ else
2114
+ st.nonRuntimeReasons.add("runtime use is outside any test block");
2115
+ }
2116
+ else if (isExpectArg(node))
2117
+ st.nonRuntimeReasons.add("passed to expect() but never called (shallow)");
2118
+ else
2119
+ st.nonRuntimeReasons.add("referenced but not called/rendered");
2120
+ }
2121
+ }
2122
+ else if (!target && nameSet.has(node.text) && !anc.inImportOrExport && isRuntimeUse(node)) {
2123
+ // a same-named binding used at runtime that does NOT resolve to the terminal impl
2124
+ stateFor(node.text).collision = true;
2125
+ }
2126
+ }
2127
+ ts.forEachChild(node, visit);
2128
+ };
2129
+ visit(testSf);
2130
+ // If the test MOCKS a framework package that provides render/screen/act, those helpers
2131
+ // are fakes — render() mounts nothing and screen returns seeded values — so a
2132
+ // render-effect confirmation is bogus. Detected by raw mocked specifier string (no
2133
+ // node_modules resolution needed). Value-flow confirmations are unaffected.
2134
+ const FRAMEWORK_TRUST_PKGS = [...FRAMEWORK_RENDER_PKGS, ...FRAMEWORK_DOM_PKGS, ...INVOKING_WRAPPER_PKGS];
2135
+ const frameworkMocked = [...mockedSpecifiers].some((s) => FRAMEWORK_TRUST_PKGS.some((p) => (p.endsWith("/") ? s.startsWith(p) : s === p || s.startsWith(p + "/"))));
2136
+ // Conjunct 5 (relatedness): a runtime use is "observed" only when an in-block
2137
+ // assertion is structurally connected to it — the result is WRAPPED by the
2138
+ // assertion (`expect(call())`), BOUND to a CONST the assertion references AFTER
2139
+ // the call (`const r = call(); expect(r)`), or the use is a render exercise
2140
+ // observed by a framework effect assertion (`render(<X/>); expect(screen…)`). A
2141
+ // co-located unrelated assertion, or a reassigned `let`, does NOT confirm.
2142
+ const enclosingAssertedConstVar = (useExpr, block) => {
2143
+ let n = useExpr.parent;
2144
+ while (n && n !== block) {
2145
+ if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && n.initializer && nodeContains(n.initializer, useExpr)) {
2146
+ const list = n.parent; // a let/var could be reassigned before the assertion — only const is safe.
2147
+ return list && ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0 ? n.name : null;
2148
+ }
2149
+ n = n.parent;
2150
+ }
2151
+ return null;
2152
+ };
2153
+ const argsReferenceSymbol = (args, sym) => {
2154
+ let found = false;
2155
+ const walk = (n) => {
2156
+ if (found)
2157
+ return;
2158
+ if (ts.isIdentifier(n) && checker.getSymbolAtLocation(n) === sym) {
2159
+ found = true;
2160
+ return;
2161
+ }
2162
+ ts.forEachChild(n, walk);
2163
+ };
2164
+ for (const arg of args)
2165
+ walk(arg);
2166
+ return found;
2167
+ };
2168
+ // The trailing method of a call's callee: `assert.ok` -> "ok", bare `assert(...)` -> "".
2169
+ const trailingMethod = (call) => {
2170
+ const ct = calleeText(call);
2171
+ return ct.includes(".") ? ct.slice(ct.lastIndexOf(".") + 1) : "";
2172
+ };
2173
+ // The arguments whose value the assertion observes: expect() observes its FIRST
2174
+ // argument PLUS the matcher call's arguments (the expected/comparand value, e.g.
2175
+ // `expect(actual).toEqual(use())`), but NOT expect's ignored 2nd+ argument; a
2176
+ // trusted assert observes its COMPARANDS only, never the trailing failure message
2177
+ // (`assert.ok(true, \`${use()}\`)` does not assert `use()`).
2178
+ const observedArgs = (a) => {
2179
+ if (a.kind === "expect") {
2180
+ const first = a.call.arguments.length > 0 ? [a.call.arguments[0]] : [];
2181
+ const m = expectMatcherCall(a.call);
2182
+ if (!m)
2183
+ return first;
2184
+ // A single-comparand matcher (toBe/toEqual/toContain/…) IGNORES a 2nd+ argument,
2185
+ // so a use there is not asserted (`expect(x).toBe(x, use())`); only the variadic
2186
+ // call/return families assert every argument.
2187
+ const mArgs = VARIADIC_MATCHER.test(calleeText(m)) ? m.arguments : m.arguments.slice(0, 1);
2188
+ return [...first, ...mArgs];
2189
+ }
2190
+ if (a.kind === "assert") {
2191
+ const count = ASSERT_COMPARANDS[trailingMethod(a.call)] ?? 1;
2192
+ return a.call.arguments.slice(0, count);
2193
+ }
2194
+ return a.call.arguments;
2195
+ };
2196
+ const constInitializerForIdentifier = (id, block) => {
2197
+ const sym = checker.getSymbolAtLocation(id);
2198
+ if (!sym)
2199
+ return null;
2200
+ for (const decl of sym.getDeclarations() ?? []) {
2201
+ if (ts.isVariableDeclaration(decl) &&
2202
+ ts.isIdentifier(decl.name) &&
2203
+ checker.getSymbolAtLocation(decl.name) === sym &&
2204
+ decl.initializer &&
2205
+ nodeContains(block, decl)) {
2206
+ const list = decl.parent;
2207
+ return list && ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0 ? decl.initializer : null;
2208
+ }
2209
+ }
2210
+ return null;
2211
+ };
2212
+ const receiverSymbolOfCall = (expr) => {
2213
+ const call = unwrapExpression(expr);
2214
+ if (!ts.isCallExpression(call) || !ts.isPropertyAccessExpression(call.expression))
2215
+ return null;
2216
+ const receiver = unwrapExpression(call.expression.expression);
2217
+ return ts.isIdentifier(receiver) ? checker.getSymbolAtLocation(receiver) ?? null : null;
2218
+ };
2219
+ const expressionContainsLaterCallOnReceiver = (expr, receiver, after) => {
2220
+ let found = false;
2221
+ const walk = (n) => {
2222
+ if (found)
2223
+ return;
2224
+ if (ts.isCallExpression(n) && n.getStart() > after) {
2225
+ const callee = unwrapExpression(n.expression);
2226
+ if (ts.isPropertyAccessExpression(callee)) {
2227
+ const recv = unwrapExpression(callee.expression);
2228
+ if (ts.isIdentifier(recv) && checker.getSymbolAtLocation(recv) === receiver) {
2229
+ found = true;
2230
+ return;
2231
+ }
2232
+ }
2233
+ }
2234
+ ts.forEachChild(n, walk);
2235
+ };
2236
+ walk(expr);
2237
+ return found;
2238
+ };
2239
+ const observesSameNestReceiverAfter = (u, a) => {
2240
+ if (u.block !== a.block || u.kind !== "value" || a.kind === "selfassert")
2241
+ return false;
2242
+ const receiver = receiverSymbolOfCall(u.useExpr);
2243
+ if (!receiver)
2244
+ return false;
2245
+ const args = observedArgs(a);
2246
+ for (const arg of args) {
2247
+ if (expressionContainsLaterCallOnReceiver(arg, receiver, u.useExpr.getStart()))
2248
+ return true;
2249
+ const value = ts.isIdentifier(unwrapExpression(arg)) ? constInitializerForIdentifier(unwrapExpression(arg), u.block) : null;
2250
+ if (value && expressionContainsLaterCallOnReceiver(value, receiver, u.useExpr.getStart()))
2251
+ return true;
2252
+ }
2253
+ return false;
2254
+ };
2255
+ // Whether the assertion INVOKES a function-expression argument (a use inside that
2256
+ // callback therefore executes): expect(() => f()).toThrow(), expect.poll(() => f()),
2257
+ // assert.throws(() => f()).
2258
+ const assertionInvokesCallback = (a) => {
2259
+ if (a.kind === "expect") {
2260
+ if (/(^|\.)expect\.poll$/.test(calleeText(a.call)))
2261
+ return true; // poll re-runs its callback
2262
+ const m = expectMatcherCall(a.call);
2263
+ return !!m && INVOKING_EXPECT_MATCHER.test(calleeText(m));
2264
+ }
2265
+ if (a.kind === "assert")
2266
+ return INVOKING_ASSERT_METHODS.has(trailingMethod(a.call));
2267
+ return false;
2268
+ };
2269
+ // Checker-gated HOF-callback test (built-in array/promise receivers only) for flowsInto.
2270
+ const invokedCallback = (fn) => isInvokedCallback(fn, checker);
2271
+ const observesUse = (u, a) => {
2272
+ if (u.block !== a.block)
2273
+ return false;
2274
+ // A self-assert query (getBy…/findBy…/should/…) observes a RENDER effect ONLY
2275
+ // (selfAssert.ts) — it never observes a plain value call via its arguments. The
2276
+ // WRAPPED and BOUND-const value paths apply only to expect/assert assertions AND a
2277
+ // VALUE use (a render use — a JSX element — is observed only by the render-effect
2278
+ // path below, never by asserting the element object itself).
2279
+ if (a.kind !== "selfassert" && u.kind === "value") {
2280
+ const args = observedArgs(a);
2281
+ const intoFns = assertionInvokesCallback(a);
2282
+ if (args.some((arg) => flowsInto(arg, u.useExpr, intoFns, invokedCallback)))
2283
+ return true; // WRAPPED (value flows in)
2284
+ const varName = enclosingAssertedConstVar(u.useExpr, u.block); // BOUND const + referenced after
2285
+ if (varName && a.call.getStart() > u.node.getStart()) {
2286
+ const sym = checker.getSymbolAtLocation(varName);
2287
+ if (sym && argsReferenceSymbol(args, sym))
2288
+ return true;
2289
+ }
2290
+ if (observesSameNestReceiverAfter(u, a))
2291
+ return true;
2292
+ }
2293
+ // render EFFECT-observed: the actual render() CALL must PRECEDE the effect assertion
2294
+ // (a `const ui = <X/>` declared early but render(ui)'d AFTER the assertion observes only
2295
+ // pre-render DOM), and the block must contain EXACTLY ONE framework render() call —
2296
+ // multiple distinct renders cannot be tied to the effect (a garbage 2nd component would
2297
+ // pass), and zero means nothing was actually rendered to the DOM.
2298
+ const renderPos = (u.renderCall ?? u.node).getStart();
2299
+ if (u.kind === "render" &&
2300
+ !frameworkMocked &&
2301
+ isFrameworkEffect(a.call) &&
2302
+ renderPos < a.call.getStart() &&
2303
+ renderCallsByBlock.get(u.block) === 1) {
2304
+ return true;
2305
+ }
2306
+ return false;
2307
+ };
2308
+ const implMocked = mockedFiles.has(implNorm);
2309
+ const srcMocked = (s) => implMocked || mockedFiles.has(s.file);
2310
+ for (const name of behaviorNames) {
2311
+ const st = stateFor(name);
2312
+ // CONFIRMED iff some runtime use's OWN binding is a clean, non-mocked terminal
2313
+ // AND that same use is observed by a related assertion — all evaluated against
2314
+ // the binding actually exercised (per-binding, not aggregated per-behavior).
2315
+ const confirmed = st.runtimeUses.some((u) => u.src != null && cleanBindingSource(u.src) && !srcMocked(u.src) && assertions.some((a) => observesUse(u, a)));
2316
+ if (confirmed) {
2317
+ if (hasNestTestingModuleHarness) {
2318
+ out.set(name, {
2319
+ verdict: "inferred",
2320
+ reason: "NestJS TestingModule harness: assertion observed on a real binding, but provider substitution via alias/container escape cannot be ruled out; treated as associated signal only",
2321
+ rejected_conjunct: 6
2322
+ });
2323
+ continue;
2324
+ }
2325
+ out.set(name, { verdict: "confirmed", reason: "runtime use of the real binding, observed by a related assertion (all 5 conjuncts)" });
2326
+ continue;
2327
+ }
2328
+ const importedToImpl = st.importedRuntime || st.importedType;
2329
+ if (!importedToImpl) {
2330
+ out.set(name, st.collision
2331
+ ? { verdict: "inferred", reason: "a binding with this name resolves to a non-terminal/other module, not the implementation", rejected_conjunct: 1 }
2332
+ : { verdict: "none", reason: "no structural link from this test to this behavior" });
2333
+ continue;
2334
+ }
2335
+ if (st.importedType && !st.importedRuntime) {
2336
+ out.set(name, { verdict: "inferred", reason: "type-only import — no runtime value is exercised", rejected_conjunct: 2 });
2337
+ continue;
2338
+ }
2339
+ if (implMocked || st.importSources.some((s) => mockedFiles.has(s.file))) {
2340
+ out.set(name, { verdict: "inferred", reason: "the imported module is mocked — the test does not hit real code", rejected_conjunct: 4 });
2341
+ continue;
2342
+ }
2343
+ if (!st.importSources.some((s) => cleanBindingSource(s))) {
2344
+ out.set(name, {
2345
+ verdict: "inferred",
2346
+ reason: "imported through a re-export that does not follow to a single terminal definition (ambiguous or non-terminal)",
2347
+ rejected_conjunct: 1
2348
+ });
2349
+ continue;
2350
+ }
2351
+ if (st.runtimeUses.length === 0) {
2352
+ const reason = st.nonRuntimeReasons.size > 0 ? [...st.nonRuntimeReasons].join("; ") : "imported but never used at runtime";
2353
+ const conj = [...st.nonRuntimeReasons].some((r) => r.includes("mock") || r.includes("skipped")) ? 4 : 3;
2354
+ out.set(name, { verdict: "inferred", reason, rejected_conjunct: conj });
2355
+ continue;
2356
+ }
2357
+ // Runtime uses + a clean import both exist, but no clean-binding use is observed
2358
+ // (the exercised binding is the ambiguous/mocked one, or no assertion is connected).
2359
+ out.set(name, {
2360
+ verdict: "inferred",
2361
+ reason: "the behavior is exercised but no assertion consumes its result or observes its rendered effect via the real binding",
2362
+ rejected_conjunct: 5
2363
+ });
2364
+ }
2365
+ return out;
2366
+ }
2367
+ /** `expect(X)` with X the bare binding — a shallow reference, not a runtime use. */
2368
+ function isExpectArg(id) {
2369
+ const p = id.parent;
2370
+ return !!p && ts.isCallExpression(p) && p.arguments.includes(id) && /(^|\.)expect$/.test(calleeText(p));
2371
+ }
2372
+ /** Single-pair convenience wrapper (used by the golden-fixture gate test). */
2373
+ export function confirmPair(ctx, testAbsPath, implAbsPath, behaviorName) {
2374
+ return (confirmBehaviors(ctx, testAbsPath, implAbsPath, [behaviorName]).get(behaviorName) ?? {
2375
+ verdict: "none",
2376
+ reason: "behavior not evaluated"
2377
+ });
2378
+ }
2379
+ /**
2380
+ * Run the confirmer over every candidate (test -> impl) pair (the resolver-derived
2381
+ * MAY_RELATE_TO links). For each denominator-eligible exported symbol of the impl
2382
+ * file, classify the pair and, on CONFIRMED, record a hard COVERS/TESTED_BY edge —
2383
+ * UNLESS the symbol was capped out of the graph, in which case it is downgraded to
2384
+ * INFERRED (4.4: COVERS stays symbol-precise, never falls back to the file).
2385
+ */
2386
+ export function runConfirmer(input) {
2387
+ const { candidates, symbolsByImpl, existingSymIds, anchorFile } = input;
2388
+ const confirmations = [];
2389
+ let attempted = 0;
2390
+ let capped_downgrades = 0;
2391
+ if (candidates.length === 0)
2392
+ return { confirmations, attempted, capped_downgrades };
2393
+ const absFiles = new Set();
2394
+ for (const c of candidates) {
2395
+ absFiles.add(c.testAbs);
2396
+ absFiles.add(c.implAbs);
2397
+ }
2398
+ const ctx = buildConfirmProgram([...absFiles], anchorFile);
2399
+ const seen = new Set(); // dedup (testRel|symId)
2400
+ for (const c of candidates) {
2401
+ const names = symbolsByImpl.get(c.implRel);
2402
+ if (!names || names.length === 0)
2403
+ continue;
2404
+ const verdicts = confirmBehaviors(ctx, c.testAbs, c.implAbs, names);
2405
+ for (const [name, v] of verdicts) {
2406
+ attempted++;
2407
+ if (v.verdict !== "confirmed")
2408
+ continue;
2409
+ const symId = `sym:${c.implRel}#${name}`;
2410
+ if (!existingSymIds.has(symId)) {
2411
+ capped_downgrades++; // symbol exists in source but was capped out — downgrade, don't COVERS-to-file
2412
+ continue;
2413
+ }
2414
+ const key = `${c.testRel}|${symId}`;
2415
+ if (seen.has(key))
2416
+ continue;
2417
+ seen.add(key);
2418
+ confirmations.push({ testRel: c.testRel, implRel: c.implRel, behaviorName: name, symId, reason: v.reason });
2419
+ }
2420
+ }
2421
+ return { confirmations, attempted, capped_downgrades };
2422
+ }