pi-ultracode 0.1.2 → 0.3.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/README.md +47 -42
- package/examples/workflows/loop-until-dry-bugs.workflow.js +16 -6
- package/extensions/ultracode.ts +0 -1
- package/package.json +7 -7
- package/src/commands.ts +19 -23
- package/src/index.ts +1 -1
- package/src/mode.ts +6 -40
- package/src/prompts.ts +18 -28
- package/src/workflow/admission.ts +318 -0
- package/src/workflow/agent-runner.ts +12 -0
- package/src/workflow/display.ts +17 -13
- package/src/workflow/journal.ts +0 -0
- package/src/workflow/json-schema.ts +39 -17
- package/src/workflow/leases.ts +60 -0
- package/src/workflow/parser.ts +809 -5
- package/src/workflow/registry.ts +39 -22
- package/src/workflow/run-artifacts.ts +231 -0
- package/src/workflow/run-details.ts +106 -28
- package/src/workflow/runtime.ts +640 -303
- package/src/workflow/script-executor.ts +317 -0
- package/src/workflow/script-worker.mjs +678 -0
- package/src/workflow/structured-output.ts +2 -0
- package/src/workflow/tool.ts +207 -74
- package/src/workflow/value-limits.ts +363 -0
- package/src/workflow/worktree.ts +5 -3
- package/types/workflow.d.ts +60 -22
package/src/workflow/parser.ts
CHANGED
|
@@ -26,10 +26,31 @@ export interface WorkflowMeta {
|
|
|
26
26
|
|
|
27
27
|
export interface ParsedWorkflow {
|
|
28
28
|
meta: WorkflowMeta;
|
|
29
|
-
/** Script with the `export const meta` statement removed
|
|
29
|
+
/** Script with the `export const meta` statement removed and checkpoint-instrumented. */
|
|
30
30
|
body: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export const WORKFLOW_CHECKPOINT_IDENTIFIER = "__ultracodeCheckpoint";
|
|
34
|
+
const WORKFLOW_LOOP_IDENTIFIER = "__ultracodeLoop";
|
|
35
|
+
const WORKFLOW_INVOKE_IDENTIFIER = "__ultracodeInvoke";
|
|
36
|
+
const WORKFLOW_INTERNAL_CALLS = {
|
|
37
|
+
agent: "__ultracodeAgent",
|
|
38
|
+
parallel: "__ultracodeParallel",
|
|
39
|
+
pipeline: "__ultracodePipeline",
|
|
40
|
+
workflow: "__ultracodeWorkflow",
|
|
41
|
+
} as const;
|
|
42
|
+
const WORKFLOW_RESERVED_IDENTIFIERS = new Set([
|
|
43
|
+
WORKFLOW_CHECKPOINT_IDENTIFIER,
|
|
44
|
+
WORKFLOW_LOOP_IDENTIFIER,
|
|
45
|
+
WORKFLOW_INVOKE_IDENTIFIER,
|
|
46
|
+
...Object.values(WORKFLOW_INTERNAL_CALLS),
|
|
47
|
+
]);
|
|
48
|
+
export const DEFAULT_WORKFLOW_CHECKPOINT_LIMIT = 1_000_000;
|
|
49
|
+
const MAX_WORKFLOW_SOURCE_BYTES = 16 * 1024 * 1024;
|
|
50
|
+
const MAX_WORKFLOW_AST_DEPTH = 128;
|
|
51
|
+
const MAX_WORKFLOW_FUNCTION_DEPTH = 64;
|
|
52
|
+
const MAX_WORKFLOW_LITERAL_ITEMS = 65_536;
|
|
53
|
+
|
|
33
54
|
type AnyNode = Node & { [key: string]: any; start: number; end: number };
|
|
34
55
|
|
|
35
56
|
const NONDETERMINISM_ERROR =
|
|
@@ -45,6 +66,9 @@ export function normalizeScript(script: string): string {
|
|
|
45
66
|
|
|
46
67
|
export function parseWorkflowScript(rawScript: string): ParsedWorkflow {
|
|
47
68
|
const script = normalizeScript(rawScript);
|
|
69
|
+
if (Buffer.byteLength(script, "utf8") > MAX_WORKFLOW_SOURCE_BYTES) {
|
|
70
|
+
throw new Error(`workflow script exceeds the ${MAX_WORKFLOW_SOURCE_BYTES}-byte source limit`);
|
|
71
|
+
}
|
|
48
72
|
let ast: AnyNode;
|
|
49
73
|
try {
|
|
50
74
|
ast = parse(script, {
|
|
@@ -58,7 +82,10 @@ export function parseWorkflowScript(rawScript: string): ParsedWorkflow {
|
|
|
58
82
|
throw new Error(`workflow script is not valid JavaScript: ${detail}`);
|
|
59
83
|
}
|
|
60
84
|
|
|
61
|
-
|
|
85
|
+
const orchestrationBindings = collectOrchestrationBindings(ast);
|
|
86
|
+
assertDeterministicAst(ast, orchestrationBindings);
|
|
87
|
+
assertNoReservedIdentifiers(ast);
|
|
88
|
+
assertStructuralLimits(ast);
|
|
62
89
|
|
|
63
90
|
const first = ast.body?.[0] as AnyNode | undefined;
|
|
64
91
|
if (first?.type !== "ExportNamedDeclaration") {
|
|
@@ -84,10 +111,69 @@ export function parseWorkflowScript(rawScript: string): ParsedWorkflow {
|
|
|
84
111
|
|
|
85
112
|
return {
|
|
86
113
|
meta,
|
|
87
|
-
body: script.slice(0, first.start) + script.slice(first.end),
|
|
114
|
+
body: instrumentWorkflowBody(script.slice(0, first.start) + script.slice(first.end)),
|
|
88
115
|
};
|
|
89
116
|
}
|
|
90
117
|
|
|
118
|
+
export function instrumentWorkflowBody(body: string): string {
|
|
119
|
+
let ast: AnyNode;
|
|
120
|
+
try {
|
|
121
|
+
ast = parse(body, {
|
|
122
|
+
ecmaVersion: "latest",
|
|
123
|
+
sourceType: "script",
|
|
124
|
+
allowAwaitOutsideFunction: true,
|
|
125
|
+
allowReturnOutsideFunction: true,
|
|
126
|
+
}) as AnyNode;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
129
|
+
throw new Error(`workflow script body is not valid JavaScript: ${detail}`);
|
|
130
|
+
}
|
|
131
|
+
assertNoReservedIdentifiers(ast);
|
|
132
|
+
const checkpointReplacements: Array<{ start: number; end: number; text: string }> = [];
|
|
133
|
+
const checkpointInsertions: Array<{ pos: number; text: string }> = [];
|
|
134
|
+
collectCheckpointEdits(ast, body, checkpointInsertions, checkpointReplacements, new Set());
|
|
135
|
+
const checkpointed = applySourceEdits(body, checkpointInsertions, checkpointReplacements);
|
|
136
|
+
|
|
137
|
+
const instrumentedAst = parse(checkpointed, {
|
|
138
|
+
ecmaVersion: "latest",
|
|
139
|
+
sourceType: "script",
|
|
140
|
+
allowAwaitOutsideFunction: true,
|
|
141
|
+
allowReturnOutsideFunction: true,
|
|
142
|
+
}) as AnyNode;
|
|
143
|
+
const callReplacements: Array<{ start: number; end: number; text: string }> = [];
|
|
144
|
+
const callInsertions: Array<{ pos: number; text: string }> = [];
|
|
145
|
+
collectCallSiteEdits(instrumentedAst, checkpointed, callInsertions, callReplacements);
|
|
146
|
+
const instrumented = applySourceEdits(checkpointed, callInsertions, callReplacements);
|
|
147
|
+
try {
|
|
148
|
+
parse(instrumented, {
|
|
149
|
+
ecmaVersion: "latest",
|
|
150
|
+
sourceType: "script",
|
|
151
|
+
allowAwaitOutsideFunction: true,
|
|
152
|
+
allowReturnOutsideFunction: true,
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
156
|
+
throw new Error(`workflow instrumentation produced invalid JavaScript: ${detail}`);
|
|
157
|
+
}
|
|
158
|
+
return instrumented;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function applySourceEdits(
|
|
162
|
+
source: string,
|
|
163
|
+
insertions: Array<{ pos: number; text: string }>,
|
|
164
|
+
replacements: Array<{ start: number; end: number; text: string }>,
|
|
165
|
+
): string {
|
|
166
|
+
const edits = [
|
|
167
|
+
...replacements,
|
|
168
|
+
...insertions.map((edit) => ({ start: edit.pos, end: edit.pos, text: edit.text })),
|
|
169
|
+
];
|
|
170
|
+
let out = source;
|
|
171
|
+
for (const edit of edits.sort((a, b) => b.start - a.start || b.end - a.end)) {
|
|
172
|
+
out = `${out.slice(0, edit.start)}${edit.text}${out.slice(edit.end)}`;
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
91
177
|
/** Evaluate a strictly-literal AST node (no identifiers, calls, or interpolation). */
|
|
92
178
|
function evaluateLiteral(node: AnyNode, path: string): unknown {
|
|
93
179
|
switch (node.type) {
|
|
@@ -135,11 +221,252 @@ function propertyKey(node: AnyNode, path: string): string {
|
|
|
135
221
|
throw new Error(`unsupported key type in ${path}: ${node.type}`);
|
|
136
222
|
}
|
|
137
223
|
|
|
138
|
-
function assertDeterministicAst(
|
|
224
|
+
function assertDeterministicAst(
|
|
225
|
+
node: AnyNode,
|
|
226
|
+
orchestrationBindings: ReadonlySet<string>,
|
|
227
|
+
parent?: AnyNode,
|
|
228
|
+
grandparent?: AnyNode,
|
|
229
|
+
): void {
|
|
139
230
|
if (isDateNowCall(node) || isMathRandomCall(node) || isNewDateExpression(node)) {
|
|
140
231
|
throw new Error(NONDETERMINISM_ERROR);
|
|
141
232
|
}
|
|
142
|
-
|
|
233
|
+
if (isDynamicFunctionConstructor(node)) {
|
|
234
|
+
throw new Error("Workflow scripts must not construct functions dynamically; use ordinary functions so checkpoints can be injected.");
|
|
235
|
+
}
|
|
236
|
+
if (isPromiseChainMember(node) || isPromiseChainCall(node) || isForbiddenPromiseDestructure(node, parent)) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
"Workflow scripts must not access .then, .catch, or .finally promise chains; await or return orchestration calls directly.",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (isUnsupportedHelperCall(node, orchestrationBindings)) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
"Workflow orchestration helpers must use a declared identifier or a static member on a declared object; assign complex receivers first.",
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (isUnsupportedLoopHeader(node, orchestrationBindings)) {
|
|
247
|
+
throw new Error("Workflow orchestration calls must be inside a loop body, not its initializer, condition, or update.");
|
|
248
|
+
}
|
|
249
|
+
if (isUnsupportedClassEagerOrchestration(node, orchestrationBindings)) {
|
|
250
|
+
throw new Error("Workflow classes may call orchestration only from ordinary methods, not constructors, fields, or static blocks.");
|
|
251
|
+
}
|
|
252
|
+
if (isUnsupportedFactoryProperty(node) || isUnsupportedAccessorForwarding(node, orchestrationBindings)) {
|
|
253
|
+
throw new Error("Workflow orchestration helper properties must use static methods; accessor forwarding is unsupported.");
|
|
254
|
+
}
|
|
255
|
+
if (isUnsupportedCompositeFactory(node)) {
|
|
256
|
+
throw new Error("Workflow orchestration helper factories must use a direct function, object, or class declaration.");
|
|
257
|
+
}
|
|
258
|
+
if (isUnsupportedHelperAssignment(node, orchestrationBindings)) {
|
|
259
|
+
throw new Error("Workflow orchestration helper functions must be declared, not assigned through a member expression.");
|
|
260
|
+
}
|
|
261
|
+
if (isForbiddenPromiseMember(node, parent) || isForbiddenPromiseCall(node)) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
"Workflow scripts must not access native Promise combinators; use parallel() or pipeline() for deterministic concurrency.",
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (
|
|
267
|
+
isDynamicMethodCall(node)
|
|
268
|
+
|| isDynamicOrchestrationMember(node, orchestrationBindings)
|
|
269
|
+
|| isOrchestrationDestructure(node, orchestrationBindings)
|
|
270
|
+
|| isOrchestrationArgumentEscape(node, orchestrationBindings)
|
|
271
|
+
|| isOrchestrationFunctionAlias(node, parent)
|
|
272
|
+
|| isOrchestrationMemberAlias(node, parent, orchestrationBindings)
|
|
273
|
+
|| isOrchestrationBindingEscape(node, parent, orchestrationBindings)
|
|
274
|
+
) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
"Workflow scripts must use a static method name; dynamic method calls can bypass deterministic promise checks.",
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (node.type === "Identifier" && node.name === "globalThis") {
|
|
280
|
+
throw new Error("Workflow scripts must not access globalThis; use the declared workflow globals directly.");
|
|
281
|
+
}
|
|
282
|
+
if (
|
|
283
|
+
node.type === "MemberExpression"
|
|
284
|
+
&& node.object?.type === "ThisExpression"
|
|
285
|
+
&& ORCHESTRATION_FUNCTIONS.has(propertyNameOf(node) ?? "")
|
|
286
|
+
) {
|
|
287
|
+
throw new Error("Workflow orchestration globals must be called directly so call sites remain stable.");
|
|
288
|
+
}
|
|
289
|
+
if (node.type === "Identifier" && node.name === "Promise" && !isAllowedPromiseReference(node, parent, grandparent)) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
"Workflow scripts only allow new Promise(...) and Promise.resolve(...); use parallel() or pipeline() instead of native Promise combinators.",
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
for (const child of astChildren(node)) {
|
|
295
|
+
assertDeterministicAst(child, orchestrationBindings, node, parent);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function assertStructuralLimits(root: AnyNode): void {
|
|
300
|
+
const visit = (node: AnyNode, depth: number, functionDepth: number): void => {
|
|
301
|
+
if (depth > MAX_WORKFLOW_AST_DEPTH) {
|
|
302
|
+
throw new Error(`workflow script exceeds the ${MAX_WORKFLOW_AST_DEPTH}-level AST nesting depth limit`);
|
|
303
|
+
}
|
|
304
|
+
const nextFunctionDepth = functionDepth + (isFunctionNode(node) ? 1 : 0);
|
|
305
|
+
if (nextFunctionDepth > MAX_WORKFLOW_FUNCTION_DEPTH) {
|
|
306
|
+
throw new Error(`workflow script exceeds the ${MAX_WORKFLOW_FUNCTION_DEPTH}-level function nesting limit`);
|
|
307
|
+
}
|
|
308
|
+
if (node.type === "ArrayExpression" && (node.elements?.length ?? 0) > MAX_WORKFLOW_LITERAL_ITEMS) {
|
|
309
|
+
throw new Error(`workflow array literal exceeds ${MAX_WORKFLOW_LITERAL_ITEMS} items`);
|
|
310
|
+
}
|
|
311
|
+
if (node.type === "ObjectExpression" && (node.properties?.length ?? 0) > MAX_WORKFLOW_LITERAL_ITEMS) {
|
|
312
|
+
throw new Error(`workflow object literal exceeds ${MAX_WORKFLOW_LITERAL_ITEMS} properties`);
|
|
313
|
+
}
|
|
314
|
+
if (node.type === "Literal" && typeof node.value === "string"
|
|
315
|
+
&& Buffer.byteLength(node.value, "utf8") > MAX_WORKFLOW_SOURCE_BYTES) {
|
|
316
|
+
throw new Error(`workflow string literal exceeds ${MAX_WORKFLOW_SOURCE_BYTES} bytes`);
|
|
317
|
+
}
|
|
318
|
+
for (const child of astChildren(node)) visit(child, depth + 1, nextFunctionDepth);
|
|
319
|
+
};
|
|
320
|
+
visit(root, 0, 0);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function assertNoReservedIdentifiers(node: AnyNode): void {
|
|
324
|
+
if (node.type === "Identifier" && WORKFLOW_RESERVED_IDENTIFIERS.has(node.name)) {
|
|
325
|
+
throw new Error(`workflow scripts may not use the reserved identifier ${node.name}`);
|
|
326
|
+
}
|
|
327
|
+
for (const child of astChildren(node)) assertNoReservedIdentifiers(child);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function collectCallSiteEdits(
|
|
331
|
+
root: AnyNode,
|
|
332
|
+
source: string,
|
|
333
|
+
insertions: Array<{ pos: number; text: string }>,
|
|
334
|
+
replacements: Array<{ start: number; end: number; text: string }>,
|
|
335
|
+
): void {
|
|
336
|
+
const orchestrationBindings = collectOrchestrationBindings(root);
|
|
337
|
+
const localFunctions = collectLocalFunctionNames(root, orchestrationBindings);
|
|
338
|
+
const stack: Array<{ node: AnyNode; loops: string[] }> = [{ node: root, loops: [] }];
|
|
339
|
+
while (stack.length > 0) {
|
|
340
|
+
const { node, loops } = stack.pop()!;
|
|
341
|
+
if (
|
|
342
|
+
node.type === "CallExpression"
|
|
343
|
+
&& node.callee?.type === "Identifier"
|
|
344
|
+
&& Object.hasOwn(WORKFLOW_INTERNAL_CALLS, node.callee.name)
|
|
345
|
+
) {
|
|
346
|
+
const name = node.callee.name as keyof typeof WORKFLOW_INTERNAL_CALLS;
|
|
347
|
+
const openOffset = source.slice(node.callee.end, node.end).indexOf("(");
|
|
348
|
+
if (openOffset < 0) throw new Error(`unable to instrument workflow call site for ${name}`);
|
|
349
|
+
const openParen = node.callee.end + openOffset;
|
|
350
|
+
const loopSuffix = loops.length > 0 ? `@${loops.join(",")}` : "";
|
|
351
|
+
const callSite = `${name}:${node.start.toString(36)}${loopSuffix}`;
|
|
352
|
+
replacements.push({
|
|
353
|
+
start: node.callee.start,
|
|
354
|
+
end: node.callee.end,
|
|
355
|
+
text: WORKFLOW_INTERNAL_CALLS[name],
|
|
356
|
+
});
|
|
357
|
+
insertions.push({
|
|
358
|
+
pos: openParen + 1,
|
|
359
|
+
text: `${JSON.stringify(callSite)}${node.arguments.length > 0 ? "," : ""}`,
|
|
360
|
+
});
|
|
361
|
+
} else if (node.type === "CallExpression" && (
|
|
362
|
+
(node.callee?.type === "Identifier" && localFunctions.has(node.callee.name))
|
|
363
|
+
|| (node.callee?.type === "MemberExpression"
|
|
364
|
+
&& isTaintedStaticMember(node.callee, orchestrationBindings))
|
|
365
|
+
)) {
|
|
366
|
+
const openOffset = source.slice(node.callee.end, node.end).indexOf("(");
|
|
367
|
+
if (openOffset < 0) throw new Error("unable to instrument workflow helper call site");
|
|
368
|
+
const openParen = node.callee.end + openOffset;
|
|
369
|
+
const loopSuffix = loops.length > 0 ? `@${loops.join(",")}` : "";
|
|
370
|
+
const callSite = `invoke:${node.start.toString(36)}${loopSuffix}`;
|
|
371
|
+
const callable = source.slice(node.callee.start, node.callee.end);
|
|
372
|
+
const thisArg = node.callee.type === "MemberExpression"
|
|
373
|
+
? source.slice(node.callee.object.start, node.callee.object.end)
|
|
374
|
+
: "undefined";
|
|
375
|
+
replacements.push({
|
|
376
|
+
start: node.callee.start,
|
|
377
|
+
end: node.callee.end,
|
|
378
|
+
text: WORKFLOW_INVOKE_IDENTIFIER,
|
|
379
|
+
});
|
|
380
|
+
insertions.push({
|
|
381
|
+
pos: openParen + 1,
|
|
382
|
+
text: `${JSON.stringify(callSite)},${callable},${thisArg}${node.arguments.length > 0 ? "," : ""}`,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
const body = isLoopStatement(node) ? node.body as AnyNode | undefined : undefined;
|
|
386
|
+
const loopSite = body ? `loop:${node.start.toString(36)}` : undefined;
|
|
387
|
+
if (body && loopSite) {
|
|
388
|
+
if (body.type !== "BlockStatement") throw new Error("workflow loop instrumentation requires a block body");
|
|
389
|
+
insertions.push({
|
|
390
|
+
pos: body.start + 1,
|
|
391
|
+
text: `\n${WORKFLOW_LOOP_IDENTIFIER}(${JSON.stringify(loopSite)});`,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
for (const child of astChildren(node)) {
|
|
395
|
+
stack.push({
|
|
396
|
+
node: child,
|
|
397
|
+
loops: body && loopSite && child === body ? [...loops, loopSite] : loops,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function collectLocalFunctionNames(root: AnyNode, bindings: ReadonlySet<string>): Set<string> {
|
|
404
|
+
const names = new Set<string>();
|
|
405
|
+
const stack = [root];
|
|
406
|
+
while (stack.length > 0) {
|
|
407
|
+
const node = stack.pop()!;
|
|
408
|
+
if (
|
|
409
|
+
node.type === "FunctionDeclaration"
|
|
410
|
+
&& node.id?.type === "Identifier"
|
|
411
|
+
&& bindings.has(node.id.name)
|
|
412
|
+
) names.add(node.id.name);
|
|
413
|
+
if (
|
|
414
|
+
node.type === "VariableDeclarator"
|
|
415
|
+
&& node.id?.type === "Identifier"
|
|
416
|
+
&& node.init
|
|
417
|
+
&& isFunctionNode(node.init)
|
|
418
|
+
&& bindings.has(node.id.name)
|
|
419
|
+
) names.add(node.id.name);
|
|
420
|
+
stack.push(...astChildren(node));
|
|
421
|
+
}
|
|
422
|
+
return names;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function collectCheckpointEdits(
|
|
426
|
+
node: AnyNode,
|
|
427
|
+
source: string,
|
|
428
|
+
insertions: Array<{ pos: number; text: string }>,
|
|
429
|
+
replacements: Array<{ start: number; end: number; text: string }>,
|
|
430
|
+
skipped: Set<AnyNode>,
|
|
431
|
+
): void {
|
|
432
|
+
if (skipped.has(node)) return;
|
|
433
|
+
if (isLoopStatement(node)) {
|
|
434
|
+
const body = node.body as AnyNode | undefined;
|
|
435
|
+
if (body?.type === "BlockStatement") {
|
|
436
|
+
insertions.push({ pos: body.start + 1, text: `\n${WORKFLOW_CHECKPOINT_IDENTIFIER}();` });
|
|
437
|
+
} else if (body) {
|
|
438
|
+
replacements.push({
|
|
439
|
+
start: body.start,
|
|
440
|
+
end: body.end,
|
|
441
|
+
text: `{\n${WORKFLOW_CHECKPOINT_IDENTIFIER}();\n${source.slice(body.start, body.end)}\n}`,
|
|
442
|
+
});
|
|
443
|
+
skipped.add(body);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (isFunctionNode(node)) {
|
|
448
|
+
const body = node.body as AnyNode | undefined;
|
|
449
|
+
if (body?.type === "BlockStatement") {
|
|
450
|
+
insertions.push({ pos: body.start + 1, text: `\n${WORKFLOW_CHECKPOINT_IDENTIFIER}();` });
|
|
451
|
+
} else if (body && node.type === "ArrowFunctionExpression") {
|
|
452
|
+
replacements.push({
|
|
453
|
+
start: body.start,
|
|
454
|
+
end: body.end,
|
|
455
|
+
text: `(${WORKFLOW_CHECKPOINT_IDENTIFIER}(), ${source.slice(body.start, body.end)})`,
|
|
456
|
+
});
|
|
457
|
+
skipped.add(body);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
for (const child of astChildren(node)) collectCheckpointEdits(child, source, insertions, replacements, skipped);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function isLoopStatement(node: AnyNode): boolean {
|
|
465
|
+
return ["WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"].includes(node.type);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function isFunctionNode(node: AnyNode): boolean {
|
|
469
|
+
return ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"].includes(node.type);
|
|
143
470
|
}
|
|
144
471
|
|
|
145
472
|
function astChildren(node: AnyNode): AnyNode[] {
|
|
@@ -167,6 +494,483 @@ function isNewDateExpression(node: AnyNode): boolean {
|
|
|
167
494
|
return node.type === "NewExpression" && node.callee?.type === "Identifier" && node.callee.name === "Date";
|
|
168
495
|
}
|
|
169
496
|
|
|
497
|
+
function isDynamicFunctionConstructor(node: AnyNode): boolean {
|
|
498
|
+
if ((node.type !== "CallExpression" && node.type !== "NewExpression") || node.callee?.type !== "Identifier") {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
return ["Function", "AsyncFunction", "GeneratorFunction", "AsyncGeneratorFunction"].includes(node.callee.name);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const ORCHESTRATION_FUNCTIONS = new Set(["agent", "parallel", "pipeline", "workflow"]);
|
|
505
|
+
|
|
506
|
+
function collectOrchestrationBindings(root: AnyNode): Set<string> {
|
|
507
|
+
const dependencies = new Map<string, Set<string>>();
|
|
508
|
+
const reverse = new Map<string, Set<string>>();
|
|
509
|
+
const aliases = new Map<string, Set<string>>();
|
|
510
|
+
const seeds = new Set<string>();
|
|
511
|
+
const stack = [root];
|
|
512
|
+
while (stack.length > 0) {
|
|
513
|
+
const node = stack.pop()!;
|
|
514
|
+
const definition = bindingDefinition(node);
|
|
515
|
+
if (definition) {
|
|
516
|
+
const deps = collectReferencedIdentifiers(definition.value);
|
|
517
|
+
const existing = dependencies.get(definition.name) ?? new Set<string>();
|
|
518
|
+
for (const dependency of deps) existing.add(dependency);
|
|
519
|
+
dependencies.set(definition.name, existing);
|
|
520
|
+
const tainted = isFactoryDefinition(definition.value)
|
|
521
|
+
? containsDirectOrchestration(definition.value)
|
|
522
|
+
: containsUnconsumedOrchestration(definition.value);
|
|
523
|
+
if (tainted) seeds.add(definition.name);
|
|
524
|
+
if (definition.value.type === "ObjectExpression") {
|
|
525
|
+
for (const member of objectFactoryMembers(definition.name, definition.value)) addMemberSeed(seeds, member);
|
|
526
|
+
}
|
|
527
|
+
for (const alias of collectStaticAliases(definition.name, definition.value)) {
|
|
528
|
+
const targets = aliases.get(alias.source) ?? new Set<string>();
|
|
529
|
+
targets.add(alias.target);
|
|
530
|
+
aliases.set(alias.source, targets);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
stack.push(...astChildren(node));
|
|
534
|
+
}
|
|
535
|
+
for (const [name, deps] of dependencies) {
|
|
536
|
+
for (const dependency of deps) {
|
|
537
|
+
const dependents = reverse.get(dependency) ?? new Set<string>();
|
|
538
|
+
dependents.add(name);
|
|
539
|
+
reverse.set(dependency, dependents);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const bindings = new Set(seeds);
|
|
543
|
+
const queue = [...seeds];
|
|
544
|
+
for (let queueIndex = 0; queueIndex < queue.length; queueIndex++) {
|
|
545
|
+
const dependency = queue[queueIndex];
|
|
546
|
+
for (const dependent of reverse.get(dependency) ?? []) {
|
|
547
|
+
if (bindings.has(dependent)) continue;
|
|
548
|
+
bindings.add(dependent);
|
|
549
|
+
queue.push(dependent);
|
|
550
|
+
}
|
|
551
|
+
const parts = dependency.split(".");
|
|
552
|
+
for (let prefixLength = parts.length; prefixLength >= 1; prefixLength--) {
|
|
553
|
+
const source = parts.slice(0, prefixLength).join(".");
|
|
554
|
+
const suffix = parts.slice(prefixLength).join(".");
|
|
555
|
+
for (const target of aliases.get(source) ?? []) {
|
|
556
|
+
const propagated = suffix ? `${target}.${suffix}` : target;
|
|
557
|
+
if (bindings.has(propagated)) continue;
|
|
558
|
+
bindings.add(propagated);
|
|
559
|
+
queue.push(propagated);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return bindings;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function addMemberSeed(seeds: Set<string>, member: string): void {
|
|
567
|
+
seeds.add(member);
|
|
568
|
+
const parts = member.split(".");
|
|
569
|
+
for (let length = 1; length < parts.length; length++) {
|
|
570
|
+
seeds.add(`${parts.slice(0, length).join(".")}.*`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function collectStaticAliases(target: string, value: AnyNode): Array<{ source: string; target: string }> {
|
|
575
|
+
if (value.type === "Identifier") return [{ source: value.name, target }];
|
|
576
|
+
const member = staticMemberReference(value);
|
|
577
|
+
if (member) return [{ source: member, target }];
|
|
578
|
+
if (value.type !== "ObjectExpression") return [];
|
|
579
|
+
const aliases: Array<{ source: string; target: string }> = [];
|
|
580
|
+
for (const property of value.properties ?? []) {
|
|
581
|
+
if (property.type !== "Property") continue;
|
|
582
|
+
const key = property.computed
|
|
583
|
+
? staticStringOf(property.key)
|
|
584
|
+
: property.key?.name ?? (typeof property.key?.value === "string" ? property.key.value : undefined);
|
|
585
|
+
if (!key) continue;
|
|
586
|
+
aliases.push(...collectStaticAliases(`${target}.${key}`, property.value));
|
|
587
|
+
}
|
|
588
|
+
return aliases;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function bindingDefinition(node: AnyNode): { name: string; value: AnyNode } | undefined {
|
|
592
|
+
if (node.type === "VariableDeclarator" && node.id?.type === "Identifier" && node.init) {
|
|
593
|
+
return { name: node.id.name, value: node.init };
|
|
594
|
+
}
|
|
595
|
+
if (
|
|
596
|
+
(node.type === "FunctionDeclaration" || node.type === "ClassDeclaration")
|
|
597
|
+
&& node.id?.type === "Identifier"
|
|
598
|
+
) {
|
|
599
|
+
return { name: node.id.name, value: node };
|
|
600
|
+
}
|
|
601
|
+
if (node.type === "AssignmentExpression") {
|
|
602
|
+
if (node.left?.type === "Identifier") return { name: node.left.name, value: node.right };
|
|
603
|
+
if (node.left?.type === "MemberExpression" && node.left.object?.type === "Identifier") {
|
|
604
|
+
return { name: node.left.object.name, value: node.right };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function collectReferencedIdentifiers(root: AnyNode): Set<string> {
|
|
611
|
+
const references = new Set<string>();
|
|
612
|
+
const stack: Array<{ node: AnyNode; parent?: AnyNode }> = [{ node: root }];
|
|
613
|
+
while (stack.length > 0) {
|
|
614
|
+
const { node, parent } = stack.pop()!;
|
|
615
|
+
if (node.type === "Identifier" && isIdentifierReference(node, parent)) references.add(node.name);
|
|
616
|
+
const member = staticMemberReference(node);
|
|
617
|
+
if (member) references.add(member);
|
|
618
|
+
for (const child of astChildren(node)) stack.push({ node: child, parent: node });
|
|
619
|
+
}
|
|
620
|
+
return references;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function objectFactoryMembers(name: string, node: AnyNode): string[] {
|
|
624
|
+
const members: string[] = [];
|
|
625
|
+
for (const property of node.properties ?? []) {
|
|
626
|
+
if (property.type !== "Property") continue;
|
|
627
|
+
const key = property.computed
|
|
628
|
+
? staticStringOf(property.key)
|
|
629
|
+
: property.key?.name ?? (typeof property.key?.value === "string" ? property.key.value : undefined);
|
|
630
|
+
if (!key) continue;
|
|
631
|
+
const member = `${name}.${key}`;
|
|
632
|
+
if (isFunctionNode(property.value) && containsDirectOrchestration(property.value)) members.push(member);
|
|
633
|
+
if (property.value?.type === "ObjectExpression") {
|
|
634
|
+
members.push(...objectFactoryMembers(member, property.value));
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return members;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function staticMemberReference(node: AnyNode): string | undefined {
|
|
641
|
+
if (node.type !== "MemberExpression") return undefined;
|
|
642
|
+
const property = propertyNameOf(node);
|
|
643
|
+
if (!property) return undefined;
|
|
644
|
+
if (node.object?.type === "Identifier") return `${node.object.name}.${property}`;
|
|
645
|
+
const parent = staticMemberReference(node.object);
|
|
646
|
+
return parent ? `${parent}.${property}` : undefined;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function isFactoryDefinition(node: AnyNode): boolean {
|
|
650
|
+
return isFunctionNode(node) || node.type === "ClassExpression" || node.type === "ClassDeclaration";
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function containsUnconsumedOrchestration(root: AnyNode): boolean {
|
|
654
|
+
const stack: Array<{ node: AnyNode; parent?: AnyNode }> = [{ node: root }];
|
|
655
|
+
while (stack.length > 0) {
|
|
656
|
+
const { node, parent } = stack.pop()!;
|
|
657
|
+
if (node.type === "AwaitExpression") continue;
|
|
658
|
+
if (node !== root && isFunctionNode(node)) continue;
|
|
659
|
+
if (
|
|
660
|
+
node.type === "CallExpression"
|
|
661
|
+
&& node.callee?.type === "Identifier"
|
|
662
|
+
&& ORCHESTRATION_FUNCTIONS.has(node.callee.name)
|
|
663
|
+
) return true;
|
|
664
|
+
if (isOrchestrationFunctionAlias(node, parent)) return true;
|
|
665
|
+
for (const child of astChildren(node)) stack.push({ node: child, parent: node });
|
|
666
|
+
}
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function containsDirectOrchestration(root: AnyNode): boolean {
|
|
671
|
+
const stack: Array<{ node: AnyNode; parent?: AnyNode }> = [{ node: root }];
|
|
672
|
+
while (stack.length > 0) {
|
|
673
|
+
const { node, parent } = stack.pop()!;
|
|
674
|
+
if (
|
|
675
|
+
node.type === "CallExpression"
|
|
676
|
+
&& node.callee?.type === "Identifier"
|
|
677
|
+
&& ORCHESTRATION_FUNCTIONS.has(node.callee.name)
|
|
678
|
+
) return true;
|
|
679
|
+
if (isOrchestrationFunctionAlias(node, parent)) return true;
|
|
680
|
+
for (const child of astChildren(node)) stack.push({ node: child, parent: node });
|
|
681
|
+
}
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function isIdentifierReference(node: AnyNode, parent?: AnyNode): boolean {
|
|
686
|
+
if (!parent) return true;
|
|
687
|
+
if (parent.type === "Property" && parent.key === node && !parent.computed) return false;
|
|
688
|
+
if (parent.type === "MemberExpression" && parent.property === node && !parent.computed) return false;
|
|
689
|
+
if (parent.type === "VariableDeclarator" && parent.id === node) return false;
|
|
690
|
+
if ((parent.type === "FunctionDeclaration" || parent.type === "ClassDeclaration") && parent.id === node) return false;
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function isOrchestrationExpression(
|
|
695
|
+
node: AnyNode | undefined,
|
|
696
|
+
bindings: ReadonlySet<string>,
|
|
697
|
+
): boolean {
|
|
698
|
+
if (!node) return false;
|
|
699
|
+
if (node.type === "Identifier") return bindings.has(node.name) || hasTaintedMember(bindings, node.name);
|
|
700
|
+
if (node.type === "CallExpression") {
|
|
701
|
+
if (
|
|
702
|
+
node.callee?.type === "Identifier"
|
|
703
|
+
&& (ORCHESTRATION_FUNCTIONS.has(node.callee.name) || bindings.has(node.callee.name))
|
|
704
|
+
) return true;
|
|
705
|
+
if (node.callee?.type === "MemberExpression") {
|
|
706
|
+
return isTaintedStaticMember(node.callee, bindings);
|
|
707
|
+
}
|
|
708
|
+
if (isFunctionNode(node.callee)) return containsDirectOrchestration(node.callee);
|
|
709
|
+
}
|
|
710
|
+
if (node.type === "MemberExpression") {
|
|
711
|
+
return isTaintedStaticMember(node, bindings);
|
|
712
|
+
}
|
|
713
|
+
if (node.type === "ChainExpression") return isOrchestrationExpression(node.expression, bindings);
|
|
714
|
+
if (node.type === "NewExpression") {
|
|
715
|
+
return (node.callee?.type === "Identifier" && bindings.has(node.callee.name))
|
|
716
|
+
|| node.arguments.some((argument: AnyNode) =>
|
|
717
|
+
argument.type === "SpreadElement"
|
|
718
|
+
? isOrchestrationExpression(argument.argument, bindings)
|
|
719
|
+
: isOrchestrationExpression(argument, bindings)
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
if (node.type === "ArrayExpression") {
|
|
723
|
+
return node.elements.some((element: AnyNode | null) =>
|
|
724
|
+
!!element && (element.type === "SpreadElement"
|
|
725
|
+
? isOrchestrationExpression(element.argument, bindings)
|
|
726
|
+
: isOrchestrationExpression(element, bindings))
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
if (node.type === "ObjectExpression") {
|
|
730
|
+
return node.properties.some((property: AnyNode) =>
|
|
731
|
+
property.type === "SpreadElement"
|
|
732
|
+
? isOrchestrationExpression(property.argument, bindings)
|
|
733
|
+
: property.type === "Property"
|
|
734
|
+
&& !isFunctionNode(property.value)
|
|
735
|
+
&& isOrchestrationExpression(property.value, bindings)
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
if (node.type === "ClassExpression" || node.type === "ClassDeclaration" || isFunctionNode(node)) {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
if (node.type === "ConditionalExpression") {
|
|
742
|
+
return isOrchestrationExpression(node.consequent, bindings)
|
|
743
|
+
|| isOrchestrationExpression(node.alternate, bindings);
|
|
744
|
+
}
|
|
745
|
+
if (node.type === "LogicalExpression" || node.type === "AssignmentExpression") {
|
|
746
|
+
return isOrchestrationExpression(node.left, bindings)
|
|
747
|
+
|| isOrchestrationExpression(node.right, bindings);
|
|
748
|
+
}
|
|
749
|
+
if (node.type === "SequenceExpression") {
|
|
750
|
+
return node.expressions.some((expression: AnyNode) => isOrchestrationExpression(expression, bindings));
|
|
751
|
+
}
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function isDynamicOrchestrationMember(node: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
756
|
+
return node.type === "MemberExpression"
|
|
757
|
+
&& node.computed
|
|
758
|
+
&& propertyNameOf(node) === undefined
|
|
759
|
+
&& isOrchestrationExpression(node.object, bindings);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function isOrchestrationArgumentEscape(node: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
763
|
+
if (node.type === "TaggedTemplateExpression") {
|
|
764
|
+
return (node.quasi?.expressions ?? []).some((expression: AnyNode) =>
|
|
765
|
+
isOrchestrationExpression(expression, bindings)
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
if (
|
|
769
|
+
node.type === "CallExpression"
|
|
770
|
+
&& node.callee?.type === "Identifier"
|
|
771
|
+
&& ORCHESTRATION_FUNCTIONS.has(node.callee.name)
|
|
772
|
+
) return false;
|
|
773
|
+
return (node.type === "CallExpression" || node.type === "NewExpression")
|
|
774
|
+
&& node.arguments.some((argument: AnyNode) =>
|
|
775
|
+
argument.type === "SpreadElement"
|
|
776
|
+
? isOrchestrationExpression(argument.argument, bindings)
|
|
777
|
+
: isOrchestrationExpression(argument, bindings)
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function isUnsupportedHelperCall(node: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
782
|
+
if (node.type !== "CallExpression") return false;
|
|
783
|
+
if (node.callee?.type === "MemberExpression") {
|
|
784
|
+
if (node.callee.object?.type === "ThisExpression" || node.callee.object?.type === "AwaitExpression") return true;
|
|
785
|
+
return (node.callee.object?.type === "CallExpression" || node.callee.object?.type === "NewExpression")
|
|
786
|
+
&& isOrchestrationExpression(node.callee.object, bindings);
|
|
787
|
+
}
|
|
788
|
+
return node.callee?.type === "CallExpression" && isOrchestrationExpression(node.callee, bindings);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function isUnsupportedLoopHeader(node: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
792
|
+
let expressions: Array<AnyNode | undefined> = [];
|
|
793
|
+
if (node.type === "ForStatement") expressions = [node.init, node.test, node.update];
|
|
794
|
+
else if (node.type === "ForInStatement" || node.type === "ForOfStatement") expressions = [node.right];
|
|
795
|
+
else if (node.type === "WhileStatement" || node.type === "DoWhileStatement") expressions = [node.test];
|
|
796
|
+
else return false;
|
|
797
|
+
return expressions.some((expression) => !!expression
|
|
798
|
+
&& (containsDirectOrchestration(expression) || isOrchestrationExpression(expression, bindings)));
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function isUnsupportedClassEagerOrchestration(node: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
802
|
+
if (node.type === "StaticBlock") return containsOrchestrationCall(node, bindings);
|
|
803
|
+
if (node.type === "MethodDefinition" && node.kind === "constructor") {
|
|
804
|
+
return containsOrchestrationCall(node.value, bindings);
|
|
805
|
+
}
|
|
806
|
+
if (node.type === "PropertyDefinition" && node.value) {
|
|
807
|
+
return containsOrchestrationCall(node.value, bindings) || isOrchestrationExpression(node.value, bindings);
|
|
808
|
+
}
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function containsOrchestrationCall(root: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
813
|
+
const stack = [root];
|
|
814
|
+
while (stack.length > 0) {
|
|
815
|
+
const node = stack.pop()!;
|
|
816
|
+
if (node.type === "CallExpression" && isOrchestrationExpression(node, bindings)) return true;
|
|
817
|
+
stack.push(...astChildren(node));
|
|
818
|
+
}
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function isUnsupportedFactoryProperty(node: AnyNode): boolean {
|
|
823
|
+
if (node.type !== "Property" && node.type !== "MethodDefinition") return false;
|
|
824
|
+
const value = node.value as AnyNode | undefined;
|
|
825
|
+
return !!value
|
|
826
|
+
&& isFunctionNode(value)
|
|
827
|
+
&& containsDirectOrchestration(value)
|
|
828
|
+
&& node.computed
|
|
829
|
+
&& staticStringOf(node.key) === undefined;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function isUnsupportedAccessorForwarding(node: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
833
|
+
if ((node.type !== "Property" && node.type !== "MethodDefinition") || node.kind !== "get") return false;
|
|
834
|
+
const value = node.value as AnyNode | undefined;
|
|
835
|
+
if (!value) return false;
|
|
836
|
+
const stack = [value];
|
|
837
|
+
while (stack.length > 0) {
|
|
838
|
+
const child = stack.pop()!;
|
|
839
|
+
if (child.type === "ThisExpression") return true;
|
|
840
|
+
if (child.type === "Identifier"
|
|
841
|
+
&& (bindings.has(child.name) || hasTaintedMember(bindings, child.name))) return true;
|
|
842
|
+
stack.push(...astChildren(child));
|
|
843
|
+
}
|
|
844
|
+
return containsDirectOrchestration(value);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function isUnsupportedCompositeFactory(node: AnyNode): boolean {
|
|
848
|
+
if (node.type !== "VariableDeclarator" || !node.init) return false;
|
|
849
|
+
return ["ConditionalExpression", "LogicalExpression", "SequenceExpression"].includes(node.init.type)
|
|
850
|
+
&& containsDirectOrchestration(node.init);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function isUnsupportedHelperAssignment(node: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
854
|
+
if (node.type !== "AssignmentExpression") return false;
|
|
855
|
+
const orchestrationValue = isFunctionNode(node.right)
|
|
856
|
+
? containsDirectOrchestration(node.right)
|
|
857
|
+
: isOrchestrationExpression(node.right, bindings);
|
|
858
|
+
return orchestrationValue
|
|
859
|
+
&& (node.left?.type === "Identifier" || node.left?.type === "MemberExpression");
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function hasTaintedMember(bindings: ReadonlySet<string>, name: string): boolean {
|
|
863
|
+
return bindings.has(`${name}.*`);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function isOrchestrationBindingEscape(
|
|
867
|
+
node: AnyNode,
|
|
868
|
+
parent: AnyNode | undefined,
|
|
869
|
+
bindings: ReadonlySet<string>,
|
|
870
|
+
): boolean {
|
|
871
|
+
if (node.type !== "Identifier" || !parent) return false;
|
|
872
|
+
const memberContainer = hasTaintedMember(bindings, node.name);
|
|
873
|
+
if (!bindings.has(node.name) && !memberContainer) return false;
|
|
874
|
+
if (memberContainer) {
|
|
875
|
+
if (parent.type === "MemberExpression" && parent.object === node) return false;
|
|
876
|
+
if (parent.type === "VariableDeclarator" && parent.id === node) return false;
|
|
877
|
+
if ((parent.type === "FunctionDeclaration" || parent.type === "ClassDeclaration") && parent.id === node) return false;
|
|
878
|
+
if (parent.type === "Property" && parent.key === node && !parent.computed && !parent.shorthand) return false;
|
|
879
|
+
return true;
|
|
880
|
+
}
|
|
881
|
+
if (parent.type === "VariableDeclarator") return parent.init === node;
|
|
882
|
+
if (parent.type === "AssignmentExpression") return parent.right === node;
|
|
883
|
+
if (parent.type === "Property") return parent.value === node && (parent.shorthand || parent.key !== node);
|
|
884
|
+
if (parent.type === "ReturnStatement") return parent.argument === node;
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function isTaintedStaticMember(node: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
889
|
+
const member = staticMemberReference(node);
|
|
890
|
+
if (!member) return isOrchestrationExpression(node.object, bindings);
|
|
891
|
+
if (bindings.has(member) || hasTaintedMember(bindings, member)) return true;
|
|
892
|
+
return node.object?.type === "Identifier" && bindings.has(node.object.name);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function isOrchestrationMemberAlias(
|
|
896
|
+
node: AnyNode,
|
|
897
|
+
parent: AnyNode | undefined,
|
|
898
|
+
bindings: ReadonlySet<string>,
|
|
899
|
+
): boolean {
|
|
900
|
+
if (node.type !== "MemberExpression") return false;
|
|
901
|
+
if (!isTaintedStaticMember(node, bindings)) return false;
|
|
902
|
+
return !(parent?.type === "CallExpression" && parent.callee === node);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function isOrchestrationFunctionAlias(node: AnyNode, parent?: AnyNode): boolean {
|
|
906
|
+
if (node.type !== "Identifier" || !ORCHESTRATION_FUNCTIONS.has(node.name)) return false;
|
|
907
|
+
if (parent?.type === "CallExpression" && parent.callee === node) return false;
|
|
908
|
+
if (parent?.type === "Property" && parent.key === node && !parent.computed) return false;
|
|
909
|
+
if (parent?.type === "MemberExpression" && parent.property === node && !parent.computed) return false;
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function isOrchestrationDestructure(node: AnyNode, bindings: ReadonlySet<string>): boolean {
|
|
914
|
+
return node.type === "VariableDeclarator"
|
|
915
|
+
&& (node.id?.type === "ObjectPattern" || node.id?.type === "ArrayPattern")
|
|
916
|
+
&& isOrchestrationExpression(node.init, bindings);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function isPromiseChainCall(node: AnyNode): boolean {
|
|
920
|
+
return node.type === "CallExpression"
|
|
921
|
+
&& node.callee?.type === "MemberExpression"
|
|
922
|
+
&& ["then", "catch", "finally"].includes(propertyNameOf(node.callee) ?? "");
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function isPromiseChainMember(node: AnyNode): boolean {
|
|
926
|
+
return node.type === "MemberExpression"
|
|
927
|
+
&& ["then", "catch", "finally"].includes(propertyNameOf(node) ?? "");
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function isForbiddenPromiseDestructure(node: AnyNode, parent?: AnyNode): boolean {
|
|
931
|
+
return node.type === "Property"
|
|
932
|
+
&& parent?.type === "ObjectPattern"
|
|
933
|
+
&& ["then", "catch", "finally"].includes(propertyNameOf(node) ?? propertyKeyName(node.key) ?? "");
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function isDynamicMethodCall(node: AnyNode): boolean {
|
|
937
|
+
return node.type === "CallExpression"
|
|
938
|
+
&& node.callee?.type === "MemberExpression"
|
|
939
|
+
&& node.callee.computed
|
|
940
|
+
&& propertyNameOf(node.callee) === undefined;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function propertyKeyName(node: AnyNode | undefined): string | undefined {
|
|
944
|
+
if (node?.type === "Identifier") return node.name;
|
|
945
|
+
return staticStringOf(node);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function isForbiddenPromiseCall(node: AnyNode): boolean {
|
|
949
|
+
return node.type === "CallExpression"
|
|
950
|
+
&& node.callee?.type === "MemberExpression"
|
|
951
|
+
&& node.callee.object?.type === "Identifier"
|
|
952
|
+
&& node.callee.object.name === "Promise"
|
|
953
|
+
&& ["all", "allSettled", "race", "any"].includes(propertyNameOf(node.callee) ?? "");
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function isForbiddenPromiseMember(node: AnyNode, parent?: AnyNode): boolean {
|
|
957
|
+
if (node.type !== "MemberExpression") return false;
|
|
958
|
+
const property = propertyNameOf(node);
|
|
959
|
+
if (property === "Promise") return true;
|
|
960
|
+
return ["all", "allSettled", "race", "any"].includes(property ?? "")
|
|
961
|
+
&& parent?.type === "CallExpression"
|
|
962
|
+
&& parent.callee === node;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function isAllowedPromiseReference(node: AnyNode, parent?: AnyNode, grandparent?: AnyNode): boolean {
|
|
966
|
+
if (parent?.type === "NewExpression" && parent.callee === node) return true;
|
|
967
|
+
return parent?.type === "MemberExpression"
|
|
968
|
+
&& parent.object === node
|
|
969
|
+
&& propertyNameOf(parent) === "resolve"
|
|
970
|
+
&& grandparent?.type === "CallExpression"
|
|
971
|
+
&& grandparent.callee === parent;
|
|
972
|
+
}
|
|
973
|
+
|
|
170
974
|
function isMemberExpression(node: AnyNode | undefined, objectName: string, propertyName: string): boolean {
|
|
171
975
|
if (node?.type !== "MemberExpression" || node.object?.type !== "Identifier" || node.object.name !== objectName) {
|
|
172
976
|
return false;
|