@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.
- package/LICENSE +21 -0
- package/README.md +328 -0
- package/dist/local/agentWorkflow.js +81 -0
- package/dist/local/aiGraph/links.js +635 -0
- package/dist/local/analyze/analyzer.js +2129 -0
- package/dist/local/analyze/behaviorContracts.js +169 -0
- package/dist/local/analyze/boilerplate.js +42 -0
- package/dist/local/analyze/callGraph.js +458 -0
- package/dist/local/analyze/classify.js +219 -0
- package/dist/local/analyze/clustering.js +357 -0
- package/dist/local/analyze/confirm.js +2422 -0
- package/dist/local/analyze/coverage.js +518 -0
- package/dist/local/analyze/coverageArtifacts.js +607 -0
- package/dist/local/analyze/frameworks.js +115 -0
- package/dist/local/analyze/linkage/conventions.js +160 -0
- package/dist/local/analyze/parseCache.js +164 -0
- package/dist/local/analyze/selfAssert.js +53 -0
- package/dist/local/analyze/symbols.js +430 -0
- package/dist/local/analyze/testLayer.js +135 -0
- package/dist/local/analyze/treeSitter/engine.js +1253 -0
- package/dist/local/analyze/treeSitter/languages.js +101 -0
- package/dist/local/autoProve.js +620 -0
- package/dist/local/cli.js +1468 -0
- package/dist/local/cliArgs.js +112 -0
- package/dist/local/corpusScope.js +162 -0
- package/dist/local/enrich/csv.js +348 -0
- package/dist/local/enrich/index.js +43 -0
- package/dist/local/enrich/markdown.js +193 -0
- package/dist/local/explain/explain.js +91 -0
- package/dist/local/exportCli.js +26 -0
- package/dist/local/flows/flowWalker.js +215 -0
- package/dist/local/flows/llmFlowDiscovery.js +567 -0
- package/dist/local/freshness/changed.js +280 -0
- package/dist/local/freshness/manifest.js +35 -0
- package/dist/local/freshness/status.js +30 -0
- package/dist/local/gaps/gaps.js +114 -0
- package/dist/local/generate/buckets.js +73 -0
- package/dist/local/generate/compareJudge.js +124 -0
- package/dist/local/generate/compareReport.js +538 -0
- package/dist/local/generate/compareScore.js +105 -0
- package/dist/local/generate/deriveImports.js +91 -0
- package/dist/local/generate/generator.js +2586 -0
- package/dist/local/generate/prompt.js +144 -0
- package/dist/local/generate/promptV5.js +438 -0
- package/dist/local/generate/providers.js +400 -0
- package/dist/local/generate/runHints.js +304 -0
- package/dist/local/graph/citations.js +73 -0
- package/dist/local/graph/confirmable.js +72 -0
- package/dist/local/graph/factories.js +210 -0
- package/dist/local/graph/ontology.js +18 -0
- package/dist/local/interactive.js +53 -0
- package/dist/local/jobs/jobStore.js +80 -0
- package/dist/local/jobs/notify.js +29 -0
- package/dist/local/jobs/runner.js +75 -0
- package/dist/local/ledger.js +117 -0
- package/dist/local/localConfig.js +112 -0
- package/dist/local/mcp.js +548 -0
- package/dist/local/operations.js +1749 -0
- package/dist/local/pack/coverageReport.js +192 -0
- package/dist/local/pack/exporter.js +195 -0
- package/dist/local/pack/schema.js +128 -0
- package/dist/local/pack/summary.js +127 -0
- package/dist/local/pack/validate.js +25 -0
- package/dist/local/proofRunnability.js +366 -0
- package/dist/local/recipe/dbSqljs.js +255 -0
- package/dist/local/reprove/paths.js +13 -0
- package/dist/local/reprove/scoped.js +136 -0
- package/dist/local/resolve/barrelWalker.js +178 -0
- package/dist/local/resolve/exportIndex.js +270 -0
- package/dist/local/resolve/importGraph.js +347 -0
- package/dist/local/resolve/resolver.js +122 -0
- package/dist/local/resolve/resolverCache.js +117 -0
- package/dist/local/rtm.js +413 -0
- package/dist/local/score/coverage.js +99 -0
- package/dist/local/score/doctor.js +67 -0
- package/dist/local/score/risk.js +362 -0
- package/dist/local/score/score.js +182 -0
- package/dist/local/types.js +1 -0
- package/dist/local/util/hash.js +16 -0
- package/dist/local/util/ids.js +16 -0
- package/dist/local/util/progress.js +8 -0
- package/dist/local/util/redact.js +39 -0
- package/dist/local/util/time.js +1 -0
- package/dist/local/util/walk.js +174 -0
- package/dist/local/viz/behaviorReportData.js +367 -0
- package/dist/local/viz/behaviorReportHtml.js +664 -0
- package/dist/local/viz/d3.bundle.js +3 -0
- package/dist/local/viz/html.js +1152 -0
- package/dist/local/viz/payload.js +525 -0
- package/dist/local/workspace.js +99 -0
- package/docs/agent-workflow.md +167 -0
- package/docs/agents/claude-code.md +43 -0
- package/docs/agents/codex.md +52 -0
- package/docs/agents/cursor.md +39 -0
- package/docs/agents/opencode.md +43 -0
- package/docs/agents/vscode.md +34 -0
- package/docs/local-proof-kit.md +269 -0
- package/package.json +92 -0
- package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
- package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
- package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
- package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
- 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
|
+
}
|