@glubean/scanner 0.5.2 → 0.7.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/dist/contract-extraction.d.ts +131 -137
- package/dist/contract-extraction.d.ts.map +1 -1
- package/dist/contract-extraction.js +76 -212
- package/dist/contract-extraction.js.map +1 -1
- package/dist/extractor-ast.d.ts +1 -9
- package/dist/extractor-ast.d.ts.map +1 -1
- package/dist/extractor-ast.js +676 -87
- package/dist/extractor-ast.js.map +1 -1
- package/dist/extractor-static.d.ts +9 -11
- package/dist/extractor-static.d.ts.map +1 -1
- package/dist/extractor-static.js +12 -5
- package/dist/extractor-static.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +53 -21
- package/dist/scanner.js.map +1 -1
- package/dist/static.d.ts +2 -2
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +1 -1
- package/dist/static.js.map +1 -1
- package/dist/types.d.ts +45 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -1
package/dist/extractor-ast.js
CHANGED
|
@@ -14,12 +14,15 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { parseSource, forEachExportedConst, unwrapExpression, stringFromExpression, objectFromExpression, objectProperty, stringProperty, propertyNameText, findPropertyCall, walk, lineOf, } from "./ast.js";
|
|
16
16
|
import { resolveDataPath } from "./data-path.js";
|
|
17
|
-
|
|
17
|
+
// `workflow` is the vNext graph authoring factory (@glubean/sdk workflow());
|
|
18
|
+
// its build() wraps the graph in a simple Test, so a `workflow("id")...` export
|
|
19
|
+
// is a runnable test for discovery purposes (S2.5).
|
|
20
|
+
const BASE_FNS = new Set(["test", "task", "workflow"]);
|
|
18
21
|
/** Whether a factory identifier name is a recognized test/task function. */
|
|
19
22
|
function isTestFnName(name, customFns) {
|
|
20
23
|
if (customFns)
|
|
21
24
|
return customFns.has(name);
|
|
22
|
-
// Convention fallback: test | task | *Test | *Task (capitalized suffix).
|
|
25
|
+
// Convention fallback: test | task | workflow | *Test | *Task (capitalized suffix).
|
|
23
26
|
return BASE_FNS.has(name) || /(?:Test|Task)$/.test(name);
|
|
24
27
|
}
|
|
25
28
|
/** Parse `{ id, name, tags, timeout, requires, defaultRun }` from a TestMeta object literal. */
|
|
@@ -31,6 +34,12 @@ function parseMetaObject(obj) {
|
|
|
31
34
|
const name = stringProperty(obj, "name");
|
|
32
35
|
if (name !== undefined)
|
|
33
36
|
out.name = name;
|
|
37
|
+
// WorkflowMeta.skip is a string reason (TestMeta.skip is boolean — a string
|
|
38
|
+
// here only ever comes from a workflow). Carried as `deferred` so the
|
|
39
|
+
// upload gate can exclude skipped workflows (codex S2.6 R13 P2).
|
|
40
|
+
const skipReason = stringProperty(obj, "skip");
|
|
41
|
+
if (skipReason !== undefined)
|
|
42
|
+
out.deferred = skipReason;
|
|
34
43
|
const tagsProp = objectProperty(obj, "tags");
|
|
35
44
|
if (tagsProp) {
|
|
36
45
|
const value = unwrapExpression(tagsProp.value);
|
|
@@ -64,6 +73,16 @@ function parseMetaObject(obj) {
|
|
|
64
73
|
out.defaultRun = defaultRun;
|
|
65
74
|
return out;
|
|
66
75
|
}
|
|
76
|
+
/** `parallel: true` from a meta OBJECT — read only for data-driven (.each/
|
|
77
|
+
* .pick) declarations; a plain test() meta must not gain a parallel group
|
|
78
|
+
* (codex S2.12 R5 P2). */
|
|
79
|
+
function parseMetaParallel(obj) {
|
|
80
|
+
const parallelProp = objectProperty(obj, "parallel");
|
|
81
|
+
if (!parallelProp)
|
|
82
|
+
return false;
|
|
83
|
+
const value = unwrapExpression(parallelProp.value);
|
|
84
|
+
return value?.type === "BooleanLiteral" && value.value === true;
|
|
85
|
+
}
|
|
67
86
|
/** Builder `.meta({...})` fields (name/tags/timeout/requires/defaultRun) from the chain. */
|
|
68
87
|
function builderMeta(init) {
|
|
69
88
|
const metaCall = findPropertyCall(init, "meta");
|
|
@@ -110,7 +129,611 @@ function chainHead(init) {
|
|
|
110
129
|
}
|
|
111
130
|
return node;
|
|
112
131
|
}
|
|
113
|
-
function
|
|
132
|
+
/** Like `walk`, but does NOT descend into nested function scopes (arrow /
|
|
133
|
+
* function expressions / declarations). The factory's graph calls live at its
|
|
134
|
+
* top level — branch sides etc. are flagged by the outer `.branch(` on the
|
|
135
|
+
* chain — while runtime callbacks may declare shadowing locals (`const wf =
|
|
136
|
+
* ...` inside an action) that must not leak into the scan (codex S2.12 R4). */
|
|
137
|
+
function walkSameScope(node, cb, onNestedFn) {
|
|
138
|
+
cb(node);
|
|
139
|
+
for (const key of Object.keys(node)) {
|
|
140
|
+
const value = node[key];
|
|
141
|
+
const visit = (v) => {
|
|
142
|
+
if (!v || typeof v !== "object")
|
|
143
|
+
return;
|
|
144
|
+
const child = v;
|
|
145
|
+
if (typeof child.type !== "string")
|
|
146
|
+
return;
|
|
147
|
+
if (child.type === "ArrowFunctionExpression" ||
|
|
148
|
+
child.type === "FunctionExpression" ||
|
|
149
|
+
child.type === "FunctionDeclaration") {
|
|
150
|
+
onNestedFn?.(child); // nested scope — the caller decides what to do
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
walkSameScope(child, cb, onNestedFn);
|
|
154
|
+
};
|
|
155
|
+
if (Array.isArray(value))
|
|
156
|
+
value.forEach(visit);
|
|
157
|
+
else
|
|
158
|
+
visit(value);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Babel emits Optional* variants for `?.` — every call/member check must
|
|
162
|
+
* accept both or optional chaining bypasses the gate (codex S2.13 R9 P2). */
|
|
163
|
+
function isCallNode(n) {
|
|
164
|
+
return n?.type === "CallExpression" || n?.type === "OptionalCallExpression";
|
|
165
|
+
}
|
|
166
|
+
function isMemberNode(n) {
|
|
167
|
+
return n?.type === "MemberExpression" || n?.type === "OptionalMemberExpression";
|
|
168
|
+
}
|
|
169
|
+
/** The branch-family + poll method names the Cloud-render gate flags. */
|
|
170
|
+
const BRANCH_FAMILY_METHODS = new Set(["branch", "poll", "pollAction", "switch", "route"]);
|
|
171
|
+
/** The full builder chain surface — used to decide whether a member call on
|
|
172
|
+
* a live name is the builder's OWN chain call (callbacks handled by the
|
|
173
|
+
* shadow-aware recursion) or an unknown method on a live CONTAINER
|
|
174
|
+
* (`h.makeFlow(() => b)`) whose function args must be deep-checked
|
|
175
|
+
* (codex S2.13 R17 P2). */
|
|
176
|
+
const KNOWN_BUILDER_METHODS = new Set([
|
|
177
|
+
"setup",
|
|
178
|
+
"teardown",
|
|
179
|
+
"meta",
|
|
180
|
+
"call",
|
|
181
|
+
"action",
|
|
182
|
+
"check",
|
|
183
|
+
"compute",
|
|
184
|
+
"branch",
|
|
185
|
+
"switch",
|
|
186
|
+
"route",
|
|
187
|
+
"poll",
|
|
188
|
+
"pollAction",
|
|
189
|
+
"use",
|
|
190
|
+
"group",
|
|
191
|
+
"build",
|
|
192
|
+
]);
|
|
193
|
+
/** The delegated callback of a use/group call: the FIRST function-shaped
|
|
194
|
+
* argument (use carries it at 0, group at 1 behind the id). None found →
|
|
195
|
+
* undefined → the caller fails closed. */
|
|
196
|
+
function delegatedCallbackOf(args) {
|
|
197
|
+
for (const arg of args ?? []) {
|
|
198
|
+
const u = unwrapExpression(arg);
|
|
199
|
+
if (u?.type === "ArrowFunctionExpression" || u?.type === "FunctionExpression")
|
|
200
|
+
return u;
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
/** Chain methods that DELEGATE part of the graph to a callback argument —
|
|
205
|
+
* the callback is scanned like an each-factory; references fail closed.
|
|
206
|
+
* "use" (S2.13) and "group" (S2.14) — phase4 §4's shared rule. */
|
|
207
|
+
const DELEGATING_CHAIN_METHODS = new Set(["use", "group"]);
|
|
208
|
+
/** Descend a receiver/initializer expression to its root identifier —
|
|
209
|
+
* through call chains AND member access (`h.b.branch(...)` roots at `h`:
|
|
210
|
+
* a container holding a live builder is itself suspect — codex S2.13 R7). */
|
|
211
|
+
function chainRootOf(expr) {
|
|
212
|
+
let root = expr ? unwrapExpression(expr) : undefined;
|
|
213
|
+
for (;;) {
|
|
214
|
+
if (!root)
|
|
215
|
+
return root;
|
|
216
|
+
if (isCallNode(root)) {
|
|
217
|
+
const innerCallee = unwrapExpression(root.callee);
|
|
218
|
+
if (isMemberNode(innerCallee)) {
|
|
219
|
+
root = unwrapExpression(innerCallee.object);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
return root;
|
|
223
|
+
}
|
|
224
|
+
if (isMemberNode(root)) {
|
|
225
|
+
// computed or not — for ROOT tracking, descending the object is the
|
|
226
|
+
// fail-closed direction (`h["b"].branch(...)` roots at h — codex R8).
|
|
227
|
+
root = unwrapExpression(root.object);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
return root;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** Does the expression subtree REFERENCE any live builder name? Same-scope
|
|
234
|
+
* only (nested functions are handled by the scanner's own recursion), and
|
|
235
|
+
* non-computed member/property KEY identifiers are skipped — `client.b` is a
|
|
236
|
+
* property name, not a reference (codex S2.13 R7). */
|
|
237
|
+
function containsLiveIdentifier(expr, live, opts) {
|
|
238
|
+
if (!expr || typeof expr !== "object")
|
|
239
|
+
return false;
|
|
240
|
+
let found = false;
|
|
241
|
+
const visit = (node) => {
|
|
242
|
+
if (found || !node || typeof node.type !== "string")
|
|
243
|
+
return;
|
|
244
|
+
// TYPE-ONLY subtrees never hold runtime references — a type argument
|
|
245
|
+
// named like the builder (`makeClient<b>()`) must not taint anything
|
|
246
|
+
// (codex S2.13 R15 P2). But TS EXPRESSION wrappers (`b as any`, `b!`)
|
|
247
|
+
// carry a real runtime expression — descend into it (codex R16 P2).
|
|
248
|
+
if (node.type.startsWith("TS")) {
|
|
249
|
+
const inner = node.expression;
|
|
250
|
+
if (inner && typeof inner.type === "string")
|
|
251
|
+
visit(inner);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (!opts?.intoFunctions &&
|
|
255
|
+
(node.type === "ArrowFunctionExpression" ||
|
|
256
|
+
node.type === "FunctionExpression" ||
|
|
257
|
+
node.type === "FunctionDeclaration")) {
|
|
258
|
+
return; // nested scope — the scanner recurses there separately
|
|
259
|
+
}
|
|
260
|
+
if (node.type === "Identifier") {
|
|
261
|
+
if (live.has(node.name))
|
|
262
|
+
found = true;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
for (const key of Object.keys(node)) {
|
|
266
|
+
// skip NAME positions: a non-computed member's property and a
|
|
267
|
+
// non-computed object-property key are labels, not references.
|
|
268
|
+
if (key === "typeParameters" ||
|
|
269
|
+
key === "typeAnnotation" ||
|
|
270
|
+
key === "returnType" ||
|
|
271
|
+
key === "superTypeParameters" ||
|
|
272
|
+
(isMemberNode(node) && node.computed !== true && key === "property") ||
|
|
273
|
+
((node.type === "ObjectProperty" ||
|
|
274
|
+
node.type === "Property" ||
|
|
275
|
+
// `{ b() {} }` — a method NAME is a label too (codex S2.13 R11 P2)
|
|
276
|
+
node.type === "ObjectMethod" ||
|
|
277
|
+
node.type === "ClassMethod") &&
|
|
278
|
+
node.computed !== true &&
|
|
279
|
+
key === "key")) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const value = node[key];
|
|
283
|
+
if (Array.isArray(value)) {
|
|
284
|
+
for (const v of value) {
|
|
285
|
+
if (v && typeof v.type === "string")
|
|
286
|
+
visit(v);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else if (value && typeof value.type === "string") {
|
|
290
|
+
visit(value);
|
|
291
|
+
}
|
|
292
|
+
if (found)
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
visit(unwrapExpression(expr) ?? expr);
|
|
297
|
+
return found;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* The ONE shared rule (phase4 §4): any construct handing graph authorship to
|
|
301
|
+
* a callback — each/pick factories, `.use(` fragments, (S2.14) `.group(`
|
|
302
|
+
* bodies — is scanned with the binding-aware live/everLive dataflow
|
|
303
|
+
* (codex S2.12 R2/R4/R10/R12/R13/R14/R15/R18) when INLINE.
|
|
304
|
+
*
|
|
305
|
+
* Returns `true` (branch-family call rooted at the builder found), `false`
|
|
306
|
+
* (inline and clean), or `undefined` (not an inspectable inline function —
|
|
307
|
+
* the caller fails CLOSED).
|
|
308
|
+
*/
|
|
309
|
+
function scanCallbackForBranchFamily(fn) {
|
|
310
|
+
if (!fn ||
|
|
311
|
+
(fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression")) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
// TS `this` parameters are erased at runtime but occupy params[0] in the
|
|
315
|
+
// AST (`function (this: void, b) {...}`) — skip them or the scan tracks
|
|
316
|
+
// "this" instead of the builder (codex S2.13 R21 P2).
|
|
317
|
+
const params = (fn.params ?? []).filter((p) => !(p.type === "Identifier" && p.name === "this"));
|
|
318
|
+
const builderParam = params[0]?.type === "Identifier" ? params[0].name : undefined;
|
|
319
|
+
if (!builderParam)
|
|
320
|
+
return undefined; // destructured/absent — cannot root the dataflow
|
|
321
|
+
// A default-parameter INITIALIZER can author the delegated chain before the
|
|
322
|
+
// body (`(b, r = b.branch(...)) => r`) — the body-only scan would miss it
|
|
323
|
+
// (codex S2.13 R5 P2). Only initializers can EXECUTE code, so fail closed
|
|
324
|
+
// exactly on AssignmentPattern (incl. nested inside destructuring); a plain
|
|
325
|
+
// destructured row param (`(wf, { region }) => ...`) is pure binding and
|
|
326
|
+
// stays scannable (codex S2.13 R6 P2).
|
|
327
|
+
for (const param of (params ?? []).slice(1)) {
|
|
328
|
+
let hasInitializer = false;
|
|
329
|
+
walk(param, (n) => {
|
|
330
|
+
if (n.type === "AssignmentPattern")
|
|
331
|
+
hasInitializer = true;
|
|
332
|
+
});
|
|
333
|
+
if (hasInitializer)
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
let found = false;
|
|
337
|
+
// Per-scope dataflow: every scope folds its own bindings into the inherited
|
|
338
|
+
// builder-name set in SOURCE ORDER, detects calls against the live set, then
|
|
339
|
+
// recurses into nested functions with the everLive snapshot (closures
|
|
340
|
+
// capture bindings — see the S2.12 R14/R15 commit messages for the cases).
|
|
341
|
+
const scanScope = (scopeBody, inherited, inheritedDirect) => {
|
|
342
|
+
const live = new Set(inherited);
|
|
343
|
+
const everLive = new Set(inherited);
|
|
344
|
+
// DIRECT builder aliases (the chain itself) vs tainted CONTAINERS — the
|
|
345
|
+
// own-chain callback exemption may only apply to direct aliases: a
|
|
346
|
+
// container can carry a foreign function under a builder-method name
|
|
347
|
+
// (`{ b, compute: makeFlow }` — codex S2.13 R18 P2).
|
|
348
|
+
const liveDirect = new Set(inheritedDirect);
|
|
349
|
+
const everDirect = new Set(inheritedDirect);
|
|
350
|
+
// All names BOUND by a target — an Identifier, or every binding inside a
|
|
351
|
+
// destructuring pattern (`const { b: x } = h` binds x — codex S2.13 R10).
|
|
352
|
+
const boundNames = (target) => {
|
|
353
|
+
if (!target)
|
|
354
|
+
return [];
|
|
355
|
+
if (target.type === "Identifier")
|
|
356
|
+
return [target.name];
|
|
357
|
+
const names = [];
|
|
358
|
+
const visit = (node) => {
|
|
359
|
+
if (!node || typeof node.type !== "string")
|
|
360
|
+
return;
|
|
361
|
+
if (node.type === "Identifier") {
|
|
362
|
+
names.push(node.name);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (node.type === "ObjectPattern") {
|
|
366
|
+
for (const propNode of node.properties ?? []) {
|
|
367
|
+
if (propNode.type === "ObjectProperty" || propNode.type === "Property") {
|
|
368
|
+
visit(propNode.value); // the BINDING side, never the key
|
|
369
|
+
}
|
|
370
|
+
else if (propNode.type === "RestElement") {
|
|
371
|
+
visit(propNode.argument);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (node.type === "ArrayPattern") {
|
|
377
|
+
for (const el of node.elements ?? []) {
|
|
378
|
+
if (el)
|
|
379
|
+
visit(el);
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (node.type === "RestElement")
|
|
384
|
+
visit(node.argument);
|
|
385
|
+
if (node.type === "AssignmentPattern")
|
|
386
|
+
visit(node.left);
|
|
387
|
+
};
|
|
388
|
+
visit(target);
|
|
389
|
+
return names;
|
|
390
|
+
};
|
|
391
|
+
const bind = (nameNode, valueExpr, isDeclaration) => {
|
|
392
|
+
const names = boundNames(nameNode);
|
|
393
|
+
if (names.length === 0)
|
|
394
|
+
return;
|
|
395
|
+
// A PATTERN-INTERNAL default can smuggle the builder into a binding
|
|
396
|
+
// even when the RHS is clean: `const { x = b } = {}` (codex S2.13 R13).
|
|
397
|
+
let patternDefaultIsLive = false;
|
|
398
|
+
if (nameNode && nameNode.type !== "Identifier") {
|
|
399
|
+
walk(nameNode, (n) => {
|
|
400
|
+
if (n.type === "AssignmentPattern" &&
|
|
401
|
+
containsLiveIdentifier(n.right, live)) {
|
|
402
|
+
patternDefaultIsLive = true;
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
// A value that CONTAINS a live builder reference (chain root, or a
|
|
407
|
+
// wrapper like `{ b }` — codex S2.13 R7) makes every bound name
|
|
408
|
+
// suspect: alias them all (fail-closed; destructuring from a live
|
|
409
|
+
// container — codex S2.13 R10).
|
|
410
|
+
if (patternDefaultIsLive || containsLiveIdentifier(valueExpr, live)) {
|
|
411
|
+
// A plain Identifier bound to a chain rooted at a DIRECT alias is
|
|
412
|
+
// itself direct (`const c = b.compute(...)`); anything else (wrapper
|
|
413
|
+
// objects, destructured pulls, pattern defaults) is tainted-only.
|
|
414
|
+
const valueRoot = chainRootOf(valueExpr);
|
|
415
|
+
const isDirectChain = nameNode?.type === "Identifier" &&
|
|
416
|
+
valueRoot?.type === "Identifier" &&
|
|
417
|
+
liveDirect.has(valueRoot.name);
|
|
418
|
+
for (const name of names) {
|
|
419
|
+
live.add(name);
|
|
420
|
+
everLive.add(name);
|
|
421
|
+
if (isDirectChain) {
|
|
422
|
+
liveDirect.add(name);
|
|
423
|
+
everDirect.add(name);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else if (valueExpr !== undefined) {
|
|
428
|
+
for (const name of names) {
|
|
429
|
+
live.delete(name);
|
|
430
|
+
liveDirect.delete(name);
|
|
431
|
+
if (isDeclaration) {
|
|
432
|
+
everLive.delete(name);
|
|
433
|
+
everDirect.delete(name);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
const deferredFns = [];
|
|
439
|
+
// Block-aware traversal (codex S2.13 R9 P2): a block-scoped shadow
|
|
440
|
+
// (`{ const b = client; }`) must not delete the outer builder for the
|
|
441
|
+
// REST of the function. On block exit the pre-block names are UNIONED
|
|
442
|
+
// back in: block-local deletions are undone, block-born aliases leak
|
|
443
|
+
// outward (fail-closed direction; a block-local `b = client` assignment
|
|
444
|
+
// being undone is the same conservative trade).
|
|
445
|
+
const handleNode = (n) => {
|
|
446
|
+
if (n.type === "VariableDeclarator") {
|
|
447
|
+
bind(n.id, n.init, true);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (n.type === "AssignmentExpression" &&
|
|
451
|
+
(n.operator === "=" ||
|
|
452
|
+
// logical assignments MAY assign at runtime — taint like plain
|
|
453
|
+
// `=` on the live side; never delete on the foreign side (the
|
|
454
|
+
// old value may survive) — codex S2.13 R16 P2.
|
|
455
|
+
n.operator === "??=" ||
|
|
456
|
+
n.operator === "||=" ||
|
|
457
|
+
n.operator === "&&=")) {
|
|
458
|
+
const conditional = n.operator !== "=";
|
|
459
|
+
const left = unwrapExpression(n.left);
|
|
460
|
+
// A MEMBER write storing a live builder taints the container:
|
|
461
|
+
// `h.b = b; h.b.branch(...)` (codex S2.13 R14 P2).
|
|
462
|
+
if (left && isMemberNode(left)) {
|
|
463
|
+
if (containsLiveIdentifier(n.right, live)) {
|
|
464
|
+
const root = chainRootOf(left);
|
|
465
|
+
if (root?.type === "Identifier") {
|
|
466
|
+
live.add(root.name);
|
|
467
|
+
everLive.add(root.name);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (conditional) {
|
|
473
|
+
// taint-only path: add aliases when the RHS is live, never delete
|
|
474
|
+
// (and never as DIRECT — the assignment may not happen)
|
|
475
|
+
if (left?.type === "Identifier" && containsLiveIdentifier(n.right, live)) {
|
|
476
|
+
live.add(left.name);
|
|
477
|
+
everLive.add(left.name);
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
bind(left, n.right, false);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (found)
|
|
485
|
+
return;
|
|
486
|
+
// CONSTRUCTOR delegation: `new Fragment(b)` / `new X(() => b)` hands
|
|
487
|
+
// the builder (or a capturing closure) to uninspectable code — same
|
|
488
|
+
// fail-closed rule as call arguments (codex S2.13 R17 P2).
|
|
489
|
+
if (n.type === "NewExpression") {
|
|
490
|
+
for (const arg of n.arguments ?? []) {
|
|
491
|
+
const u = unwrapExpression(arg) ?? arg;
|
|
492
|
+
const isFnArg = u.type === "ArrowFunctionExpression" || u.type === "FunctionExpression";
|
|
493
|
+
if (containsLiveIdentifier(arg, live, { intoFunctions: isFnArg })) {
|
|
494
|
+
found = true;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (!isCallNode(n))
|
|
501
|
+
return;
|
|
502
|
+
const callee = unwrapExpression(n.callee);
|
|
503
|
+
// An EXTRACTED method alias (`const branch = b.branch; branch(...)`)
|
|
504
|
+
// calls a live alias directly — no member access to inspect; fail
|
|
505
|
+
// closed (codex S2.13 R13 P2).
|
|
506
|
+
if (callee?.type === "Identifier" && live.has(callee.name)) {
|
|
507
|
+
found = true;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
let methodName;
|
|
511
|
+
if (callee && isMemberNode(callee)) {
|
|
512
|
+
const prop = unwrapExpression(callee.property);
|
|
513
|
+
if (callee.computed !== true && prop?.type === "Identifier") {
|
|
514
|
+
methodName = prop.name;
|
|
515
|
+
}
|
|
516
|
+
else if (callee.computed === true && prop?.type === "StringLiteral") {
|
|
517
|
+
// bracket syntax with a literal name resolves statically
|
|
518
|
+
methodName = prop.value;
|
|
519
|
+
}
|
|
520
|
+
else if (callee.computed === true &&
|
|
521
|
+
containsLiveIdentifier(callee.object, live)) {
|
|
522
|
+
// a DYNAMIC method on a live builder cannot be resolved — fail
|
|
523
|
+
// closed (codex S2.13 R8 P2).
|
|
524
|
+
found = true;
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Nested delegation (`b.use(innerFragment)`): recurse into inline
|
|
529
|
+
// callbacks; references fail closed — BEFORE the generic
|
|
530
|
+
// argument-delegation check so an inline-and-clean fragment doesn't
|
|
531
|
+
// false-positive on its own builder argument.
|
|
532
|
+
if (methodName !== undefined &&
|
|
533
|
+
DELEGATING_CHAIN_METHODS.has(methodName) &&
|
|
534
|
+
(() => {
|
|
535
|
+
const root = chainRootOf(callee.object);
|
|
536
|
+
// DIRECT aliases only — `h.use(cb)` on a tainted container is a
|
|
537
|
+
// foreign method wearing the name (codex S2.13 R19 P2); it falls
|
|
538
|
+
// through to the foreign-call deep checks below.
|
|
539
|
+
return root?.type === "Identifier" && liveDirect.has(root.name);
|
|
540
|
+
})()) {
|
|
541
|
+
const cb = delegatedCallbackOf(n.arguments);
|
|
542
|
+
const verdict = scanCallbackForBranchFamily(cb);
|
|
543
|
+
if (verdict !== false)
|
|
544
|
+
found = true;
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
// DELEGATION fails closed (codex S2.12 R18 P2): passing a live
|
|
548
|
+
// builder to any call — bare (`makeFlow(wf)`) or WRAPPED
|
|
549
|
+
// (`makeFlow({ wf })`, codex S2.13 R7) — hands the graph to code
|
|
550
|
+
// this scan cannot see. Nested-function arguments are exempt (the
|
|
551
|
+
// scanner recurses into them itself).
|
|
552
|
+
// Whose call is this? A live builder's OWN chain call (`b.action("x",
|
|
553
|
+
// cb)`) hands its callbacks to the runtime — the recursion scans them
|
|
554
|
+
// shadow-aware. A FOREIGN call's function argument capturing the
|
|
555
|
+
// builder (`makeFlow(() => b)`) hands authoring capability to code we
|
|
556
|
+
// can't see — deep-check those (codex S2.13 R15 P2; shadow-unaware by
|
|
557
|
+
// design, fail-closed).
|
|
558
|
+
const calleeRootForArgs = callee && isMemberNode(callee)
|
|
559
|
+
? chainRootOf(callee.object)
|
|
560
|
+
: undefined;
|
|
561
|
+
// The exemption holds only for the builder's OWN chain surface — an
|
|
562
|
+
// unknown method on a live CONTAINER (`h.makeFlow(() => b)`) is a
|
|
563
|
+
// foreign call wearing a live root (codex S2.13 R17 P2).
|
|
564
|
+
const calleeIsLiveChain = calleeRootForArgs?.type === "Identifier" &&
|
|
565
|
+
liveDirect.has(calleeRootForArgs.name) &&
|
|
566
|
+
methodName !== undefined &&
|
|
567
|
+
KNOWN_BUILDER_METHODS.has(methodName);
|
|
568
|
+
for (const arg of n.arguments ?? []) {
|
|
569
|
+
const unwrappedArg = unwrapExpression(arg) ?? arg;
|
|
570
|
+
const isFnArg = unwrappedArg.type === "ArrowFunctionExpression" ||
|
|
571
|
+
unwrappedArg.type === "FunctionExpression";
|
|
572
|
+
if (containsLiveIdentifier(arg, live, {
|
|
573
|
+
intoFunctions: isFnArg && !calleeIsLiveChain,
|
|
574
|
+
})) {
|
|
575
|
+
found = true;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// A member call on a LIVE root that is NOT the builder's own chain
|
|
580
|
+
// surface hands the receiver — and the builder inside it — to foreign
|
|
581
|
+
// code as `this` (`h.use(cb)` reaching b via this.b — codex S2.13 R19
|
|
582
|
+
// P2). Fail closed.
|
|
583
|
+
if (calleeRootForArgs?.type === "Identifier" &&
|
|
584
|
+
live.has(calleeRootForArgs.name) &&
|
|
585
|
+
!calleeIsLiveChain) {
|
|
586
|
+
found = true;
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (methodName === undefined || !BRANCH_FAMILY_METHODS.has(methodName))
|
|
590
|
+
return;
|
|
591
|
+
const root = chainRootOf(callee.object);
|
|
592
|
+
if (root?.type === "Identifier") {
|
|
593
|
+
if (live.has(root.name))
|
|
594
|
+
found = true;
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
// The receiver didn't root at an identifier — a conditional/sequence
|
|
598
|
+
// or other expression (`(cond ? b : other).branch(...)`). If it
|
|
599
|
+
// CONTAINS a live builder anywhere, fail closed (codex S2.13 R14 P2).
|
|
600
|
+
if (containsLiveIdentifier(callee.object, live)) {
|
|
601
|
+
found = true;
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
const visitTree = (node) => {
|
|
605
|
+
if (node.type === "ArrowFunctionExpression" ||
|
|
606
|
+
node.type === "FunctionExpression" ||
|
|
607
|
+
node.type === "FunctionDeclaration") {
|
|
608
|
+
deferredFns.push({ fn: node, excluded: new Set() });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const isBlock = node.type === "BlockStatement" && node !== scopeBody;
|
|
612
|
+
const liveSnap = isBlock ? new Set(live) : undefined;
|
|
613
|
+
const everSnap = isBlock ? new Set(everLive) : undefined;
|
|
614
|
+
const directSnap = isBlock ? new Set(liveDirect) : undefined;
|
|
615
|
+
const everDirectSnap = isBlock ? new Set(everDirect) : undefined;
|
|
616
|
+
const deferredStart = isBlock ? deferredFns.length : 0;
|
|
617
|
+
handleNode(node);
|
|
618
|
+
for (const key of Object.keys(node)) {
|
|
619
|
+
const value = node[key];
|
|
620
|
+
const visitChild = (v) => {
|
|
621
|
+
if (!v || typeof v !== "object")
|
|
622
|
+
return;
|
|
623
|
+
const child = v;
|
|
624
|
+
if (typeof child.type !== "string")
|
|
625
|
+
return;
|
|
626
|
+
visitTree(child);
|
|
627
|
+
};
|
|
628
|
+
if (Array.isArray(value))
|
|
629
|
+
value.forEach(visitChild);
|
|
630
|
+
else
|
|
631
|
+
visitChild(value);
|
|
632
|
+
}
|
|
633
|
+
if (isBlock) {
|
|
634
|
+
// Names a block-local DECLARATION shadowed (present before, deleted
|
|
635
|
+
// by a declaration inside): closures defined IN this block captured
|
|
636
|
+
// the shadow binding, not the outer builder — scanning them against
|
|
637
|
+
// the restored outer names would false-positive a linear workflow
|
|
638
|
+
// (codex S2.13 R20 P2). Record exclusions before the union restore.
|
|
639
|
+
// Late-ASSIGNED aliases (R15) are unaffected: assignments never
|
|
640
|
+
// delete from everLive, so they don't appear here.
|
|
641
|
+
for (const name of everSnap) {
|
|
642
|
+
if (!everLive.has(name)) {
|
|
643
|
+
for (let i = deferredStart; i < deferredFns.length; i++) {
|
|
644
|
+
deferredFns[i].excluded.add(name);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
for (const n of liveSnap)
|
|
649
|
+
live.add(n);
|
|
650
|
+
for (const n of everSnap)
|
|
651
|
+
everLive.add(n);
|
|
652
|
+
for (const n of directSnap)
|
|
653
|
+
liveDirect.add(n);
|
|
654
|
+
for (const n of everDirectSnap)
|
|
655
|
+
everDirect.add(n);
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
visitTree(scopeBody);
|
|
659
|
+
const fnQueue = [...deferredFns];
|
|
660
|
+
while (fnQueue.length > 0) {
|
|
661
|
+
if (found)
|
|
662
|
+
return;
|
|
663
|
+
const { fn: fnNode, excluded } = fnQueue.shift();
|
|
664
|
+
const childInherited = new Set(everLive);
|
|
665
|
+
const childDirect = new Set(everDirect);
|
|
666
|
+
for (const name of excluded) {
|
|
667
|
+
childInherited.delete(name);
|
|
668
|
+
childDirect.delete(name);
|
|
669
|
+
}
|
|
670
|
+
for (const param of fnNode.params ?? []) {
|
|
671
|
+
// A default-parameter INITIALIZER referencing a live builder executes
|
|
672
|
+
// at call time with the closure's bindings (`const make = (x =
|
|
673
|
+
// b.branch(...)) => x`) — the body-only recursion never sees it, and
|
|
674
|
+
// the param may BECOME the builder (`(x = b) => x.branch()`).
|
|
675
|
+
// Initializers can hide ANYWHERE in a destructuring pattern
|
|
676
|
+
// (`({ x = b.branch(...) } = {}) => x` — codex S2.13 R11 P2), so the
|
|
677
|
+
// WHOLE param subtree is searched. Fail closed on the reference.
|
|
678
|
+
// A CLOSURE inside an initializer (`(x = () => b.branch(...)) =>`)
|
|
679
|
+
// is invisible to containsLiveIdentifier (it skips nested fns) —
|
|
680
|
+
// queue it for the same recursive scan as any other nested function
|
|
681
|
+
// (codex S2.13 R12 P2).
|
|
682
|
+
let liveInitializer = false;
|
|
683
|
+
walk(param, (n) => {
|
|
684
|
+
if (n.type === "AssignmentPattern" &&
|
|
685
|
+
containsLiveIdentifier(n.right, everLive)) {
|
|
686
|
+
liveInitializer = true;
|
|
687
|
+
}
|
|
688
|
+
if (n.type === "ArrowFunctionExpression" || n.type === "FunctionExpression") {
|
|
689
|
+
fnQueue.push({ fn: n, excluded: new Set(excluded) });
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
if (liveInitializer) {
|
|
693
|
+
found = true;
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
for (const name of boundNames(param)) {
|
|
697
|
+
childInherited.delete(name);
|
|
698
|
+
childDirect.delete(name);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
scanScope(fnNode.body, childInherited, childDirect);
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
scanScope(fn.body, new Set([builderParam]), new Set([builderParam]));
|
|
705
|
+
return found;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Local names bound to the SDK's `workflow` factory via named-import aliases
|
|
709
|
+
* (`import { workflow as journeyTest } ...`). The literal `workflow` is always
|
|
710
|
+
* included. Without this, an alias that happens to satisfy the `*Test`/`*Task`
|
|
711
|
+
* convention (or customFns) would be classified as a plain test and bypass the
|
|
712
|
+
* workflow marker + the --upload branch/poll gate (codex S2.6 R12 P2).
|
|
713
|
+
*/
|
|
714
|
+
function collectWorkflowAliases(source) {
|
|
715
|
+
const aliases = new Set(["workflow"]);
|
|
716
|
+
const body = source.program.body ?? [];
|
|
717
|
+
for (const stmt of body) {
|
|
718
|
+
if (stmt.type !== "ImportDeclaration")
|
|
719
|
+
continue;
|
|
720
|
+
const specifiers = stmt.specifiers ?? [];
|
|
721
|
+
for (const spec of specifiers) {
|
|
722
|
+
if (spec.type !== "ImportSpecifier")
|
|
723
|
+
continue;
|
|
724
|
+
const imported = spec.imported;
|
|
725
|
+
const importedName = imported?.type === "Identifier"
|
|
726
|
+
? imported.name
|
|
727
|
+
: imported?.value; // string import names
|
|
728
|
+
const local = spec.local;
|
|
729
|
+
if (importedName === "workflow" && local?.type === "Identifier") {
|
|
730
|
+
aliases.add(local.name);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return aliases;
|
|
735
|
+
}
|
|
736
|
+
function parseTestDeclaration(decl, statement, customFns, workflowAliases) {
|
|
114
737
|
const exportName = decl.id?.name;
|
|
115
738
|
if (!exportName)
|
|
116
739
|
return undefined;
|
|
@@ -152,8 +775,13 @@ function parseTestDeclaration(decl, statement, customFns) {
|
|
|
152
775
|
else {
|
|
153
776
|
return undefined;
|
|
154
777
|
}
|
|
155
|
-
|
|
778
|
+
// A workflow import alias is always accepted (even when it matches no
|
|
779
|
+
// test-name convention) and always classified as a workflow — the marker
|
|
780
|
+
// must not depend on what the alias happens to look like (codex S2.6 R12).
|
|
781
|
+
const isWorkflowFactory = workflowAliases?.has(factoryName) ?? factoryName === "workflow";
|
|
782
|
+
if (!factoryName || (!isTestFnName(factoryName, customFns) && !isWorkflowFactory)) {
|
|
156
783
|
return undefined;
|
|
784
|
+
}
|
|
157
785
|
// Resolve id + inline meta from the first argument (string id or TestMeta obj).
|
|
158
786
|
let fields = {};
|
|
159
787
|
const metaObj = objectFromExpression(metaArg);
|
|
@@ -177,10 +805,15 @@ function parseTestDeclaration(decl, statement, customFns) {
|
|
|
177
805
|
fields.requires = bMeta.requires;
|
|
178
806
|
if (fields.defaultRun === undefined && bMeta.defaultRun !== undefined)
|
|
179
807
|
fields.defaultRun = bMeta.defaultRun;
|
|
808
|
+
if (fields.deferred === undefined && bMeta.deferred !== undefined)
|
|
809
|
+
fields.deferred = bMeta.deferred;
|
|
180
810
|
if (fields.id === undefined)
|
|
181
811
|
return undefined;
|
|
182
812
|
// `.each(data, { parallel: true })`.
|
|
183
|
-
|
|
813
|
+
// meta-object `parallel` counts only for data-driven declarations
|
|
814
|
+
// (workflow.each puts it there); legacy each-args form parsed below.
|
|
815
|
+
const metaObjForParallel = (variant === "each" || variant === "pick") && metaArg ? objectFromExpression(metaArg) : undefined;
|
|
816
|
+
let parallel = metaObjForParallel ? parseMetaParallel(metaObjForParallel) : false;
|
|
184
817
|
if (variant === "each" && eachArgs && eachArgs.length > 1) {
|
|
185
818
|
const optsObj = objectFromExpression(eachArgs[1]);
|
|
186
819
|
const parallelProp = optsObj ? objectProperty(optsObj, "parallel") : undefined;
|
|
@@ -211,6 +844,20 @@ function parseTestDeclaration(decl, statement, customFns) {
|
|
|
211
844
|
result.steps = steps;
|
|
212
845
|
if (parallel)
|
|
213
846
|
result.parallel = true;
|
|
847
|
+
// vNext workflow exports get a static marker so downstream consumers can
|
|
848
|
+
// classify them as graph orchestrators ("flow"-kind runnables) without
|
|
849
|
+
// importing the file, plus an AST-level branch/poll flag so the --upload
|
|
850
|
+
// gate fails closed for .test.ts workflows the runtime extractor can never
|
|
851
|
+
// safely inspect (codex S2.6 R10 P2).
|
|
852
|
+
if (isWorkflowFactory) {
|
|
853
|
+
result.workflow = true;
|
|
854
|
+
// WorkflowMeta.skip (string reason) — surfaced so the upload gate can
|
|
855
|
+
// exclude skipped workflows like deferred flows (codex S2.6 R13 P2).
|
|
856
|
+
// Only emitted for workflows: TestMeta.skip is boolean, so a string skip
|
|
857
|
+
// never legitimately appears on a plain test.
|
|
858
|
+
if (fields.deferred !== undefined)
|
|
859
|
+
result.deferred = fields.deferred;
|
|
860
|
+
}
|
|
214
861
|
return result;
|
|
215
862
|
}
|
|
216
863
|
/**
|
|
@@ -229,9 +876,10 @@ export function extractFromSource(content, customFns) {
|
|
|
229
876
|
return [];
|
|
230
877
|
}
|
|
231
878
|
const fns = customFns && customFns.length > 0 ? new Set([...BASE_FNS, ...customFns]) : undefined;
|
|
879
|
+
const workflowAliases = collectWorkflowAliases(source);
|
|
232
880
|
const results = [];
|
|
233
881
|
forEachExportedConst(source, (statement, declaration) => {
|
|
234
|
-
const meta = parseTestDeclaration(declaration, statement, fns);
|
|
882
|
+
const meta = parseTestDeclaration(declaration, statement, fns, workflowAliases);
|
|
235
883
|
if (meta)
|
|
236
884
|
results.push(meta);
|
|
237
885
|
});
|
|
@@ -320,15 +968,35 @@ function readCases(casesObj) {
|
|
|
320
968
|
const key = propertyNameText(property);
|
|
321
969
|
if (key === undefined)
|
|
322
970
|
continue;
|
|
323
|
-
|
|
971
|
+
// `evt: inboundCase({...})` (I2 authoring helper) — unwrap to the spec
|
|
972
|
+
// argument so description etc. stay readable, and mark the direction so
|
|
973
|
+
// static-fallback discovery never advertises a non-runnable case.
|
|
974
|
+
const rawValue = unwrapExpression(property.value);
|
|
975
|
+
let inbound = false;
|
|
976
|
+
let bodySource = rawValue;
|
|
977
|
+
if (rawValue?.type === "CallExpression") {
|
|
978
|
+
const callee = unwrapExpression(rawValue.callee);
|
|
979
|
+
if (callee?.type === "Identifier" && callee.name === "inboundCase") {
|
|
980
|
+
inbound = true;
|
|
981
|
+
bodySource = rawValue.arguments?.[0];
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const body = objectFromExpression(bodySource);
|
|
324
985
|
if (!body) {
|
|
325
986
|
// Reference / shorthand case value (`cases: { ok }` or `ok: sharedCase`):
|
|
326
987
|
// the case body isn't an inline object, so per-case fields can't be read,
|
|
327
988
|
// but the KEY is still a real case — preserve it (VSCode discovery needs it).
|
|
328
|
-
out.push({
|
|
989
|
+
out.push({
|
|
990
|
+
key,
|
|
991
|
+
line: lineOf(property.key ?? property.value),
|
|
992
|
+
...(inbound ? { direction: "inbound" } : {}),
|
|
993
|
+
});
|
|
329
994
|
continue;
|
|
330
995
|
}
|
|
331
996
|
const meta = { key, line: lineOf(property.value) };
|
|
997
|
+
if (inbound || stringProperty(body, "direction") === "inbound") {
|
|
998
|
+
meta.direction = "inbound";
|
|
999
|
+
}
|
|
332
1000
|
const description = stringProperty(body, "description");
|
|
333
1001
|
if (description !== undefined)
|
|
334
1002
|
meta.description = description;
|
|
@@ -412,85 +1080,6 @@ export function extractContractCases(content, opts) {
|
|
|
412
1080
|
});
|
|
413
1081
|
return results;
|
|
414
1082
|
}
|
|
415
|
-
/**
|
|
416
|
-
* Structural (marker-free) flow extraction: every `export const X = <fn>.flow("id")…`
|
|
417
|
-
* → `FlowStaticMeta`. Replaces vscode's `// @flow`-gated extractMarkedFlows — AST
|
|
418
|
-
* recognizes the `.flow("id")` call shape directly, so no magic comment is needed
|
|
419
|
-
* (aligns vscode discovery with the CLI, which never required markers). Returns []
|
|
420
|
-
* on parse error.
|
|
421
|
-
*/
|
|
422
|
-
export function extractFlows(content) {
|
|
423
|
-
let source;
|
|
424
|
-
try {
|
|
425
|
-
source = parseSource(content);
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
return [];
|
|
429
|
-
}
|
|
430
|
-
const results = [];
|
|
431
|
-
forEachExportedConst(source, (statement, declaration) => {
|
|
432
|
-
const exportName = declaration.id?.name;
|
|
433
|
-
const init = declaration.init;
|
|
434
|
-
if (!exportName || !init)
|
|
435
|
-
return;
|
|
436
|
-
const flowCall = findFlowCall(init);
|
|
437
|
-
if (!flowCall)
|
|
438
|
-
return;
|
|
439
|
-
// `flow(idOrMeta: string | FlowMeta)` — string id or `{ id, ... }`. Runtime
|
|
440
|
-
// honors the object's `id`; its `skip` is IGNORED at runtime (skip is only
|
|
441
|
-
// applied from a chained `.meta({ skip })`), so we read skip only from there.
|
|
442
|
-
const flowArg = flowCall.arguments?.[0];
|
|
443
|
-
const flowMetaObj = objectFromExpression(flowArg);
|
|
444
|
-
const flowId = stringFromExpression(flowArg) ?? (flowMetaObj && stringProperty(flowMetaObj, "id"));
|
|
445
|
-
if (!flowId)
|
|
446
|
-
return;
|
|
447
|
-
const metaCall = findPropertyCall(init, "meta");
|
|
448
|
-
const metaObj = metaCall
|
|
449
|
-
? objectFromExpression(metaCall.arguments?.[0])
|
|
450
|
-
: undefined;
|
|
451
|
-
const skip = metaObj ? stringProperty(metaObj, "skip") : undefined;
|
|
452
|
-
const meta = { exportName, line: lineOf(statement), flowId };
|
|
453
|
-
if (skip !== undefined)
|
|
454
|
-
meta.skip = skip;
|
|
455
|
-
results.push(meta);
|
|
456
|
-
});
|
|
457
|
-
return results;
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Find the Glubean flow call in a chain: `contract.flow(...)` — a `.flow` member
|
|
461
|
-
* on the `contract` namespace. Does NOT match an arbitrary `.flow(...)` on some
|
|
462
|
-
* other object (`otherLib.flow(...)`) nor a bare `flow(...)`, which the runtime
|
|
463
|
-
* would not recognize as a flow.
|
|
464
|
-
*
|
|
465
|
-
* Like `findContractCall` / the former regex, this keys off the literal name
|
|
466
|
-
* `contract` and does not resolve `import { contract as x }` aliases — a known,
|
|
467
|
-
* consistent limitation across the contract + flow static extractors (real code
|
|
468
|
-
* uses `contract.flow(...)`).
|
|
469
|
-
*/
|
|
470
|
-
function findFlowCall(init) {
|
|
471
|
-
let node = unwrapExpression(init);
|
|
472
|
-
while (node && node.type === "CallExpression") {
|
|
473
|
-
const callee = unwrapExpression(node.callee);
|
|
474
|
-
if (!callee)
|
|
475
|
-
break;
|
|
476
|
-
if (callee.type === "MemberExpression" && callee.computed !== true) {
|
|
477
|
-
const object = callee.object;
|
|
478
|
-
const property = callee.property;
|
|
479
|
-
if (property.type === "Identifier" && property.name === "flow" &&
|
|
480
|
-
object.type === "Identifier" && object.name === "contract") {
|
|
481
|
-
return node; // contract.flow("id")
|
|
482
|
-
}
|
|
483
|
-
node = unwrapExpression(object);
|
|
484
|
-
}
|
|
485
|
-
else if (callee.type === "CallExpression") {
|
|
486
|
-
node = callee;
|
|
487
|
-
}
|
|
488
|
-
else {
|
|
489
|
-
break;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
return undefined;
|
|
493
|
-
}
|
|
494
1083
|
/** Classify a data-loader call (`fromDir(...)`, `fromYaml.map(...)`, …) → {type, raw path}. */
|
|
495
1084
|
function classifyLoader(call) {
|
|
496
1085
|
const callee = unwrapExpression(call.callee);
|