supercov 0.0.43 → 0.0.44
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/README.md +2 -2
- package/analyzers/typescript/README.md +4 -0
- package/analyzers/typescript/bin/identity.mjs +4 -0
- package/analyzers/typescript/dist/analyze.js +1352 -51
- package/analyzers/typescript/dist/archive.js +31 -3
- package/analyzers/typescript/dist/awaited-observations.js +376 -0
- package/analyzers/typescript/dist/build-identity.json +1 -1
- package/analyzers/typescript/dist/mock-counts.js +2517 -0
- package/analyzers/typescript/dist/pragmas.js +59 -16
- package/analyzers/typescript/src/analyze.ts +1738 -96
- package/analyzers/typescript/src/archive.ts +36 -3
- package/analyzers/typescript/src/awaited-observations.ts +561 -0
- package/analyzers/typescript/src/mock-counts.ts +3219 -0
- package/analyzers/typescript/src/pragmas.ts +90 -24
- package/docs/agent-loop.md +116 -31
- package/docs/assertion-evidence.md +560 -1
- package/docs/cli.md +15 -15
- package/docs/code-verification.md +3 -181
- package/docs/coverage-model.md +6 -6
- package/docs/evidence.md +6 -6
- package/docs/getting-started.md +60 -72
- package/docs/performance.md +4 -4
- package/docs/troubleshooting.md +8 -8
- package/docs/verification.md +2 -2
- package/docs/workspace-isolation.md +1 -1
- package/package.json +11 -9
- package/runtime/javascript/nodeAssertAdapter.mjs +32 -8
- package/runtime/javascript/nodeTest.mjs +13 -5
- package/runtime/javascript/runnerEvidence.mjs +33 -11
- package/runtime/javascript/runtime.mjs +22 -3
|
@@ -14,7 +14,24 @@ export const PROTOCOL = {
|
|
|
14
14
|
capabilities: [
|
|
15
15
|
"requiresTotal-v1",
|
|
16
16
|
"assertion-witness-issues-v1",
|
|
17
|
+
"complete-passed-test-inventory-v1",
|
|
18
|
+
"witnessed-callback-scope-v1",
|
|
17
19
|
"assertion-hints-v1",
|
|
20
|
+
"awaited-observation-sources-v1",
|
|
21
|
+
"first-test-call-omission-v1",
|
|
22
|
+
"closed-count-sensitivity-v1",
|
|
23
|
+
"closed-payload-sensitivity-v1",
|
|
24
|
+
"payload-native-predicates-v1",
|
|
25
|
+
"first-test-direct-return-v1",
|
|
26
|
+
"mock-observation-projections-v1",
|
|
27
|
+
"assertion-comparison-relations-v2",
|
|
28
|
+
"process-exit-source-v1",
|
|
29
|
+
"process-exit-consumer-v1",
|
|
30
|
+
"mock-count-lifetimes-v1",
|
|
31
|
+
"mock-count-factories-v1",
|
|
32
|
+
"mock-count-rows-v1",
|
|
33
|
+
"mock-count-array-projections-v1",
|
|
34
|
+
"primitive-decision-sensitivity-v1",
|
|
18
35
|
],
|
|
19
36
|
};
|
|
20
37
|
type Location = {
|
|
@@ -102,6 +119,7 @@ type RecordData = {
|
|
|
102
119
|
status?: string;
|
|
103
120
|
expectedStatus?: string;
|
|
104
121
|
flaky?: boolean;
|
|
122
|
+
provenance?: { runner?: string };
|
|
105
123
|
scope?: Scope;
|
|
106
124
|
runtime: Snapshot[];
|
|
107
125
|
browser: Snapshot[];
|
|
@@ -216,7 +234,9 @@ function analyzeArchiveWithFrontend(
|
|
|
216
234
|
atoms.forEach((n, i) => {
|
|
217
235
|
const begin = sf.getLineAndCharacterOfPosition(n.getStart(sf)),
|
|
218
236
|
end = sf.getLineAndCharacterOfPosition(n.getEnd());
|
|
219
|
-
//
|
|
237
|
+
// This owner also participates in source-hint selection. Named arrow
|
|
238
|
+
// bindings must not be mislabeled <module> merely because they are not
|
|
239
|
+
// function declarations.
|
|
220
240
|
let parent: ts.Node | undefined = n.parent,
|
|
221
241
|
owner = "<module>";
|
|
222
242
|
while (parent) {
|
|
@@ -224,6 +244,15 @@ function analyzeArchiveWithFrontend(
|
|
|
224
244
|
owner = parent.name.text;
|
|
225
245
|
break;
|
|
226
246
|
}
|
|
247
|
+
if (
|
|
248
|
+
(compiler.isArrowFunction(parent) ||
|
|
249
|
+
compiler.isFunctionExpression(parent)) &&
|
|
250
|
+
compiler.isVariableDeclaration(parent.parent) &&
|
|
251
|
+
compiler.isIdentifier(parent.parent.name)
|
|
252
|
+
) {
|
|
253
|
+
owner = parent.parent.name.text;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
227
256
|
parent = parent.parent;
|
|
228
257
|
}
|
|
229
258
|
sites.push({
|
|
@@ -290,8 +319,11 @@ function analyzeArchiveWithFrontend(
|
|
|
290
319
|
for (const [i, r] of accepted.entries()) {
|
|
291
320
|
const id = `A${i + 1}`;
|
|
292
321
|
if (!r.testFile) {
|
|
293
|
-
|
|
294
|
-
|
|
322
|
+
// Dropping even one passing attempt could turn its executed/asserted sites
|
|
323
|
+
// into apparently certain gaps. Ordinary coverage can still be queried.
|
|
324
|
+
throw new Error(
|
|
325
|
+
"A passed test has no source file; assertion analysis requires test-source provenance. Recapture with a supported runner and stack formatter.",
|
|
326
|
+
);
|
|
295
327
|
}
|
|
296
328
|
if (r.browser.length)
|
|
297
329
|
limitations.add(
|
|
@@ -393,6 +425,7 @@ function analyzeArchiveWithFrontend(
|
|
|
393
425
|
line: 0,
|
|
394
426
|
ok: true,
|
|
395
427
|
phaseLines,
|
|
428
|
+
runner: r.provenance?.runner,
|
|
396
429
|
});
|
|
397
430
|
attempts.push({
|
|
398
431
|
id,
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import type ts from "typescript";
|
|
2
|
+
import type { SyntaxAPI } from "./frontend.js";
|
|
3
|
+
|
|
4
|
+
/** Source applicability only. This is not a runtime observation or a witness. */
|
|
5
|
+
export interface AwaitedObservationSource {
|
|
6
|
+
model: "node-child-capture-poll-v1";
|
|
7
|
+
factorySource: string;
|
|
8
|
+
predicateSource: string;
|
|
9
|
+
captures: { stream: "stdout" | "stderr"; source: string }[];
|
|
10
|
+
pattern: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Context {
|
|
14
|
+
syntax: SyntaxAPI;
|
|
15
|
+
declaration(node: ts.Node): ts.Declaration | undefined;
|
|
16
|
+
rawDeclaration(node: ts.Node): ts.Declaration | undefined;
|
|
17
|
+
relativeFile(file: ts.SourceFile): string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Recognize a direct awaited factory method which polls an append-only native
|
|
22
|
+
* child capture. Names, paths, regex and timeout values come from bindings/source.
|
|
23
|
+
* Unsupported control flow or aliases are not evidence of a missing assertion.
|
|
24
|
+
* Native APIs and absence of external monkey-patching remain model premises.
|
|
25
|
+
*/
|
|
26
|
+
export function awaitedObservationSource(
|
|
27
|
+
context: Context,
|
|
28
|
+
callback: ts.Node,
|
|
29
|
+
call: ts.CallExpression,
|
|
30
|
+
): AwaitedObservationSource | undefined {
|
|
31
|
+
const { syntax: t, declaration: decl, rawDeclaration: raw } = context;
|
|
32
|
+
function require(value: unknown): asserts value {
|
|
33
|
+
if (!value) throw unsupported;
|
|
34
|
+
}
|
|
35
|
+
const unsupported = Symbol("unsupported awaited observation");
|
|
36
|
+
const nodes = (root: ts.Node, predicate: (node: ts.Node) => boolean) => {
|
|
37
|
+
const found: ts.Node[] = [];
|
|
38
|
+
const visit = (n: ts.Node) => {
|
|
39
|
+
if (predicate(n)) found.push(n);
|
|
40
|
+
t.forEachChild(n, visit);
|
|
41
|
+
};
|
|
42
|
+
visit(root);
|
|
43
|
+
return found;
|
|
44
|
+
};
|
|
45
|
+
const one = <N extends ts.Node>(values: readonly N[]): N => {
|
|
46
|
+
require(values.length === 1);
|
|
47
|
+
return values[0];
|
|
48
|
+
};
|
|
49
|
+
const at = (node: ts.Node) => {
|
|
50
|
+
const sf = node.getSourceFile(),
|
|
51
|
+
p = sf.getLineAndCharacterOfPosition(node.getStart(sf));
|
|
52
|
+
return `${context.relativeFile(sf)}:${p.line + 1}:${p.character + 1}`;
|
|
53
|
+
};
|
|
54
|
+
const importedFrom = (node: ts.Node, name: string): string | undefined => {
|
|
55
|
+
const d = raw(node);
|
|
56
|
+
return !!d &&
|
|
57
|
+
t.isImportSpecifier(d) &&
|
|
58
|
+
!d.isTypeOnly &&
|
|
59
|
+
!d.parent.parent.isTypeOnly &&
|
|
60
|
+
(d.propertyName ?? d.name).text === name &&
|
|
61
|
+
t.isStringLiteral(d.parent.parent.parent.moduleSpecifier)
|
|
62
|
+
? d.parent.parent.parent.moduleSpecifier.text
|
|
63
|
+
: undefined;
|
|
64
|
+
};
|
|
65
|
+
const constant = (node: ts.Node): node is ts.VariableDeclaration =>
|
|
66
|
+
t.isVariableDeclaration(node) &&
|
|
67
|
+
t.isIdentifier(node.name) &&
|
|
68
|
+
t.isVariableDeclarationList(node.parent) &&
|
|
69
|
+
!!(node.parent.flags & t.NodeFlags.Const) &&
|
|
70
|
+
node.parent.declarations.length === 1;
|
|
71
|
+
const simpleArrow = (node: ts.Node): node is ts.ArrowFunction =>
|
|
72
|
+
t.isArrowFunction(node) &&
|
|
73
|
+
node.parameters.length === 0 &&
|
|
74
|
+
!node.modifiers?.length;
|
|
75
|
+
const bodyExpression = (node: ts.ArrowFunction): ts.Expression => {
|
|
76
|
+
if (!t.isBlock(node.body)) return node.body;
|
|
77
|
+
require(node.body.statements.length === 1);
|
|
78
|
+
const returned = node.body.statements[0];
|
|
79
|
+
require(t.isReturnStatement(returned) && returned.expression);
|
|
80
|
+
return returned.expression;
|
|
81
|
+
};
|
|
82
|
+
const positiveNumber = (node: ts.Node) =>
|
|
83
|
+
t.isNumericLiteral(node) &&
|
|
84
|
+
Number.isFinite(Number(node.text)) &&
|
|
85
|
+
Number(node.text) > 0;
|
|
86
|
+
const global = (node: ts.Node, name: string) => {
|
|
87
|
+
const d = decl(node);
|
|
88
|
+
return (
|
|
89
|
+
t.isIdentifier(node) &&
|
|
90
|
+
node.text === name &&
|
|
91
|
+
(!d || d.getSourceFile().isDeclarationFile)
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
const dateNow = (node: ts.Node) =>
|
|
95
|
+
t.isCallExpression(node) &&
|
|
96
|
+
!node.questionDotToken &&
|
|
97
|
+
node.arguments.length === 0 &&
|
|
98
|
+
t.isPropertyAccessExpression(node.expression) &&
|
|
99
|
+
node.expression.name.text === "now" &&
|
|
100
|
+
global(node.expression.expression, "Date");
|
|
101
|
+
const references = (root: ts.Node, binding: ts.Declaration) =>
|
|
102
|
+
nodes(root, (n) => t.isIdentifier(n) && decl(n) === binding);
|
|
103
|
+
try {
|
|
104
|
+
require(
|
|
105
|
+
t.isArrowFunction(callback) &&
|
|
106
|
+
t.isBlock(callback.body) &&
|
|
107
|
+
callback.modifiers?.some((m) => m.kind === t.SyntaxKind.AsyncKeyword),
|
|
108
|
+
);
|
|
109
|
+
const registration = callback.parent;
|
|
110
|
+
require(
|
|
111
|
+
t.isCallExpression(registration) &&
|
|
112
|
+
registration.arguments.at(-1) === callback &&
|
|
113
|
+
importedFrom(registration.expression, "test") === "node:test" &&
|
|
114
|
+
t.isExpressionStatement(registration.parent) &&
|
|
115
|
+
t.isSourceFile(registration.parent.parent),
|
|
116
|
+
);
|
|
117
|
+
require(
|
|
118
|
+
callback.body.statements.every(
|
|
119
|
+
(s) => t.isVariableStatement(s) || t.isExpressionStatement(s),
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
require(
|
|
123
|
+
t.isAwaitExpression(call.parent) &&
|
|
124
|
+
t.isExpressionStatement(call.parent.parent) &&
|
|
125
|
+
call.parent.parent.parent === callback.body &&
|
|
126
|
+
call.arguments.length === 0 &&
|
|
127
|
+
!call.questionDotToken &&
|
|
128
|
+
t.isPropertyAccessExpression(call.expression) &&
|
|
129
|
+
!call.expression.questionDotToken,
|
|
130
|
+
);
|
|
131
|
+
const member = call.expression.name.text,
|
|
132
|
+
receiver = decl(call.expression.expression);
|
|
133
|
+
require(
|
|
134
|
+
receiver &&
|
|
135
|
+
constant(receiver) &&
|
|
136
|
+
receiver.initializer &&
|
|
137
|
+
t.isCallExpression(receiver.initializer),
|
|
138
|
+
);
|
|
139
|
+
const launch = receiver.initializer,
|
|
140
|
+
factory = decl(launch.expression);
|
|
141
|
+
require(
|
|
142
|
+
factory &&
|
|
143
|
+
t.isFunctionDeclaration(factory) &&
|
|
144
|
+
factory.body &&
|
|
145
|
+
!factory.asteriskToken &&
|
|
146
|
+
!factory.modifiers?.some((m) => m.kind === t.SyntaxKind.AsyncKeyword) &&
|
|
147
|
+
factory.parameters.every(
|
|
148
|
+
(p) => t.isIdentifier(p.name) && !p.initializer && !p.dotDotDotToken,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
require(receiver.parent.parent.parent === callback.body);
|
|
152
|
+
const statements: readonly ts.Statement[] = callback.body.statements;
|
|
153
|
+
require(
|
|
154
|
+
statements.indexOf(call.parent.parent) ===
|
|
155
|
+
statements.indexOf(receiver.parent.parent as ts.Statement) + 1,
|
|
156
|
+
);
|
|
157
|
+
require(
|
|
158
|
+
nodes(
|
|
159
|
+
callback,
|
|
160
|
+
(n) => t.isCallExpression(n) && decl(n.expression) === factory,
|
|
161
|
+
).length === 1,
|
|
162
|
+
);
|
|
163
|
+
require(
|
|
164
|
+
nodes(
|
|
165
|
+
callback,
|
|
166
|
+
(n) =>
|
|
167
|
+
t.isCallExpression(n) &&
|
|
168
|
+
t.isPropertyAccessExpression(n.expression) &&
|
|
169
|
+
decl(n.expression.expression) === receiver &&
|
|
170
|
+
n.expression.name.text === member,
|
|
171
|
+
).length === 1,
|
|
172
|
+
);
|
|
173
|
+
require(
|
|
174
|
+
nodes(
|
|
175
|
+
callback,
|
|
176
|
+
(n) => t.isIdentifier(n) && ["eval", "Function"].includes(n.text),
|
|
177
|
+
).length === 0,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const factoryBody = factory.body;
|
|
181
|
+
require(
|
|
182
|
+
factoryBody.statements.every(
|
|
183
|
+
(s) =>
|
|
184
|
+
t.isVariableStatement(s) ||
|
|
185
|
+
t.isExpressionStatement(s) ||
|
|
186
|
+
t.isReturnStatement(s),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
const returned = one(factoryBody.statements.filter(t.isReturnStatement));
|
|
190
|
+
require(
|
|
191
|
+
returned === factoryBody.statements.at(-1) &&
|
|
192
|
+
returned.expression &&
|
|
193
|
+
t.isObjectLiteralExpression(returned.expression),
|
|
194
|
+
);
|
|
195
|
+
const members = returned.expression.properties;
|
|
196
|
+
require(
|
|
197
|
+
members.every(
|
|
198
|
+
(p) =>
|
|
199
|
+
(t.isPropertyAssignment(p) || t.isShorthandPropertyAssignment(p)) &&
|
|
200
|
+
t.isIdentifier(p.name),
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
require(
|
|
204
|
+
new Set(members.map((p) => p.name!.getText())).size === members.length,
|
|
205
|
+
);
|
|
206
|
+
const property = one(members.filter((p) => p.name?.getText() === member));
|
|
207
|
+
require(
|
|
208
|
+
t.isPropertyAssignment(property) && simpleArrow(property.initializer),
|
|
209
|
+
);
|
|
210
|
+
const pollCall = bodyExpression(property.initializer);
|
|
211
|
+
require(
|
|
212
|
+
t.isCallExpression(pollCall) &&
|
|
213
|
+
!pollCall.questionDotToken &&
|
|
214
|
+
pollCall.arguments.length >= 1,
|
|
215
|
+
);
|
|
216
|
+
const poll = decl(pollCall.expression);
|
|
217
|
+
require(
|
|
218
|
+
poll &&
|
|
219
|
+
constant(poll) &&
|
|
220
|
+
poll.parent.parent.parent === factoryBody &&
|
|
221
|
+
poll.initializer &&
|
|
222
|
+
t.isArrowFunction(poll.initializer) &&
|
|
223
|
+
t.isBlock(poll.initializer.body) &&
|
|
224
|
+
poll.initializer.modifiers?.some(
|
|
225
|
+
(m) => m.kind === t.SyntaxKind.AsyncKeyword,
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
const pollFn = poll.initializer,
|
|
229
|
+
pollBody = pollFn.body as ts.Block;
|
|
230
|
+
require(
|
|
231
|
+
pollFn.parameters.length === pollCall.arguments.length &&
|
|
232
|
+
pollFn.parameters.every(
|
|
233
|
+
(p) => t.isIdentifier(p.name) && !p.initializer && !p.dotDotDotToken,
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
require(
|
|
237
|
+
pollBody.statements.length === 2 &&
|
|
238
|
+
t.isVariableStatement(pollBody.statements[0]),
|
|
239
|
+
);
|
|
240
|
+
const deadline = one(pollBody.statements[0].declarationList.declarations);
|
|
241
|
+
require(
|
|
242
|
+
constant(deadline) &&
|
|
243
|
+
deadline.initializer &&
|
|
244
|
+
t.isBinaryExpression(deadline.initializer) &&
|
|
245
|
+
deadline.initializer.operatorToken.kind === t.SyntaxKind.PlusToken &&
|
|
246
|
+
dateNow(deadline.initializer.left) &&
|
|
247
|
+
positiveNumber(deadline.initializer.right),
|
|
248
|
+
);
|
|
249
|
+
const loop = pollBody.statements[1];
|
|
250
|
+
require(
|
|
251
|
+
t.isWhileStatement(loop) &&
|
|
252
|
+
t.isPrefixUnaryExpression(loop.expression) &&
|
|
253
|
+
loop.expression.operator === t.SyntaxKind.ExclamationToken &&
|
|
254
|
+
t.isCallExpression(loop.expression.operand),
|
|
255
|
+
);
|
|
256
|
+
const predicateCall = loop.expression.operand;
|
|
257
|
+
require(
|
|
258
|
+
predicateCall.arguments.length === 0 &&
|
|
259
|
+
decl(predicateCall.expression) === pollFn.parameters[0] &&
|
|
260
|
+
!predicateCall.questionDotToken &&
|
|
261
|
+
t.isBlock(loop.statement) &&
|
|
262
|
+
loop.statement.statements.length === 2,
|
|
263
|
+
);
|
|
264
|
+
require(
|
|
265
|
+
references(pollFn, pollFn.parameters[0]).every(
|
|
266
|
+
(n) =>
|
|
267
|
+
n === pollFn.parameters[0].name || n === predicateCall.expression,
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
const [guard, pause] = loop.statement.statements;
|
|
271
|
+
require(
|
|
272
|
+
t.isIfStatement(guard) &&
|
|
273
|
+
!guard.elseStatement &&
|
|
274
|
+
t.isBlock(guard.thenStatement) &&
|
|
275
|
+
guard.thenStatement.statements.length === 1 &&
|
|
276
|
+
t.isThrowStatement(guard.thenStatement.statements[0]),
|
|
277
|
+
);
|
|
278
|
+
require(
|
|
279
|
+
t.isExpressionStatement(pause) &&
|
|
280
|
+
t.isAwaitExpression(pause.expression) &&
|
|
281
|
+
t.isCallExpression(pause.expression.expression),
|
|
282
|
+
);
|
|
283
|
+
const delay = pause.expression.expression;
|
|
284
|
+
require(
|
|
285
|
+
importedFrom(delay.expression, "setTimeout") === "node:timers/promises" &&
|
|
286
|
+
!delay.questionDotToken &&
|
|
287
|
+
delay.arguments.length === 1 &&
|
|
288
|
+
positiveNumber(delay.arguments[0]),
|
|
289
|
+
);
|
|
290
|
+
const predicate = pollCall.arguments[0];
|
|
291
|
+
require(simpleArrow(predicate));
|
|
292
|
+
const read = bodyExpression(predicate);
|
|
293
|
+
require(
|
|
294
|
+
t.isCallExpression(read) &&
|
|
295
|
+
!read.questionDotToken &&
|
|
296
|
+
read.arguments.length === 1 &&
|
|
297
|
+
t.isPropertyAccessExpression(read.expression) &&
|
|
298
|
+
read.expression.name.text === "test" &&
|
|
299
|
+
t.isRegularExpressionLiteral(read.expression.expression),
|
|
300
|
+
);
|
|
301
|
+
const regex = read.expression.expression.text;
|
|
302
|
+
require(!/[gy]/.test(regex.slice(regex.lastIndexOf("/") + 1)));
|
|
303
|
+
const input = read.arguments[0];
|
|
304
|
+
const operands =
|
|
305
|
+
t.isBinaryExpression(input) &&
|
|
306
|
+
input.operatorToken.kind === t.SyntaxKind.PlusToken
|
|
307
|
+
? [input.left, input.right]
|
|
308
|
+
: [input];
|
|
309
|
+
const buffers = operands.map((o) => decl(o));
|
|
310
|
+
require(
|
|
311
|
+
buffers.every((b) => b && t.isVariableDeclaration(b)) &&
|
|
312
|
+
new Set(buffers).size === buffers.length,
|
|
313
|
+
);
|
|
314
|
+
let child: ts.VariableDeclaration | undefined;
|
|
315
|
+
const captures: AwaitedObservationSource["captures"] = [];
|
|
316
|
+
for (const [index, maybeBuffer] of buffers.entries()) {
|
|
317
|
+
const buffer = maybeBuffer as ts.VariableDeclaration;
|
|
318
|
+
require(
|
|
319
|
+
buffer.parent.parent.parent === factoryBody &&
|
|
320
|
+
t.isVariableDeclarationList(buffer.parent) &&
|
|
321
|
+
!!(buffer.parent.flags & t.NodeFlags.Let) &&
|
|
322
|
+
t.isIdentifier(buffer.name) &&
|
|
323
|
+
buffer.initializer &&
|
|
324
|
+
t.isStringLiteral(buffer.initializer) &&
|
|
325
|
+
buffer.initializer.text === "",
|
|
326
|
+
);
|
|
327
|
+
const append = one(
|
|
328
|
+
nodes(
|
|
329
|
+
factoryBody,
|
|
330
|
+
(n) =>
|
|
331
|
+
t.isBinaryExpression(n) &&
|
|
332
|
+
decl(n.left) === buffer &&
|
|
333
|
+
n.operatorToken.kind >= t.SyntaxKind.FirstAssignment &&
|
|
334
|
+
n.operatorToken.kind <= t.SyntaxKind.LastAssignment,
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
require(
|
|
338
|
+
t.isBinaryExpression(append) &&
|
|
339
|
+
append.operatorToken.kind === t.SyntaxKind.PlusEqualsToken &&
|
|
340
|
+
t.isExpressionStatement(append.parent) &&
|
|
341
|
+
t.isBlock(append.parent.parent),
|
|
342
|
+
);
|
|
343
|
+
const capture = append.parent.parent.parent;
|
|
344
|
+
require(
|
|
345
|
+
t.isArrowFunction(capture) &&
|
|
346
|
+
!capture.modifiers?.length &&
|
|
347
|
+
capture.parameters.length === 1 &&
|
|
348
|
+
!capture.parameters[0].initializer &&
|
|
349
|
+
!capture.parameters[0].dotDotDotToken &&
|
|
350
|
+
decl(append.right) === capture.parameters[0] &&
|
|
351
|
+
t.isBlock(capture.body) &&
|
|
352
|
+
capture.body.statements.length === 1,
|
|
353
|
+
);
|
|
354
|
+
const on = capture.parent;
|
|
355
|
+
require(
|
|
356
|
+
t.isCallExpression(on) &&
|
|
357
|
+
on.arguments.length === 2 &&
|
|
358
|
+
on.arguments[1] === capture &&
|
|
359
|
+
!on.questionDotToken &&
|
|
360
|
+
t.isStringLiteral(on.arguments[0]) &&
|
|
361
|
+
on.arguments[0].text === "data" &&
|
|
362
|
+
t.isPropertyAccessExpression(on.expression) &&
|
|
363
|
+
!on.expression.questionDotToken &&
|
|
364
|
+
on.expression.name.text === "on" &&
|
|
365
|
+
t.isCallExpression(on.expression.expression),
|
|
366
|
+
);
|
|
367
|
+
const encoding = on.expression.expression;
|
|
368
|
+
require(
|
|
369
|
+
!encoding.questionDotToken &&
|
|
370
|
+
encoding.arguments.length === 1 &&
|
|
371
|
+
t.isStringLiteral(encoding.arguments[0]) &&
|
|
372
|
+
encoding.arguments[0].text === "utf8" &&
|
|
373
|
+
t.isPropertyAccessExpression(encoding.expression) &&
|
|
374
|
+
encoding.expression.name.text === "setEncoding" &&
|
|
375
|
+
!encoding.expression.questionDotToken &&
|
|
376
|
+
t.isPropertyAccessExpression(encoding.expression.expression),
|
|
377
|
+
);
|
|
378
|
+
const stream = encoding.expression.expression;
|
|
379
|
+
require(
|
|
380
|
+
!stream.questionDotToken &&
|
|
381
|
+
(stream.name.text === "stdout" || stream.name.text === "stderr"),
|
|
382
|
+
);
|
|
383
|
+
const streamChild = decl(stream.expression);
|
|
384
|
+
require(
|
|
385
|
+
streamChild &&
|
|
386
|
+
constant(streamChild) &&
|
|
387
|
+
(!child || child === streamChild),
|
|
388
|
+
);
|
|
389
|
+
child = streamChild;
|
|
390
|
+
require(
|
|
391
|
+
t.isExpressionStatement(on.parent) && on.parent.parent === factoryBody,
|
|
392
|
+
);
|
|
393
|
+
// Only initialization, append, the predicate, trivial accessors and the
|
|
394
|
+
// throw diagnostic can read this binding. No transformed/escaped capture.
|
|
395
|
+
const allowed = new Set<ts.Node>([
|
|
396
|
+
buffer.name,
|
|
397
|
+
append.left,
|
|
398
|
+
operands[index],
|
|
399
|
+
...references(guard.thenStatement, buffer),
|
|
400
|
+
]);
|
|
401
|
+
for (const p of members)
|
|
402
|
+
if (t.isPropertyAssignment(p) && simpleArrow(p.initializer)) {
|
|
403
|
+
const value = bodyExpression(p.initializer);
|
|
404
|
+
if (decl(value) === buffer) allowed.add(value);
|
|
405
|
+
}
|
|
406
|
+
require(references(factoryBody, buffer).every((n) => allowed.has(n)));
|
|
407
|
+
const streamRefs = nodes(
|
|
408
|
+
factoryBody,
|
|
409
|
+
(n) =>
|
|
410
|
+
t.isPropertyAccessExpression(n) &&
|
|
411
|
+
decl(n.expression) === child &&
|
|
412
|
+
n.name.text === stream.name.text,
|
|
413
|
+
);
|
|
414
|
+
require(streamRefs.length === 1);
|
|
415
|
+
captures.push({ stream: stream.name.text, source: at(append) });
|
|
416
|
+
}
|
|
417
|
+
require(
|
|
418
|
+
child &&
|
|
419
|
+
child.initializer &&
|
|
420
|
+
t.isCallExpression(child.initializer) &&
|
|
421
|
+
child.parent.parent.parent === factoryBody &&
|
|
422
|
+
importedFrom(child.initializer.expression, "spawn") ===
|
|
423
|
+
"node:child_process",
|
|
424
|
+
);
|
|
425
|
+
const spawn = child.initializer;
|
|
426
|
+
require(
|
|
427
|
+
!spawn.questionDotToken &&
|
|
428
|
+
spawn.arguments.length === 3 &&
|
|
429
|
+
t.isObjectLiteralExpression(spawn.arguments[2]),
|
|
430
|
+
);
|
|
431
|
+
const options = spawn.arguments[2].properties;
|
|
432
|
+
require(
|
|
433
|
+
options.every(
|
|
434
|
+
(p) => t.isPropertyAssignment(p) && t.isIdentifier(p.name),
|
|
435
|
+
) &&
|
|
436
|
+
new Set(options.map((p) => p.name!.getText())).size === options.length,
|
|
437
|
+
);
|
|
438
|
+
const stdio = one(options.filter((p) => p.name?.getText() === "stdio"));
|
|
439
|
+
require(
|
|
440
|
+
t.isPropertyAssignment(stdio) &&
|
|
441
|
+
t.isStringLiteral(stdio.initializer) &&
|
|
442
|
+
stdio.initializer.text === "pipe",
|
|
443
|
+
);
|
|
444
|
+
for (const ref of references(factoryBody, child)) {
|
|
445
|
+
if (
|
|
446
|
+
ref === child.name ||
|
|
447
|
+
(t.isShorthandPropertyAssignment(ref.parent) &&
|
|
448
|
+
ref.parent.parent === returned.expression)
|
|
449
|
+
)
|
|
450
|
+
continue;
|
|
451
|
+
require(
|
|
452
|
+
t.isPropertyAccessExpression(ref.parent) &&
|
|
453
|
+
ref.parent.expression === ref,
|
|
454
|
+
);
|
|
455
|
+
const access = ref.parent;
|
|
456
|
+
require(
|
|
457
|
+
[
|
|
458
|
+
"stdout",
|
|
459
|
+
"stderr",
|
|
460
|
+
"exitCode",
|
|
461
|
+
"signalCode",
|
|
462
|
+
"pid",
|
|
463
|
+
"once",
|
|
464
|
+
"kill",
|
|
465
|
+
].includes(access.name.text),
|
|
466
|
+
);
|
|
467
|
+
require(
|
|
468
|
+
!(
|
|
469
|
+
t.isBinaryExpression(access.parent) &&
|
|
470
|
+
access.parent.left === access &&
|
|
471
|
+
access.parent.operatorToken.kind >= t.SyntaxKind.FirstAssignment &&
|
|
472
|
+
access.parent.operatorToken.kind <= t.SyntaxKind.LastAssignment
|
|
473
|
+
) &&
|
|
474
|
+
!t.isDeleteExpression(access.parent) &&
|
|
475
|
+
!t.isPostfixUnaryExpression(access.parent) &&
|
|
476
|
+
!(
|
|
477
|
+
t.isPrefixUnaryExpression(access.parent) &&
|
|
478
|
+
[t.SyntaxKind.PlusPlusToken, t.SyntaxKind.MinusMinusToken].includes(
|
|
479
|
+
access.parent.operator,
|
|
480
|
+
)
|
|
481
|
+
),
|
|
482
|
+
);
|
|
483
|
+
if (["once", "kill"].includes(access.name.text))
|
|
484
|
+
require(
|
|
485
|
+
t.isCallExpression(access.parent) &&
|
|
486
|
+
access.parent.expression === access,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
const disjuncts = (n: ts.Expression): ts.Expression[] =>
|
|
490
|
+
t.isBinaryExpression(n) &&
|
|
491
|
+
n.operatorToken.kind === t.SyntaxKind.BarBarToken
|
|
492
|
+
? [...disjuncts(n.left), ...disjuncts(n.right)]
|
|
493
|
+
: [n];
|
|
494
|
+
const checks = disjuncts(guard.expression);
|
|
495
|
+
require(checks.length === 3);
|
|
496
|
+
for (const [index, name] of ["exitCode", "signalCode"].entries()) {
|
|
497
|
+
const check = checks[index];
|
|
498
|
+
require(
|
|
499
|
+
t.isBinaryExpression(check) &&
|
|
500
|
+
check.operatorToken.kind ===
|
|
501
|
+
t.SyntaxKind.ExclamationEqualsEqualsToken &&
|
|
502
|
+
check.right.kind === t.SyntaxKind.NullKeyword &&
|
|
503
|
+
t.isPropertyAccessExpression(check.left) &&
|
|
504
|
+
check.left.name.text === name &&
|
|
505
|
+
decl(check.left.expression) === child,
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
const expired = checks[2];
|
|
509
|
+
require(
|
|
510
|
+
t.isBinaryExpression(expired) &&
|
|
511
|
+
expired.operatorToken.kind === t.SyntaxKind.GreaterThanToken &&
|
|
512
|
+
dateNow(expired.left) &&
|
|
513
|
+
decl(expired.right) === deadline,
|
|
514
|
+
);
|
|
515
|
+
require(
|
|
516
|
+
references(factoryBody, poll).every(
|
|
517
|
+
(n) =>
|
|
518
|
+
n === poll.name ||
|
|
519
|
+
n === pollCall.expression ||
|
|
520
|
+
(t.isShorthandPropertyAssignment(n.parent) &&
|
|
521
|
+
n.parent.parent === returned.expression),
|
|
522
|
+
),
|
|
523
|
+
);
|
|
524
|
+
for (const ref of references(callback, receiver)) {
|
|
525
|
+
if (ref === receiver.name) continue;
|
|
526
|
+
require(
|
|
527
|
+
t.isPropertyAccessExpression(ref.parent) &&
|
|
528
|
+
ref.parent.expression === ref,
|
|
529
|
+
);
|
|
530
|
+
const use = ref.parent;
|
|
531
|
+
require(t.isCallExpression(use.parent) && use.parent.expression === use);
|
|
532
|
+
const usedProperty = one(
|
|
533
|
+
members.filter((p) => p.name?.getText() === use.name.text),
|
|
534
|
+
);
|
|
535
|
+
require(
|
|
536
|
+
usedProperty === property ||
|
|
537
|
+
(t.isShorthandPropertyAssignment(usedProperty) &&
|
|
538
|
+
decl(usedProperty.name) === poll) ||
|
|
539
|
+
(t.isPropertyAssignment(usedProperty) &&
|
|
540
|
+
simpleArrow(usedProperty.initializer) &&
|
|
541
|
+
buffers.includes(decl(bodyExpression(usedProperty.initializer)))),
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
require(
|
|
545
|
+
nodes(
|
|
546
|
+
factoryBody,
|
|
547
|
+
(n) => t.isIdentifier(n) && ["eval", "Function"].includes(n.text),
|
|
548
|
+
).length === 0,
|
|
549
|
+
);
|
|
550
|
+
return {
|
|
551
|
+
model: "node-child-capture-poll-v1",
|
|
552
|
+
factorySource: at(factory),
|
|
553
|
+
predicateSource: at(read),
|
|
554
|
+
captures,
|
|
555
|
+
pattern: regex,
|
|
556
|
+
};
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (error === unsupported) return undefined;
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
}
|