@gate-forge/pack-http 0.7.1 → 0.9.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 +70 -0
- package/dist/client-calls.d.ts +26 -0
- package/dist/client-calls.d.ts.map +1 -1
- package/dist/client-calls.js +664 -15
- package/dist/client-calls.js.map +1 -1
- package/dist/detector.d.ts +8 -3
- package/dist/detector.d.ts.map +1 -1
- package/dist/detector.js +34 -3
- package/dist/detector.js.map +1 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -10
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/client-calls.js
CHANGED
|
@@ -134,6 +134,491 @@ export function activeClientSymbolNamesIn(config, file) {
|
|
|
134
134
|
}
|
|
135
135
|
return [...names].sort();
|
|
136
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* The one payload property this model follows: the axios/kit response
|
|
139
|
+
* envelope whose `data` member carries the decoded body.
|
|
140
|
+
*/
|
|
141
|
+
const PAYLOAD_PROPERTY = 'data';
|
|
142
|
+
/**
|
|
143
|
+
* Members of `Response`, of the collection prototypes and of `Object`
|
|
144
|
+
* itself. A read of one of these is a JavaScript member, never a
|
|
145
|
+
* response-model field, so it is never collected — without this a
|
|
146
|
+
* `res.data.map(...)` on a list endpoint would read as a missing field.
|
|
147
|
+
*/
|
|
148
|
+
const NEVER_MODEL_FIELDS = {
|
|
149
|
+
at: true, catch: true, concat: true, constructor: true, entries: true, every: true,
|
|
150
|
+
filter: true, find: true, findIndex: true, flat: true, flatMap: true, finally: true,
|
|
151
|
+
forEach: true, get: true, has: true, headers: true, includes: true, indexOf: true,
|
|
152
|
+
join: true, keys: true, length: true, map: true, message: true, name: true, ok: true,
|
|
153
|
+
prototype: true, push: true, reduce: true, shift: true, slice: true, some: true,
|
|
154
|
+
sort: true, status: true, statusText: true, then: true, toJSON: true, toString: true,
|
|
155
|
+
unshift: true, values: true, valueOf: true,
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Members of the response envelope that decide whether the call
|
|
159
|
+
* SUCCEEDED. A branch that opens on one of them of the call's own result
|
|
160
|
+
* is a guard: the error body a failure branch reads is the framework's,
|
|
161
|
+
* never the success model.
|
|
162
|
+
*/
|
|
163
|
+
const ENVELOPE_MEMBERS = {
|
|
164
|
+
ok: true,
|
|
165
|
+
status: true,
|
|
166
|
+
statusText: true,
|
|
167
|
+
};
|
|
168
|
+
/** The `ok` envelope member; its polarity is the whole verdict. */
|
|
169
|
+
const OK_MEMBER = 'ok';
|
|
170
|
+
/** The last status a successful call answers with (2xx/3xx). */
|
|
171
|
+
const SUCCESS_STATUS = 299;
|
|
172
|
+
/** The first status a failing call answers with. */
|
|
173
|
+
const FAILURE_STATUS = 400;
|
|
174
|
+
/** `||` and `??` are the two ways code writes a fallback chain. */
|
|
175
|
+
function chainOperator(kind) {
|
|
176
|
+
return kind === ts.SyntaxKind.BarBarToken || kind === ts.SyntaxKind.QuestionQuestionToken;
|
|
177
|
+
}
|
|
178
|
+
/** The numeric value of a boolean or numeric literal, else null. */
|
|
179
|
+
function literalValue(node) {
|
|
180
|
+
const expression = unwrapExpression(node);
|
|
181
|
+
if (expression.kind === ts.SyntaxKind.TrueKeyword)
|
|
182
|
+
return 1;
|
|
183
|
+
if (expression.kind === ts.SyntaxKind.FalseKeyword)
|
|
184
|
+
return 0;
|
|
185
|
+
if (ts.isNumericLiteral(expression)) {
|
|
186
|
+
const value = Number(expression.text);
|
|
187
|
+
return Number.isFinite(value) ? value : null;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
/** The names a binding pattern binds, in source order. */
|
|
192
|
+
function bindingNames(name) {
|
|
193
|
+
if (ts.isIdentifier(name))
|
|
194
|
+
return [name.text];
|
|
195
|
+
const names = [];
|
|
196
|
+
for (const element of name.elements) {
|
|
197
|
+
if (ts.isOmittedExpression(element) || element.dotDotDotToken !== undefined)
|
|
198
|
+
continue;
|
|
199
|
+
names.push(...bindingNames(element.name));
|
|
200
|
+
}
|
|
201
|
+
return names;
|
|
202
|
+
}
|
|
203
|
+
/** The literal key a binding element or property names, or `''`. */
|
|
204
|
+
function propertyNameText(name) {
|
|
205
|
+
if (name === undefined)
|
|
206
|
+
return '';
|
|
207
|
+
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name))
|
|
208
|
+
return name.text;
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
/** Strips `await`, parentheses and non-null assertions from an expression. */
|
|
212
|
+
function unwrapExpression(node) {
|
|
213
|
+
let current = node;
|
|
214
|
+
for (;;) {
|
|
215
|
+
const parent = current.parent;
|
|
216
|
+
if (parent === undefined)
|
|
217
|
+
return current;
|
|
218
|
+
if (ts.isAwaitExpression(parent) ||
|
|
219
|
+
ts.isParenthesizedExpression(parent) ||
|
|
220
|
+
parent.kind === ts.SyntaxKind.AsExpression ||
|
|
221
|
+
parent.kind === ts.SyntaxKind.NonNullExpression ||
|
|
222
|
+
parent.kind === ts.SyntaxKind.SatisfiesExpression) {
|
|
223
|
+
current = parent;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
return current;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The node ITSELF with its own wrapping peeled: parentheses, `as T`,
|
|
231
|
+
* `!`. {@link unwrapExpression} walks the other way (from an inner node
|
|
232
|
+
* out to the wrapper around it), which is what a value-table lookup
|
|
233
|
+
* needs; the concise arrow body `=> ({ ... })` IS that wrapper, so the
|
|
234
|
+
* request-options model has to look through it.
|
|
235
|
+
*/
|
|
236
|
+
function unwrapOwnExpression(node) {
|
|
237
|
+
let current = node;
|
|
238
|
+
for (;;) {
|
|
239
|
+
if (ts.isParenthesizedExpression(current)) {
|
|
240
|
+
current = current.expression;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (ts.isAsExpression(current) || ts.isNonNullExpression(current)) {
|
|
244
|
+
current = current.expression;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
return current;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* The bounded static read of the response fields one detected call is
|
|
252
|
+
* consumed through (plan 2026-09-25 Phase 4b item 5).
|
|
253
|
+
*
|
|
254
|
+
* The model is deliberately small and file-local, and it never guesses:
|
|
255
|
+
*
|
|
256
|
+
* - the call's own awaited result is followed through `await` and
|
|
257
|
+
* parentheses; `<result>.data` is the payload (axios/kit envelope) and
|
|
258
|
+
* `<result>` alone is the envelope;
|
|
259
|
+
* - a name bound from either of those (`const r = await call`,
|
|
260
|
+
* `const { data: d } = await call`, `const d = (await call).data`) is
|
|
261
|
+
* followed by NAME inside the enclosing function-like (nested
|
|
262
|
+
* function-likes included, so a `useEffect` callback still counts);
|
|
263
|
+
* - every `holder.<field>`, `holder.data.<field>`, `holder.data['<field>']`
|
|
264
|
+
* and `holder['<field>']` is a read, and so is each key of an object
|
|
265
|
+
* destructuring of a holder or of `<holder>.data`;
|
|
266
|
+
* - computed access (`d[key]`), a reassigned (`let`) holder, and a read
|
|
267
|
+
* of a JavaScript member (`data.map`, `data.length`, …) yield nothing;
|
|
268
|
+
* - a branch that opens on the call's OWN envelope (`if (res.ok)`,
|
|
269
|
+
* `if (!res.ok)`, `if (res.status >= 400)`, `res.ok ? … : …`) is read
|
|
270
|
+
* for its POLARITY, and only its FAILURE arm is not collected: the
|
|
271
|
+
* body a failure branch reads is the error envelope, not the success
|
|
272
|
+
* model. An undecidable condition (a compound test, a comparison this
|
|
273
|
+
* pass cannot read) drops BOTH arms. A read AFTER the guard is
|
|
274
|
+
* collected as usual;
|
|
275
|
+
* - a read that is one operand of a `||` / `??` chain carries the chain's
|
|
276
|
+
* index, so the response-model check can judge the whole chain at once
|
|
277
|
+
* instead of flagging a defensive fallback.
|
|
278
|
+
*
|
|
279
|
+
* A read is recorded once per field and location, in source order.
|
|
280
|
+
*/
|
|
281
|
+
function responseReadsOf(file, call, source) {
|
|
282
|
+
const reads = new Map();
|
|
283
|
+
const chainRoots = [];
|
|
284
|
+
// Reads the BINDING pass already saw (an inline `(await call).data.x`).
|
|
285
|
+
// They are recorded first and replayed once `add` knows the holders,
|
|
286
|
+
// which the binding pass is what discovers.
|
|
287
|
+
const bound = [];
|
|
288
|
+
let add = (field, node) => {
|
|
289
|
+
bound.push({ field, node });
|
|
290
|
+
};
|
|
291
|
+
const awaited = unwrapExpression(call);
|
|
292
|
+
const envelopeAccess = awaited.parent !== undefined &&
|
|
293
|
+
ts.isPropertyAccessExpression(awaited.parent) &&
|
|
294
|
+
awaited.parent.expression === awaited &&
|
|
295
|
+
awaited.parent.name.text === PAYLOAD_PROPERTY
|
|
296
|
+
? awaited.parent
|
|
297
|
+
: undefined;
|
|
298
|
+
const payload = envelopeAccess;
|
|
299
|
+
const holders = [];
|
|
300
|
+
bindHolder(awaited, false, holders, add);
|
|
301
|
+
if (payload !== undefined)
|
|
302
|
+
bindHolder(payload, true, holders, add);
|
|
303
|
+
const responseNames = new Set(holders.filter((h) => !h.payload).map((h) => h.name));
|
|
304
|
+
const payloadNames = new Set(holders.filter((h) => h.payload).map((h) => h.name));
|
|
305
|
+
/** Whether an expression IS the decoded payload of this call. */
|
|
306
|
+
const isPayload = (node) => {
|
|
307
|
+
const expression = unwrapExpression(node);
|
|
308
|
+
if (ts.isIdentifier(expression))
|
|
309
|
+
return payloadNames.has(expression.text);
|
|
310
|
+
return (ts.isPropertyAccessExpression(expression) &&
|
|
311
|
+
expression.name.text === PAYLOAD_PROPERTY &&
|
|
312
|
+
ts.isIdentifier(expression.expression) &&
|
|
313
|
+
responseNames.has(expression.expression.text));
|
|
314
|
+
};
|
|
315
|
+
let scope = source;
|
|
316
|
+
for (let node = call; node !== undefined; node = node.parent) {
|
|
317
|
+
if (ts.isFunctionLike(node)) {
|
|
318
|
+
scope = node;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// A branch that opens on the call's OWN envelope (`if (!res.ok)`,
|
|
323
|
+
// `if (res.status >= 400)`, `res.ok ? … : …`) is a guard: which side
|
|
324
|
+
// runs is not statically known, and the error body a failure branch
|
|
325
|
+
// reads is the framework's, not the success model. Nothing inside such a
|
|
326
|
+
// branch is a proven read of the success model — see the failure-path
|
|
327
|
+
// note in the module doc.
|
|
328
|
+
const guarded = new Set();
|
|
329
|
+
const isHolderName = (node) => ts.isIdentifier(node) && responseNames.has(node.text);
|
|
330
|
+
/** The envelope member of this call's own result an expression reads. */
|
|
331
|
+
const envelopeMember = (node) => ts.isPropertyAccessExpression(node) &&
|
|
332
|
+
isHolderName(node.expression) &&
|
|
333
|
+
ENVELOPE_MEMBERS[node.name.text] === true
|
|
334
|
+
? node.name.text
|
|
335
|
+
: null;
|
|
336
|
+
const mentionsEnvelope = (node) => {
|
|
337
|
+
if (envelopeMember(node) !== null)
|
|
338
|
+
return true;
|
|
339
|
+
let found = false;
|
|
340
|
+
const inspect = (at) => {
|
|
341
|
+
if (found)
|
|
342
|
+
return;
|
|
343
|
+
if (envelopeMember(at) !== null) {
|
|
344
|
+
found = true;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
ts.forEachChild(at, inspect);
|
|
348
|
+
};
|
|
349
|
+
ts.forEachChild(node, inspect);
|
|
350
|
+
return found;
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* The polarity of an envelope guard, or null when the condition does not
|
|
354
|
+
* test this call's envelope at all.
|
|
355
|
+
*
|
|
356
|
+
* `if (res.ok)` / `if (res.ok === true)` / `if (res.status >= 400)` /
|
|
357
|
+
* `if (res.status !== 201)` and their negations are decided here, so the
|
|
358
|
+
* SUCCESS arm keeps its reads — the error envelope belongs to the
|
|
359
|
+
* failure arm only. A compound condition, a comparison this cannot
|
|
360
|
+
* read, or a `statusText`/`headers` test is `unknown`: neither arm is a
|
|
361
|
+
* proven read of the success model.
|
|
362
|
+
*/
|
|
363
|
+
const guardPolarity = (condition) => {
|
|
364
|
+
const expression = unwrapExpression(condition);
|
|
365
|
+
if (ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.ExclamationToken) {
|
|
366
|
+
const inner = guardPolarity(expression.operand);
|
|
367
|
+
if (inner === null || inner === 'unknown')
|
|
368
|
+
return inner;
|
|
369
|
+
return inner === 'success' ? 'failure' : 'success';
|
|
370
|
+
}
|
|
371
|
+
const bare = envelopeMember(expression);
|
|
372
|
+
if (bare !== null)
|
|
373
|
+
return bare === OK_MEMBER ? 'success' : 'unknown';
|
|
374
|
+
if (!ts.isBinaryExpression(expression)) {
|
|
375
|
+
return mentionsEnvelope(expression) ? 'unknown' : null;
|
|
376
|
+
}
|
|
377
|
+
const member = envelopeMember(expression.left);
|
|
378
|
+
if (member === null)
|
|
379
|
+
return mentionsEnvelope(expression) ? 'unknown' : null;
|
|
380
|
+
const expected = literalValue(expression.right);
|
|
381
|
+
if (expected === null)
|
|
382
|
+
return 'unknown';
|
|
383
|
+
const isOk = member === OK_MEMBER;
|
|
384
|
+
const kind = expression.operatorToken.kind;
|
|
385
|
+
const equality = kind === ts.SyntaxKind.EqualsEqualsToken || kind === ts.SyntaxKind.EqualsEqualsEqualsToken;
|
|
386
|
+
const inequality = kind === ts.SyntaxKind.ExclamationEqualsToken || kind === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
|
387
|
+
if (isOk) {
|
|
388
|
+
// A boolean compared with a boolean literal: `!==` is the negated
|
|
389
|
+
// equality, so `ok !== true` is the failure arm.
|
|
390
|
+
if (!equality && !inequality)
|
|
391
|
+
return 'unknown';
|
|
392
|
+
const satisfied = (succeeded) => {
|
|
393
|
+
const same = (succeeded ? 1 : 0) === expected;
|
|
394
|
+
return inequality ? !same : same;
|
|
395
|
+
};
|
|
396
|
+
if (satisfied(true) && !satisfied(false))
|
|
397
|
+
return 'success';
|
|
398
|
+
if (!satisfied(true) && satisfied(false))
|
|
399
|
+
return 'failure';
|
|
400
|
+
return 'unknown';
|
|
401
|
+
}
|
|
402
|
+
if (inequality) {
|
|
403
|
+
// `status !== 200` reads as "not the success I expected", which is
|
|
404
|
+
// the failure arm even though a 404 satisfies it too.
|
|
405
|
+
if (expected >= 200 && expected <= SUCCESS_STATUS)
|
|
406
|
+
return 'failure';
|
|
407
|
+
if (expected >= FAILURE_STATUS)
|
|
408
|
+
return 'success';
|
|
409
|
+
return 'unknown';
|
|
410
|
+
}
|
|
411
|
+
const succeeded = expected >= 200 && expected <= SUCCESS_STATUS;
|
|
412
|
+
const failed = expected >= FAILURE_STATUS;
|
|
413
|
+
switch (kind) {
|
|
414
|
+
case ts.SyntaxKind.EqualsEqualsToken:
|
|
415
|
+
case ts.SyntaxKind.EqualsEqualsEqualsToken:
|
|
416
|
+
if (succeeded)
|
|
417
|
+
return 'success';
|
|
418
|
+
if (failed)
|
|
419
|
+
return 'failure';
|
|
420
|
+
return 'unknown';
|
|
421
|
+
case ts.SyntaxKind.LessThanToken:
|
|
422
|
+
return SUCCESS_STATUS < expected ? 'success' : FAILURE_STATUS < expected ? 'failure' : 'unknown';
|
|
423
|
+
case ts.SyntaxKind.LessThanEqualsToken:
|
|
424
|
+
return SUCCESS_STATUS <= expected ? 'success' : FAILURE_STATUS <= expected ? 'failure' : 'unknown';
|
|
425
|
+
case ts.SyntaxKind.GreaterThanToken:
|
|
426
|
+
return SUCCESS_STATUS > expected ? 'success' : FAILURE_STATUS > expected ? 'failure' : 'unknown';
|
|
427
|
+
case ts.SyntaxKind.GreaterThanEqualsToken:
|
|
428
|
+
return SUCCESS_STATUS >= expected ? 'success' : FAILURE_STATUS >= expected ? 'failure' : 'unknown';
|
|
429
|
+
default:
|
|
430
|
+
return 'unknown';
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
const markGuards = (node) => {
|
|
434
|
+
if (ts.isIfStatement(node)) {
|
|
435
|
+
const polarity = guardPolarity(node.expression);
|
|
436
|
+
if (polarity === 'failure')
|
|
437
|
+
guarded.add(node.thenStatement);
|
|
438
|
+
else if (polarity === 'success' && node.elseStatement !== undefined) {
|
|
439
|
+
guarded.add(node.elseStatement);
|
|
440
|
+
}
|
|
441
|
+
else if (polarity === 'unknown') {
|
|
442
|
+
guarded.add(node.thenStatement);
|
|
443
|
+
if (node.elseStatement !== undefined)
|
|
444
|
+
guarded.add(node.elseStatement);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (ts.isConditionalExpression(node)) {
|
|
448
|
+
// A ternary on the envelope is the same guard as an `if`: only its
|
|
449
|
+
// failure arm is the error envelope.
|
|
450
|
+
const polarity = guardPolarity(node.condition);
|
|
451
|
+
if (polarity === 'failure')
|
|
452
|
+
guarded.add(node.whenTrue);
|
|
453
|
+
else if (polarity === 'success')
|
|
454
|
+
guarded.add(node.whenFalse);
|
|
455
|
+
else if (polarity === 'unknown') {
|
|
456
|
+
guarded.add(node.whenTrue);
|
|
457
|
+
guarded.add(node.whenFalse);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
ts.forEachChild(node, markGuards);
|
|
461
|
+
};
|
|
462
|
+
markGuards(scope);
|
|
463
|
+
/** Whether a node sits inside one of the guarded branch bodies. */
|
|
464
|
+
const insideGuard = (node) => {
|
|
465
|
+
for (let at = node; at !== undefined && at !== scope; at = at.parent) {
|
|
466
|
+
if (guarded.has(at))
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
return false;
|
|
470
|
+
};
|
|
471
|
+
/** The outermost `||` / `??` chain an operand belongs to, if any. */
|
|
472
|
+
const chainOf = (node) => {
|
|
473
|
+
for (let at = node; at !== undefined && at !== scope; at = at.parent) {
|
|
474
|
+
if (ts.isBinaryExpression(at) && chainOperator(at.operatorToken.kind))
|
|
475
|
+
return at;
|
|
476
|
+
}
|
|
477
|
+
return undefined;
|
|
478
|
+
};
|
|
479
|
+
add = (field, node) => {
|
|
480
|
+
if (field.length === 0 || NEVER_MODEL_FIELDS[field] === true)
|
|
481
|
+
return;
|
|
482
|
+
if (insideGuard(node))
|
|
483
|
+
return;
|
|
484
|
+
const location = locationOf(file, source, node);
|
|
485
|
+
const key = `${field}@${String(location.line)}:${String(location.col)}`;
|
|
486
|
+
if (reads.has(key))
|
|
487
|
+
return;
|
|
488
|
+
const root = chainOf(node);
|
|
489
|
+
if (root === undefined) {
|
|
490
|
+
reads.set(key, { field, location });
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
let index = chainRoots.indexOf(root);
|
|
494
|
+
if (index < 0) {
|
|
495
|
+
chainRoots.push(root);
|
|
496
|
+
index = chainRoots.length - 1;
|
|
497
|
+
}
|
|
498
|
+
reads.set(key, { field, location, chain: index });
|
|
499
|
+
};
|
|
500
|
+
for (const read of bound)
|
|
501
|
+
add(read.field, read.node);
|
|
502
|
+
const visit = (node) => {
|
|
503
|
+
if (ts.isPropertyAccessExpression(node) && isPayload(node.expression)) {
|
|
504
|
+
add(node.name.text, node);
|
|
505
|
+
}
|
|
506
|
+
else if (ts.isElementAccessExpression(node) &&
|
|
507
|
+
ts.isStringLiteralLike(node.argumentExpression) &&
|
|
508
|
+
isPayload(node.expression)) {
|
|
509
|
+
add(node.argumentExpression.text, node);
|
|
510
|
+
}
|
|
511
|
+
else if (ts.isVariableDeclaration(node) &&
|
|
512
|
+
node.initializer !== undefined &&
|
|
513
|
+
ts.isObjectBindingPattern(node.name) &&
|
|
514
|
+
isPayload(node.initializer)) {
|
|
515
|
+
for (const element of node.name.elements) {
|
|
516
|
+
const field = bindingKey(element);
|
|
517
|
+
if (field.length > 0)
|
|
518
|
+
add(field, element);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
ts.forEachChild(node, visit);
|
|
522
|
+
};
|
|
523
|
+
visit(scope);
|
|
524
|
+
const ordered = [...reads.values()].sort((left, right) => {
|
|
525
|
+
const where = (left.location.file < right.location.file ? -1 : left.location.file > right.location.file ? 1 : 0) ||
|
|
526
|
+
left.location.line - right.location.line ||
|
|
527
|
+
left.location.col - right.location.col ||
|
|
528
|
+
(left.field < right.field ? -1 : left.field > right.field ? 1 : 0);
|
|
529
|
+
return where;
|
|
530
|
+
});
|
|
531
|
+
// Chain numbers are handed out in discovery order; renumber them by
|
|
532
|
+
// where each chain FIRST appears in source, so the fact is independent
|
|
533
|
+
// of the traversal.
|
|
534
|
+
const firstOf = (index) => ordered.find((read) => read.chain === index);
|
|
535
|
+
const chains = [...new Set(ordered.map((read) => read.chain))]
|
|
536
|
+
.filter((index) => index !== undefined)
|
|
537
|
+
.sort((left, right) => firstOf(left).location.line - firstOf(right).location.line ||
|
|
538
|
+
firstOf(left).location.col - firstOf(right).location.col);
|
|
539
|
+
const renumbered = new Map(chains.map((index, position) => [index, position]));
|
|
540
|
+
return ordered.map((read) => read.chain === undefined ? read : { ...read, chain: renumbered.get(read.chain) ?? read.chain });
|
|
541
|
+
/** The key a binding element pulls off the object it destructures. */
|
|
542
|
+
function bindingKey(element) {
|
|
543
|
+
const declared = propertyNameText(element.propertyName);
|
|
544
|
+
if (declared.length > 0)
|
|
545
|
+
return declared;
|
|
546
|
+
return ts.isIdentifier(element.name) ? element.name.text : '';
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Records what the awaited result (or its `.data` payload) binds to:
|
|
550
|
+
* a name to follow, or — for a destructured payload — the field keys
|
|
551
|
+
* that destructuring itself reads. `status`/`headers` and every other
|
|
552
|
+
* envelope member are not response-model fields, so they are ignored.
|
|
553
|
+
*/
|
|
554
|
+
function bindHolder(node, isPayloadNode, into, addRead) {
|
|
555
|
+
const parent = node.parent;
|
|
556
|
+
if (parent === undefined)
|
|
557
|
+
return;
|
|
558
|
+
if (ts.isVariableDeclaration(parent) && parent.initializer === node) {
|
|
559
|
+
// A `let` holder can be reassigned before the read, so the name no
|
|
560
|
+
// longer provably holds this call's response; a `const` one does.
|
|
561
|
+
const declarationList = parent.parent;
|
|
562
|
+
const isConst = ts.isVariableDeclarationList(declarationList) &&
|
|
563
|
+
(ts.getCombinedNodeFlags(declarationList) & ts.NodeFlags.Const) !== 0;
|
|
564
|
+
if (!isConst)
|
|
565
|
+
return;
|
|
566
|
+
if (ts.isIdentifier(parent.name)) {
|
|
567
|
+
into.push({ name: parent.name.text, payload: isPayloadNode });
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
// An array destructuring pulls ELEMENTS, not named fields, out of a
|
|
571
|
+
// response: there is no field name to compare against a model.
|
|
572
|
+
if (!ts.isObjectBindingPattern(parent.name))
|
|
573
|
+
return;
|
|
574
|
+
// `const { data: d } = await call` — the payload is one level in;
|
|
575
|
+
// every other envelope key is not a response-model field.
|
|
576
|
+
if (!isPayloadNode) {
|
|
577
|
+
for (const element of parent.name.elements) {
|
|
578
|
+
if (bindingKey(element) !== PAYLOAD_PROPERTY)
|
|
579
|
+
continue;
|
|
580
|
+
for (const name of bindingNames(element.name))
|
|
581
|
+
into.push({ name, payload: true });
|
|
582
|
+
}
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
for (const element of parent.name.elements) {
|
|
586
|
+
const field = bindingKey(element);
|
|
587
|
+
if (field.length > 0)
|
|
588
|
+
addRead(field, element);
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (ts.isBindingElement(parent) && parent.initializer === node) {
|
|
593
|
+
if (!isPayloadNode) {
|
|
594
|
+
if (bindingKey(parent) === PAYLOAD_PROPERTY && ts.isObjectBindingPattern(parent.name)) {
|
|
595
|
+
for (const name of bindingNames(parent.name))
|
|
596
|
+
into.push({ name, payload: true });
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (ts.isObjectBindingPattern(parent.name)) {
|
|
601
|
+
// `const { data: { dueDate } } = await call` — the nested keys
|
|
602
|
+
// are the fields the body is read through.
|
|
603
|
+
for (const element of parent.name.elements) {
|
|
604
|
+
const field = bindingKey(element);
|
|
605
|
+
if (field.length > 0)
|
|
606
|
+
addRead(field, element);
|
|
607
|
+
}
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const key = bindingKey(parent);
|
|
611
|
+
if (key.length > 0)
|
|
612
|
+
addRead(key, parent);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (ts.isPropertyAccessExpression(parent) && parent.expression === node) {
|
|
616
|
+
// `(await call).data.<field>` reads one field of the body inline.
|
|
617
|
+
if (isPayloadNode)
|
|
618
|
+
addRead(parent.name.text, parent);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
137
622
|
const VERB_METHODS = new Map([
|
|
138
623
|
['get', 'GET'],
|
|
139
624
|
['post', 'POST'],
|
|
@@ -143,6 +628,14 @@ const VERB_METHODS = new Map([
|
|
|
143
628
|
['head', 'HEAD'],
|
|
144
629
|
['options', 'OPTIONS'],
|
|
145
630
|
]);
|
|
631
|
+
const ABSENT_OPTIONS = { kind: 'absent' };
|
|
632
|
+
const UNPROVEN_OPTIONS = { kind: 'unproven' };
|
|
633
|
+
/**
|
|
634
|
+
* Bound hops of the request-options model (an options object, a constant
|
|
635
|
+
* naming one, or a single-return function that forwards its argument), so
|
|
636
|
+
* a chain of wrappers is reported, never followed forever.
|
|
637
|
+
*/
|
|
638
|
+
const MAX_OPTIONS_HOPS = 4;
|
|
146
639
|
const UNRESOLVED_VALUE = { kind: 'unresolved', text: '' };
|
|
147
640
|
function languageKindFor(file) {
|
|
148
641
|
if (file.endsWith('.tsx'))
|
|
@@ -162,6 +655,7 @@ function modelFile(source, config, file) {
|
|
|
162
655
|
const constants = new Map();
|
|
163
656
|
const clientFunctions = new Set();
|
|
164
657
|
const wrappers = new Map();
|
|
658
|
+
const functions = new Map();
|
|
165
659
|
const isModuleScope = (node) => {
|
|
166
660
|
let current = node.parent;
|
|
167
661
|
while (current !== undefined) {
|
|
@@ -186,23 +680,26 @@ function modelFile(source, config, file) {
|
|
|
186
680
|
// One-declaration wrapper: `const apiGet = (path) => fetch(...)`.
|
|
187
681
|
const init = declaration.initializer;
|
|
188
682
|
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
|
|
683
|
+
functions.set(declaration.name.text, init);
|
|
189
684
|
modelWrapper(declaration.name.text, init, wrappers);
|
|
190
685
|
if (containsClientCall(init, config, file))
|
|
191
686
|
clientFunctions.add(declaration.name.text);
|
|
192
687
|
}
|
|
193
688
|
}
|
|
194
689
|
}
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
//
|
|
198
|
-
if (
|
|
690
|
+
if (ts.isFunctionDeclaration(node) && node.name !== undefined) {
|
|
691
|
+
// Named function declarations can be client wrappers too, and the
|
|
692
|
+
// request-options model reads their single returned expression.
|
|
693
|
+
if (isModuleScope(node))
|
|
694
|
+
functions.set(node.name.text, node);
|
|
695
|
+
if (node.body !== undefined && containsClientCall(node, config, file)) {
|
|
199
696
|
clientFunctions.add(node.name.text);
|
|
200
697
|
}
|
|
201
698
|
}
|
|
202
699
|
ts.forEachChild(node, visit);
|
|
203
700
|
};
|
|
204
701
|
visit(source);
|
|
205
|
-
return { source, constants, clientFunctions, wrappers };
|
|
702
|
+
return { source, constants, clientFunctions, functions, wrappers };
|
|
206
703
|
}
|
|
207
704
|
/** True when the subtree contains a direct fetch/axios/client call. */
|
|
208
705
|
function containsClientCall(node, config, file) {
|
|
@@ -323,10 +820,14 @@ function extractCall(call, file, config, table, model, calls, unresolved) {
|
|
|
323
820
|
if (source === undefined)
|
|
324
821
|
return;
|
|
325
822
|
const location = locationOf(file, source, call);
|
|
823
|
+
// The response fields THIS call site reads (plan 2026-09-25 Phase 4b
|
|
824
|
+
// item 5) — the same bounded, file-local pass, attached to whichever
|
|
825
|
+
// call shape below is recognized.
|
|
826
|
+
const reads = responseReadsOf(file, call, source);
|
|
326
827
|
const expression = call.expression;
|
|
327
828
|
// fetch(url[, {method}])
|
|
328
829
|
if (ts.isIdentifier(expression) && expression.text === 'fetch') {
|
|
329
|
-
extractClientCall(call, 'fetch', 'GET', file, config, table, calls, unresolved, location);
|
|
830
|
+
extractClientCall(call, 'fetch', 'GET', file, config, table, calls, unresolved, location, undefined, reads);
|
|
330
831
|
return;
|
|
331
832
|
}
|
|
332
833
|
// axios.get(url), apiClient.post(url), window.fetch(url)
|
|
@@ -335,7 +836,7 @@ function extractCall(call, file, config, table, model, calls, unresolved) {
|
|
|
335
836
|
const verb = expression.name.text.toLowerCase();
|
|
336
837
|
const isFetchObject = objectName === 'window' && expression.name.text === 'fetch';
|
|
337
838
|
if (isFetchObject) {
|
|
338
|
-
extractClientCall(call, 'fetch', 'GET', file, config, table, calls, unresolved, location);
|
|
839
|
+
extractClientCall(call, 'fetch', 'GET', file, config, table, calls, unresolved, location, undefined, reads);
|
|
339
840
|
return;
|
|
340
841
|
}
|
|
341
842
|
// Per-symbol scoping (phase 3): the symbol counts only where its
|
|
@@ -347,7 +848,7 @@ function extractCall(call, file, config, table, model, calls, unresolved) {
|
|
|
347
848
|
// proven literal baseURL joins into the emitted path (fetch has no
|
|
348
849
|
// instance and never joins).
|
|
349
850
|
const baseURL = table.instanceBaseURL(objectName, file);
|
|
350
|
-
extractClientCall(call, objectName, method, file, config, table, calls, unresolved, location, baseURL);
|
|
851
|
+
extractClientCall(call, objectName, method, file, config, table, calls, unresolved, location, baseURL, reads);
|
|
351
852
|
return;
|
|
352
853
|
}
|
|
353
854
|
}
|
|
@@ -368,7 +869,7 @@ function extractCall(call, file, config, table, model, calls, unresolved) {
|
|
|
368
869
|
}
|
|
369
870
|
if (configCall !== null && framework !== null) {
|
|
370
871
|
const baseURL = table.instanceBaseURL(framework, file);
|
|
371
|
-
extractConfiguredCall(configCall, framework, file, config, table, calls, unresolved, location, baseURL);
|
|
872
|
+
extractConfiguredCall(configCall, framework, file, config, table, calls, unresolved, location, baseURL, reads);
|
|
372
873
|
return;
|
|
373
874
|
}
|
|
374
875
|
// Configured wrapper: the declaration must exist in the scanned set as
|
|
@@ -408,7 +909,7 @@ function extractCall(call, file, config, table, model, calls, unresolved) {
|
|
|
408
909
|
});
|
|
409
910
|
return;
|
|
410
911
|
}
|
|
411
|
-
resolveAndRecord(urlNode, wrapperConfig.name, wrapperConfig.method, file, config, bound, calls, unresolved, location);
|
|
912
|
+
resolveAndRecord(urlNode, wrapperConfig.name, wrapperConfig.method, file, config, bound, calls, unresolved, location, reads);
|
|
412
913
|
return;
|
|
413
914
|
}
|
|
414
915
|
// A module-scope function whose body issues client calls IS a client
|
|
@@ -467,8 +968,16 @@ class BoundTable {
|
|
|
467
968
|
}
|
|
468
969
|
return this.inner.evaluate(node, file);
|
|
469
970
|
}
|
|
971
|
+
optionsMethod(node, file, bindings, depth = 0) {
|
|
972
|
+
if (ts.isIdentifier(node) &&
|
|
973
|
+
node.text === this.parameter &&
|
|
974
|
+
this.argument !== undefined) {
|
|
975
|
+
return this.inner.optionsMethod(this.argument, this.argumentFile, bindings, depth + 1);
|
|
976
|
+
}
|
|
977
|
+
return this.inner.optionsMethod(node, file, bindings, depth);
|
|
978
|
+
}
|
|
470
979
|
}
|
|
471
|
-
function extractClientCall(call, framework, defaultMethod, file, config, table, calls, unresolved, location, baseURL) {
|
|
980
|
+
function extractClientCall(call, framework, defaultMethod, file, config, table, calls, unresolved, location, baseURL, reads = []) {
|
|
472
981
|
const urlNode = call.arguments[0];
|
|
473
982
|
if (urlNode === undefined) {
|
|
474
983
|
unresolved.push({
|
|
@@ -513,11 +1022,31 @@ function extractClientCall(call, framework, defaultMethod, file, config, table,
|
|
|
513
1022
|
}
|
|
514
1023
|
}
|
|
515
1024
|
}
|
|
1025
|
+
if (framework === 'fetch' &&
|
|
1026
|
+
optionsNode !== undefined &&
|
|
1027
|
+
!ts.isObjectLiteralExpression(optionsNode)) {
|
|
1028
|
+
// `fetch(url, options)` with options built elsewhere: the verb is
|
|
1029
|
+
// whatever that object carries, and until the bounded options model
|
|
1030
|
+
// proves it, assuming GET invents a route the server never serves.
|
|
1031
|
+
const resolved = table.optionsMethod(optionsNode, file);
|
|
1032
|
+
if (resolved.kind === 'unproven') {
|
|
1033
|
+
unresolved.push({
|
|
1034
|
+
code: HTTP_METHOD_DYNAMIC,
|
|
1035
|
+
detail: 'fetch call passes request options that are not a provable object literal ' +
|
|
1036
|
+
'(they are built by another function or a computed expression); the method ' +
|
|
1037
|
+
'cannot be proven statically and is never assumed to be GET',
|
|
1038
|
+
location,
|
|
1039
|
+
});
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
if (resolved.kind === 'proven')
|
|
1043
|
+
method = resolved.method;
|
|
1044
|
+
}
|
|
516
1045
|
if (method === null)
|
|
517
1046
|
return;
|
|
518
|
-
resolveAndRecord(urlNode, framework, method, file, config, table, calls, unresolved, location, baseURL);
|
|
1047
|
+
resolveAndRecord(urlNode, framework, method, file, config, table, calls, unresolved, location, reads, baseURL);
|
|
519
1048
|
}
|
|
520
|
-
function extractConfiguredCall(call, framework, file, config, table, calls, unresolved, location, baseURL) {
|
|
1049
|
+
function extractConfiguredCall(call, framework, file, config, table, calls, unresolved, location, baseURL, reads = []) {
|
|
521
1050
|
const configNode = call.arguments[0];
|
|
522
1051
|
if (configNode === undefined || !ts.isObjectLiteralExpression(configNode)) {
|
|
523
1052
|
unresolved.push({
|
|
@@ -564,7 +1093,7 @@ function extractConfiguredCall(call, framework, file, config, table, calls, unre
|
|
|
564
1093
|
});
|
|
565
1094
|
return;
|
|
566
1095
|
}
|
|
567
|
-
resolveAndRecord(urlNode, framework, method, file, config, table, calls, unresolved, location, baseURL);
|
|
1096
|
+
resolveAndRecord(urlNode, framework, method, file, config, table, calls, unresolved, location, reads, baseURL);
|
|
568
1097
|
}
|
|
569
1098
|
/**
|
|
570
1099
|
* axios `isAbsoluteURL`: a call URL beginning `<scheme>://` or a
|
|
@@ -596,7 +1125,7 @@ function joinInstanceBaseURL(baseURL, callPath) {
|
|
|
596
1125
|
joined: baseURL,
|
|
597
1126
|
};
|
|
598
1127
|
}
|
|
599
|
-
function resolveAndRecord(urlNode, framework, method, file, config, table, calls, unresolved, location, baseURL) {
|
|
1128
|
+
function resolveAndRecord(urlNode, framework, method, file, config, table, calls, unresolved, location, reads = [], baseURL) {
|
|
600
1129
|
const resolved = table.evaluate(urlNode, file);
|
|
601
1130
|
if (resolved.kind === 'unresolved') {
|
|
602
1131
|
unresolved.push({
|
|
@@ -623,8 +1152,28 @@ function resolveAndRecord(urlNode, framework, method, file, config, table, calls
|
|
|
623
1152
|
...(joined.joined !== undefined ? { joinedBaseURL: joined.joined } : {}),
|
|
624
1153
|
framework,
|
|
625
1154
|
location,
|
|
1155
|
+
...(reads.length > 0 ? { responseReads: reads } : {}),
|
|
626
1156
|
});
|
|
627
1157
|
}
|
|
1158
|
+
/**
|
|
1159
|
+
* The ONE expression a function-like returns, or undefined when it
|
|
1160
|
+
* returns none or branches: two return paths can set the verb
|
|
1161
|
+
* differently, and an unprovable options object must not be guessed.
|
|
1162
|
+
*/
|
|
1163
|
+
function returnedExpression(fn) {
|
|
1164
|
+
// A concise arrow body IS its returned expression: `(o) => ({ ...o })`.
|
|
1165
|
+
if (ts.isArrowFunction(fn) && !ts.isBlock(fn.body))
|
|
1166
|
+
return fn.body;
|
|
1167
|
+
const body = ts.isArrowFunction(fn)
|
|
1168
|
+
? ts.isBlock(fn.body)
|
|
1169
|
+
? fn.body
|
|
1170
|
+
: undefined
|
|
1171
|
+
: fn.body;
|
|
1172
|
+
if (body === undefined)
|
|
1173
|
+
return undefined;
|
|
1174
|
+
const returns = body.statements.filter((statement) => ts.isReturnStatement(statement));
|
|
1175
|
+
return returns.length === 1 ? returns[0]?.expression : undefined;
|
|
1176
|
+
}
|
|
628
1177
|
function normalizeHttpMethodValue(raw) {
|
|
629
1178
|
const upper = raw.trim().toUpperCase();
|
|
630
1179
|
if (upper === 'GET' || upper === 'HEAD' || upper === 'POST' || upper === 'PUT' || upper === 'PATCH' || upper === 'DELETE' || upper === 'OPTIONS') {
|
|
@@ -940,6 +1489,106 @@ class ValueTable {
|
|
|
940
1489
|
return UNRESOLVED_VALUE;
|
|
941
1490
|
return this.evaluate(initializer, targetFile);
|
|
942
1491
|
}
|
|
1492
|
+
/**
|
|
1493
|
+
* The verb one request-options expression provably carries.
|
|
1494
|
+
*
|
|
1495
|
+
* Bounded and deterministic, three shapes only:
|
|
1496
|
+
* - an inline object literal: `method` is read with JavaScript's
|
|
1497
|
+
* last-writer-wins order, and a spread of another resolvable options
|
|
1498
|
+
* expression participates equally (`{...base, method:'POST'}`);
|
|
1499
|
+
* - a name: a module-scope constant in the scanned set (same file or
|
|
1500
|
+
* relative import), followed at most {@link MAX_OPTIONS_HOPS} hops;
|
|
1501
|
+
* - a call to a plain-named function whose declaration is in the
|
|
1502
|
+
* scanned set and whose body is a SINGLE `return <expression>`:
|
|
1503
|
+
* the first parameter binds to this call's first argument, so a
|
|
1504
|
+
* forwarder (`(options) => ({ ...options })`) resolves to whatever
|
|
1505
|
+
* its argument proves.
|
|
1506
|
+
* Everything else — a computed member, an attribute, a member call, a
|
|
1507
|
+
* function this scan cannot see — is `unproven`, and the caller turns
|
|
1508
|
+
* that into a typed `HTTP_METHOD_DYNAMIC` entry instead of a GET.
|
|
1509
|
+
*/
|
|
1510
|
+
optionsMethod(node, file, bindings = new Map(), depth = 0) {
|
|
1511
|
+
if (depth > MAX_OPTIONS_HOPS)
|
|
1512
|
+
return UNPROVEN_OPTIONS;
|
|
1513
|
+
const expression = unwrapOwnExpression(node);
|
|
1514
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
1515
|
+
return this.optionsMethodOfObject(expression, file, bindings, depth);
|
|
1516
|
+
}
|
|
1517
|
+
if (ts.isIdentifier(expression)) {
|
|
1518
|
+
const bound = bindings.get(expression.text);
|
|
1519
|
+
if (bound !== undefined)
|
|
1520
|
+
return this.optionsMethod(bound[0], bound[1], bindings, depth + 1);
|
|
1521
|
+
const constant = this.resolveConstantInitializer(expression.text, file, new Set());
|
|
1522
|
+
if (constant === undefined)
|
|
1523
|
+
return UNPROVEN_OPTIONS;
|
|
1524
|
+
return this.optionsMethod(constant[0], constant[1], bindings, depth + 1);
|
|
1525
|
+
}
|
|
1526
|
+
if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) {
|
|
1527
|
+
return this.optionsMethodThroughCall(expression, file, bindings, depth);
|
|
1528
|
+
}
|
|
1529
|
+
return UNPROVEN_OPTIONS;
|
|
1530
|
+
}
|
|
1531
|
+
/** The verb one options object literal carries, spreads included. */
|
|
1532
|
+
optionsMethodOfObject(object, file, bindings, depth) {
|
|
1533
|
+
let result = ABSENT_OPTIONS;
|
|
1534
|
+
for (const property of object.properties) {
|
|
1535
|
+
if (ts.isSpreadAssignment(property)) {
|
|
1536
|
+
result = this.optionsMethod(property.expression, file, bindings, depth + 1);
|
|
1537
|
+
}
|
|
1538
|
+
else if ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
|
|
1539
|
+
ts.isIdentifier(property.name) &&
|
|
1540
|
+
property.name.text === 'method') {
|
|
1541
|
+
const initializer = ts.isPropertyAssignment(property) ? property.initializer : property.name;
|
|
1542
|
+
result = this.verbOfLiteral(initializer, file, bindings);
|
|
1543
|
+
}
|
|
1544
|
+
else {
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (result.kind === 'unproven')
|
|
1548
|
+
return UNPROVEN_OPTIONS;
|
|
1549
|
+
}
|
|
1550
|
+
return result;
|
|
1551
|
+
}
|
|
1552
|
+
/** A `method:` member whose value provably is one concrete verb. */
|
|
1553
|
+
verbOfLiteral(node, file, bindings) {
|
|
1554
|
+
const bound = ts.isIdentifier(node) ? bindings.get(node.text) : undefined;
|
|
1555
|
+
const resolved = bound === undefined ? this.evaluate(node, file) : this.evaluate(bound[0], bound[1]);
|
|
1556
|
+
if (resolved.kind !== 'literal')
|
|
1557
|
+
return UNPROVEN_OPTIONS;
|
|
1558
|
+
const verb = normalizeHttpMethodValue(resolved.text);
|
|
1559
|
+
return verb === null ? UNPROVEN_OPTIONS : { kind: 'proven', method: verb };
|
|
1560
|
+
}
|
|
1561
|
+
/** Options returned by a single-return function declared in the scan. */
|
|
1562
|
+
optionsMethodThroughCall(call, file, bindings, depth) {
|
|
1563
|
+
const name = ts.isIdentifier(call.expression) ? call.expression.text : '';
|
|
1564
|
+
if (name.length === 0)
|
|
1565
|
+
return UNPROVEN_OPTIONS;
|
|
1566
|
+
const declared = this.functionOf(name, file);
|
|
1567
|
+
if (declared === undefined)
|
|
1568
|
+
return UNPROVEN_OPTIONS;
|
|
1569
|
+
const returned = returnedExpression(declared);
|
|
1570
|
+
if (returned === undefined)
|
|
1571
|
+
return UNPROVEN_OPTIONS;
|
|
1572
|
+
const next = new Map(bindings);
|
|
1573
|
+
const parameter = declared.parameters[0];
|
|
1574
|
+
const argument = call.arguments[0];
|
|
1575
|
+
if (parameter !== undefined &&
|
|
1576
|
+
argument !== undefined &&
|
|
1577
|
+
ts.isIdentifier(parameter.name)) {
|
|
1578
|
+
next.set(parameter.name.text, [argument, file]);
|
|
1579
|
+
}
|
|
1580
|
+
return this.optionsMethod(returned, file, next, depth + 1);
|
|
1581
|
+
}
|
|
1582
|
+
/** A module-scope function declaration, same file or relative import. */
|
|
1583
|
+
functionOf(name, file) {
|
|
1584
|
+
const own = this.modelOf(file)?.functions.get(name);
|
|
1585
|
+
if (own !== undefined)
|
|
1586
|
+
return own;
|
|
1587
|
+
const imported = this.importedFrom(file, name);
|
|
1588
|
+
if (imported === null)
|
|
1589
|
+
return undefined;
|
|
1590
|
+
return this.modelOf(imported)?.functions.get(name);
|
|
1591
|
+
}
|
|
943
1592
|
resolveSpecifier(fromFile, specifier) {
|
|
944
1593
|
const base = posix.dirname(fromFile.split('\\').join('/'));
|
|
945
1594
|
const joined = posix.normalize(posix.join(base, specifier));
|