carrick 0.3.106 → 0.3.107
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/hook/refresh.js +8 -1
- package/dist/hook/refresh.js.map +1 -1
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/capture/anchors.d.ts +15 -2
- package/sidecar/dist/src/capture/anchors.js +28 -11
- package/sidecar/dist/src/capture/check-poison.js +4 -14
- package/sidecar/dist/src/capture/index.js +54 -36
- package/sidecar/dist/src/capture/installed-package.d.ts +2 -0
- package/sidecar/dist/src/capture/installed-package.js +2 -1
- package/sidecar/dist/src/capture/outside-root.d.ts +19 -0
- package/sidecar/dist/src/capture/outside-root.js +39 -2
- package/sidecar/dist/src/capture/self-check.js +3 -13
- package/sidecar/dist/src/capture/specifiers.d.ts +14 -0
- package/sidecar/dist/src/capture/specifiers.js +25 -0
- package/sidecar/dist/src/function-line-index.d.ts +30 -0
- package/sidecar/dist/src/function-line-index.js +162 -0
- package/sidecar/dist/src/index.d.ts +5 -0
- package/sidecar/dist/src/index.js +29 -5
- package/sidecar/dist/src/line-index.d.ts +9 -0
- package/sidecar/dist/src/line-index.js +26 -0
- package/sidecar/dist/src/progress.d.ts +22 -0
- package/sidecar/dist/src/progress.js +31 -0
- package/sidecar/dist/src/retype.js +1 -19
- package/sidecar/dist/src/type-inferrer.d.ts +92 -11
- package/sidecar/dist/src/type-inferrer.js +356 -149
- package/sidecar/dist/src/types.d.ts +13 -6
|
@@ -23,6 +23,7 @@ import { validateInferRequestItem } from './validators.js';
|
|
|
23
23
|
import { notePrintedType, PrintedTypes } from './printed-names.js';
|
|
24
24
|
import { externalImportsOf, isExternalOrigin } from './origin.js';
|
|
25
25
|
import { reachedOnlyOnFailure } from './failure-path.js';
|
|
26
|
+
import { functionAtLine } from './function-line-index.js';
|
|
26
27
|
import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
|
|
27
28
|
import { expandTypeStructural, } from './type-structural-expander.js';
|
|
28
29
|
/**
|
|
@@ -255,13 +256,17 @@ function isBareStringText(text) {
|
|
|
255
256
|
/**
|
|
256
257
|
* How long the unwidened reading of one batch may take by default. The
|
|
257
258
|
* reading adds a field and never an answer, so running out costs only the
|
|
258
|
-
* readings not yet made; the rewrite is always undone.
|
|
259
|
+
* readings not yet made; the rewrite is always undone. It reports no
|
|
260
|
+
* progress, so this is also the longest a batch goes silent after its last
|
|
261
|
+
* request: well inside the 900s the scanner allows between two signs of life.
|
|
259
262
|
*/
|
|
260
263
|
const UNWIDENED_BUDGET_MS = 120_000;
|
|
261
264
|
/**
|
|
262
|
-
* No reading starts or continues past this long after the batch began
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
+
* No reading starts or continues past this long after the batch began. The
|
|
266
|
+
* number dates from when the scanner allowed a whole request 900s; it now
|
|
267
|
+
* allows that long between progress reports (carrick#1914), so a batch can
|
|
268
|
+
* run past this and still be read. A batch that does publishes no unwidened
|
|
269
|
+
* reading, as before.
|
|
265
270
|
*/
|
|
266
271
|
const UNWIDENED_LATEST_MS = 600_000;
|
|
267
272
|
/**
|
|
@@ -306,24 +311,25 @@ export class TypeInferrer {
|
|
|
306
311
|
*
|
|
307
312
|
* @param requests - Array of inference requests
|
|
308
313
|
* @param extractionConfig - Agent-generated extraction config for payload unwrapping
|
|
314
|
+
* @param onRequestDone - Called once per request, as the batch is done with it
|
|
309
315
|
* @returns InferResult with inferred types or errors
|
|
310
316
|
*/
|
|
311
|
-
infer(requests, extractionConfig) {
|
|
317
|
+
infer(requests, extractionConfig, onRequestDone) {
|
|
312
318
|
const started = performance.now();
|
|
313
319
|
const inferredTypes = [];
|
|
314
320
|
const errors = [];
|
|
315
321
|
/** Response inferences the unwidened reading re-reads (carrick#1516). */
|
|
316
322
|
const responses = [];
|
|
317
323
|
for (const request of requests) {
|
|
318
|
-
// Plain JavaScript has no type annotations to extract, and `checkJs` is
|
|
319
|
-
// off, so inferring against a `.js` file yields nothing useful — it only
|
|
320
|
-
// crashes deep in the compiler API on undefined symbols (`escapedName`,
|
|
321
|
-
// `flags`) and floods the log with the resulting error strings. Skip it.
|
|
322
|
-
// `allowJs` stays on so `.ts` files can still resolve `.js` imports.
|
|
323
|
-
if (/\.(js|jsx|mjs|cjs)$/i.test(request.file_path)) {
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
324
|
try {
|
|
325
|
+
// Plain JavaScript has no type annotations to extract, and `checkJs` is
|
|
326
|
+
// off, so inferring against a `.js` file yields nothing useful — it only
|
|
327
|
+
// crashes deep in the compiler API on undefined symbols (`escapedName`,
|
|
328
|
+
// `flags`) and floods the log with the resulting error strings. Skip it.
|
|
329
|
+
// `allowJs` stays on so `.ts` files can still resolve `.js` imports.
|
|
330
|
+
if (/\.(js|jsx|mjs|cjs)$/i.test(request.file_path)) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
327
333
|
const loc = this.formatRequestLocation(request);
|
|
328
334
|
const itemError = validateInferRequestItem(request);
|
|
329
335
|
if (itemError) {
|
|
@@ -352,6 +358,9 @@ export class TypeInferrer {
|
|
|
352
358
|
const loc = this.formatRequestLocation(request);
|
|
353
359
|
errors.push(`Error inferring type at ${request.file_path}:${loc}: ${error}`);
|
|
354
360
|
}
|
|
361
|
+
finally {
|
|
362
|
+
onRequestDone?.();
|
|
363
|
+
}
|
|
355
364
|
}
|
|
356
365
|
try {
|
|
357
366
|
const deadline = Math.min(performance.now() + this.unwidenedBudgetMs, started + UNWIDENED_LATEST_MS);
|
|
@@ -771,7 +780,7 @@ export class TypeInferrer {
|
|
|
771
780
|
return null;
|
|
772
781
|
}
|
|
773
782
|
const anchor = this.unwrapArrayLevels(awaitedType);
|
|
774
|
-
const inferred = this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(func), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth);
|
|
783
|
+
const inferred = this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(func), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth, this.primaryTypeSymbolSource(anchor.element));
|
|
775
784
|
if (provenance && provenance.length > 0) {
|
|
776
785
|
inferred.any_provenance = provenance;
|
|
777
786
|
}
|
|
@@ -1013,7 +1022,7 @@ export class TypeInferrer {
|
|
|
1013
1022
|
typeString = this.expandResolvedTypeStructural(resolved, typeString, this.wireFormatFor(request));
|
|
1014
1023
|
}
|
|
1015
1024
|
const anchor = this.unwrapArrayLevels(this.unwrapPromiseType(payloadType));
|
|
1016
|
-
return this.createInferredType(request, typeString, false, this.getNodeLocation(payloadNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth);
|
|
1025
|
+
return this.createInferredType(request, typeString, false, this.getNodeLocation(payloadNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth, this.primaryTypeSymbolSource(anchor.element));
|
|
1017
1026
|
}
|
|
1018
1027
|
/**
|
|
1019
1028
|
* True when a located call's own result is the route's payload, so the
|
|
@@ -1069,7 +1078,12 @@ export class TypeInferrer {
|
|
|
1069
1078
|
// resolved symbol is the better answer and keeps its precedence.
|
|
1070
1079
|
const stated = recovered.statedTypeNode;
|
|
1071
1080
|
const writtenAnchor = resolvedSymbol === undefined && stated ? this.writtenAnchorOf(stated) : undefined;
|
|
1072
|
-
|
|
1081
|
+
// carrick#1819: a symbol the resolved type carries has a declaration file
|
|
1082
|
+
// as much as one read off the annotation does.
|
|
1083
|
+
const resolvedSource = resolvedSymbol !== undefined && recoveredAnchor
|
|
1084
|
+
? this.primaryTypeSymbolSource(recoveredAnchor.element)
|
|
1085
|
+
: undefined;
|
|
1086
|
+
return this.createInferredType(request, recovered.typeString, recovered.isExplicit, this.getNodeLocation(recovered.node), undefined, resolvedSymbol ?? writtenAnchor?.symbol, writtenAnchor ? writtenAnchor.depth : recoveredAnchor?.depth, writtenAnchor ? writtenAnchor.source : resolvedSource);
|
|
1073
1087
|
}
|
|
1074
1088
|
/**
|
|
1075
1089
|
* carrick#1161: type a route response from every send in the handler the
|
|
@@ -1234,6 +1248,38 @@ export class TypeInferrer {
|
|
|
1234
1248
|
typeString = explicitType;
|
|
1235
1249
|
isExplicit = true;
|
|
1236
1250
|
}
|
|
1251
|
+
// carrick#1843: a rule can now match a thenable or a carrier by its alias,
|
|
1252
|
+
// so on a result that is a carrier (`Task<Outcome<Reply<T>, E>>`) the
|
|
1253
|
+
// rules reach what the carrier holds before the carrier read below does.
|
|
1254
|
+
// That changes who finds the payload, never what is published: where the
|
|
1255
|
+
// rules read through the carrier, their answer is given the way the
|
|
1256
|
+
// carrier read gives its own. A result that is no carrier keeps the
|
|
1257
|
+
// answer it had.
|
|
1258
|
+
const where = `${request.file_path}:${request.line_number}`;
|
|
1259
|
+
const resultIsCarrier = !explicitType && this.resultCarrierArguments(returnType, terminalNode) !== undefined;
|
|
1260
|
+
// carrick#1877: the terminal holds a thenable the names do not peel, a
|
|
1261
|
+
// subclass of `Promise` or a class with a `then` of its own. What a
|
|
1262
|
+
// caller receives is what `await` yields for it, as it is for the
|
|
1263
|
+
// `Promise<T>` the text unwrap at the end of this function peels: the
|
|
1264
|
+
// class is transport. A rule that matched the type as written, a type the
|
|
1265
|
+
// source states and a carrier behind the thenable have each answered
|
|
1266
|
+
// already and keep their answers.
|
|
1267
|
+
if (!unwrapResult.wasUnwrapped && !explicitType && !resultIsCarrier) {
|
|
1268
|
+
const yielded = this.awaitedBeyondPromise(returnType);
|
|
1269
|
+
if (yielded) {
|
|
1270
|
+
typeString = typeText(yielded, terminalNode);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
// The rules verified what the carrier holds as transport and read no
|
|
1274
|
+
// payload out of it (`Reply<unknown>`). That is the decision carrick#1841
|
|
1275
|
+
// makes below, and it is decided here for the same reason: left
|
|
1276
|
+
// undecided, the capture's own locator re-reads the raw call and
|
|
1277
|
+
// publishes the library's objects.
|
|
1278
|
+
if (unwrapResult.verifiedMachinery && resultIsCarrier) {
|
|
1279
|
+
this.log(`Call result at ${where} is a carrier of transport the service's wrapper rules ` +
|
|
1280
|
+
'verify and read no payload out of; this site states no response contract');
|
|
1281
|
+
return this.transportAbstain(request, callExpr);
|
|
1282
|
+
}
|
|
1237
1283
|
// carrick#1376: the call answers a RESULT CARRIER — a generic union whose
|
|
1238
1284
|
// branches say whether the call worked and carry, on the success side, the
|
|
1239
1285
|
// value it produced. The carrier is the transport's own bookkeeping; the
|
|
@@ -1241,12 +1287,30 @@ export class TypeInferrer {
|
|
|
1241
1287
|
// the carrier instead reports an envelope as a wire contract, which is the
|
|
1242
1288
|
// same class of answer the machinery guard refuses on the producer side.
|
|
1243
1289
|
//
|
|
1244
|
-
// A
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1247
|
-
|
|
1290
|
+
// A type the source itself states outranks this: it is what the author
|
|
1291
|
+
// said. A wrapper rule that unwrapped is read first, and on a carrier it
|
|
1292
|
+
// no longer ends the matter (carrick#1843):
|
|
1293
|
+
//
|
|
1294
|
+
// - a service with a rule for `Task` and none for `Outcome` leaves
|
|
1295
|
+
// `Outcome<Reply<T>, E>`, which is as much a carrier as it was before
|
|
1296
|
+
// the rule could match. The carrier read takes what the rule left.
|
|
1297
|
+
// - a service with a rule for `Outcome` too reads the payload out of the
|
|
1298
|
+
// carrier itself. That payload is what the carrier holds, and is
|
|
1299
|
+
// published as the carrier read publishes it, so a site answers the
|
|
1300
|
+
// same whichever of the two found its payload.
|
|
1301
|
+
//
|
|
1302
|
+
// A rule that left no single payload (a union join) leaves nothing to
|
|
1303
|
+
// read.
|
|
1304
|
+
const afterRules = unwrapResult.wasUnwrapped ? unwrapResult.payloadType : returnType;
|
|
1305
|
+
const readThroughCarrier = unwrapResult.wasUnwrapped &&
|
|
1306
|
+
resultIsCarrier &&
|
|
1307
|
+
!!afterRules &&
|
|
1308
|
+
this.resultCarrierArguments(afterRules, terminalNode) === undefined;
|
|
1309
|
+
const carrierCandidate = explicitType || !afterRules
|
|
1248
1310
|
? undefined
|
|
1249
|
-
:
|
|
1311
|
+
: readThroughCarrier
|
|
1312
|
+
? afterRules
|
|
1313
|
+
: this.resultCarrierPayload(afterRules, terminalNode, use.projections, where);
|
|
1250
1314
|
// carrick#1841: what the carrier holds goes through the service's wrapper
|
|
1251
1315
|
// rules before it is published, as the call's own result did. The carrier
|
|
1252
1316
|
// is found by its shape, so what it holds can still be a library's
|
|
@@ -1269,21 +1333,9 @@ export class TypeInferrer {
|
|
|
1269
1333
|
carrierUnwrap?.wasUnwrapped &&
|
|
1270
1334
|
carrierUnwrap.typeString.trim() === 'unknown') {
|
|
1271
1335
|
const carried = typeText(carrierCandidate, terminalNode);
|
|
1272
|
-
this.log(`Call result at ${
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
const abstain = this.createInferredType(request, 'unknown', false, this.getNodeLocation(callExpr));
|
|
1276
|
-
abstain.any_provenance = [
|
|
1277
|
-
{
|
|
1278
|
-
path: '',
|
|
1279
|
-
kind: 'unknown',
|
|
1280
|
-
reason: 'machinery_envelope',
|
|
1281
|
-
detail: "what this call's result carries is transport that the service's wrapper rules " +
|
|
1282
|
-
'verify and read no payload out of (a library response object around the body), ' +
|
|
1283
|
-
'so this site states no response contract',
|
|
1284
|
-
},
|
|
1285
|
-
];
|
|
1286
|
-
return abstain;
|
|
1336
|
+
this.log(`Call result at ${where} carries ${carried}, which the service's wrapper rules ` +
|
|
1337
|
+
'verify as transport and read no payload out of; this site states no response contract');
|
|
1338
|
+
return this.transportAbstain(request, callExpr);
|
|
1287
1339
|
}
|
|
1288
1340
|
const carried = carrierUnwrap?.wasUnwrapped && carrierUnwrap.payloadType
|
|
1289
1341
|
? carrierUnwrap.payloadType
|
|
@@ -1358,19 +1410,40 @@ export class TypeInferrer {
|
|
|
1358
1410
|
// to no single payload (union join, verified machinery) carries no
|
|
1359
1411
|
// payloadType and anchors nothing; the wrapper's own symbol must never
|
|
1360
1412
|
// anchor via that path.
|
|
1361
|
-
|
|
1362
|
-
|
|
1413
|
+
//
|
|
1414
|
+
// carrick#1877: the call's payload is what `await` yields for the call's
|
|
1415
|
+
// type. Where no rule matched that type as the names peel it, the same
|
|
1416
|
+
// protocol reading the text takes is read here, so a subclass of
|
|
1417
|
+
// `Promise` anchors the row on what it resolves to and never on itself.
|
|
1418
|
+
let callPayloadType = this.unwrapPromiseType(callExpr.getType());
|
|
1419
|
+
let callUnwrap = this.unwrapTypeWithConfig(callPayloadType, callExpr, extractionConfig);
|
|
1420
|
+
let readByProtocol = false;
|
|
1421
|
+
if (!callUnwrap.wasUnwrapped) {
|
|
1422
|
+
const yielded = this.awaitedBeyondPromise(callPayloadType);
|
|
1423
|
+
if (yielded) {
|
|
1424
|
+
callPayloadType = yielded;
|
|
1425
|
+
callUnwrap = this.unwrapTypeWithConfig(yielded, callExpr, extractionConfig);
|
|
1426
|
+
readByProtocol = true;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1363
1429
|
// carrick#1376: the carrier's own symbol must never anchor either. The
|
|
1364
1430
|
// surface pre-claims the alias from the anchor, so anchoring on the
|
|
1365
1431
|
// carrier makes the capture emit the transport's bookkeeping as the
|
|
1366
1432
|
// operation's declaration — and where the carrier is a local interface the
|
|
1367
1433
|
// service does not export, nothing is emitted at all and the row reads
|
|
1368
1434
|
// null. The payload the carrier was found to hold is the anchor.
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1435
|
+
//
|
|
1436
|
+
// carrick#1843: that holds for a carrier a rule left of the call's own
|
|
1437
|
+
// result too. A rule for `Task` alone turns `Task<Outcome<T, E>>` into
|
|
1438
|
+
// `Outcome<T, E>` here, and where the terminal's read took no payload out
|
|
1439
|
+
// of it (an open payload, or a terminal further down a chain) the
|
|
1440
|
+
// carrier's alias would otherwise be the anchor. The same holds for a
|
|
1441
|
+
// carrier the protocol reading left (carrick#1877).
|
|
1442
|
+
const callAfterRules = callUnwrap.wasUnwrapped ? callUnwrap.payloadType : callPayloadType;
|
|
1443
|
+
const leftCarrier = (callUnwrap.wasUnwrapped || readByProtocol) &&
|
|
1444
|
+
!!callAfterRules &&
|
|
1445
|
+
this.resultCarrierArguments(callAfterRules, callExpr) !== undefined;
|
|
1446
|
+
const anchorSource = carrierPayload ?? (leftCarrier ? undefined : callAfterRules);
|
|
1374
1447
|
let anchor = anchorSource
|
|
1375
1448
|
? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
|
|
1376
1449
|
: undefined;
|
|
@@ -1415,7 +1488,7 @@ export class TypeInferrer {
|
|
|
1415
1488
|
}
|
|
1416
1489
|
}
|
|
1417
1490
|
}
|
|
1418
|
-
const inferred = this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(terminalNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, anchor ? this.primaryTypeSymbol(anchor.element) : undefined, anchor?.depth);
|
|
1491
|
+
const inferred = this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(terminalNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, anchor ? this.primaryTypeSymbol(anchor.element) : undefined, anchor?.depth, anchor ? this.primaryTypeSymbolSource(anchor.element) : undefined);
|
|
1419
1492
|
// carrick#1749: say when the text is the source's own statement of the
|
|
1420
1493
|
// body, and what that statement is rooted at, so the scanner can tell a
|
|
1421
1494
|
// model symbol that names the body from one that names a part of it.
|
|
@@ -1973,6 +2046,21 @@ export class TypeInferrer {
|
|
|
1973
2046
|
return { terminal: returnExpr, projectionOnly: false, projections: [] };
|
|
1974
2047
|
}
|
|
1975
2048
|
}
|
|
2049
|
+
// carrick#1851: the source reads the body in place on the call's own
|
|
2050
|
+
// value, `const body = await (await fetch(url)).text()`. No binding holds
|
|
2051
|
+
// the response, so the walk below has nothing to follow, and the call
|
|
2052
|
+
// itself would stand as the terminal and publish the transport object.
|
|
2053
|
+
// The read is the terminal, as it is when the walk finds it on a binding,
|
|
2054
|
+
// and a call that takes the read and states what it returns says more
|
|
2055
|
+
// than the read does (carrick#1382).
|
|
2056
|
+
const inPlaceRead = this.bodyReadOnCallValue(callExpr);
|
|
2057
|
+
if (inPlaceRead) {
|
|
2058
|
+
return {
|
|
2059
|
+
terminal: this.statedPayloadAroundBodyRead(inPlaceRead) ?? inPlaceRead,
|
|
2060
|
+
projectionOnly: false,
|
|
2061
|
+
projections: [],
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
1976
2064
|
const binding = this.extractBindingFromCall(callExpr);
|
|
1977
2065
|
if (binding && func) {
|
|
1978
2066
|
let currentNames = binding.names;
|
|
@@ -2134,36 +2222,11 @@ export class TypeInferrer {
|
|
|
2134
2222
|
* reader can see is an envelope.
|
|
2135
2223
|
*/
|
|
2136
2224
|
resultCarrierPayload(type, at, projections, where) {
|
|
2137
|
-
const
|
|
2138
|
-
if (!
|
|
2139
|
-
return undefined;
|
|
2140
|
-
}
|
|
2141
|
-
const branches = carrier.getUnionTypes();
|
|
2142
|
-
if (branches.length < 2 || !branches.every((branch) => this.isObjectShape(branch))) {
|
|
2143
|
-
return undefined;
|
|
2144
|
-
}
|
|
2145
|
-
const args = [
|
|
2146
|
-
...carrier.getAliasTypeArguments(),
|
|
2147
|
-
...carrier.getTypeArguments(),
|
|
2148
|
-
];
|
|
2149
|
-
if (args.length < 2) {
|
|
2150
|
-
return undefined;
|
|
2151
|
-
}
|
|
2152
|
-
// A type argument only names a payload when a branch actually holds it:
|
|
2153
|
-
// a generic that parameterises a status code or a key carries nothing.
|
|
2154
|
-
const carried = args.filter((arg) => branches.some((branch) => branch
|
|
2155
|
-
.getProperties()
|
|
2156
|
-
.some((property) => {
|
|
2157
|
-
try {
|
|
2158
|
-
return property.getTypeAtLocation(at).getText() === arg.getText();
|
|
2159
|
-
}
|
|
2160
|
-
catch {
|
|
2161
|
-
return false;
|
|
2162
|
-
}
|
|
2163
|
-
})));
|
|
2164
|
-
if (carried.length === 0) {
|
|
2225
|
+
const shape = this.resultCarrierArguments(type, at);
|
|
2226
|
+
if (!shape) {
|
|
2165
2227
|
return undefined;
|
|
2166
2228
|
}
|
|
2229
|
+
const { carrier, carried } = shape;
|
|
2167
2230
|
const argTexts = new Map(carried.map((arg) => [arg.getText(), arg]));
|
|
2168
2231
|
const read = new Set();
|
|
2169
2232
|
for (const projection of projections) {
|
|
@@ -2198,12 +2261,102 @@ export class TypeInferrer {
|
|
|
2198
2261
|
return undefined;
|
|
2199
2262
|
}
|
|
2200
2263
|
/**
|
|
2201
|
-
*
|
|
2202
|
-
*
|
|
2203
|
-
*
|
|
2204
|
-
|
|
2264
|
+
* The shape test of `resultCarrierPayload`: the carrier `type` is, once a
|
|
2265
|
+
* promise-like around it is peeled, and the type arguments a branch of it
|
|
2266
|
+
* holds as a member. `undefined` when `type` is not a carrier.
|
|
2267
|
+
*/
|
|
2268
|
+
resultCarrierArguments(type, at) {
|
|
2269
|
+
const carrier = this.unwrapThenableType(this.unwrapPromiseType(type));
|
|
2270
|
+
if (!carrier.isUnion()) {
|
|
2271
|
+
return undefined;
|
|
2272
|
+
}
|
|
2273
|
+
const branches = carrier.getUnionTypes();
|
|
2274
|
+
if (branches.length < 2 || !branches.every((branch) => this.isObjectShape(branch))) {
|
|
2275
|
+
return undefined;
|
|
2276
|
+
}
|
|
2277
|
+
const args = [
|
|
2278
|
+
...carrier.getAliasTypeArguments(),
|
|
2279
|
+
...carrier.getTypeArguments(),
|
|
2280
|
+
];
|
|
2281
|
+
if (args.length < 2) {
|
|
2282
|
+
return undefined;
|
|
2283
|
+
}
|
|
2284
|
+
// A type argument only names a payload when a branch actually holds it:
|
|
2285
|
+
// a generic that parameterises a status code or a key carries nothing.
|
|
2286
|
+
const carried = args.filter((arg) => branches.some((branch) => branch
|
|
2287
|
+
.getProperties()
|
|
2288
|
+
.some((property) => {
|
|
2289
|
+
try {
|
|
2290
|
+
return property.getTypeAtLocation(at).getText() === arg.getText();
|
|
2291
|
+
}
|
|
2292
|
+
catch {
|
|
2293
|
+
return false;
|
|
2294
|
+
}
|
|
2295
|
+
})));
|
|
2296
|
+
return carried.length === 0 ? undefined : { carrier, carried };
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* The decided abstain of a call whose result carries transport the
|
|
2300
|
+
* service's wrapper rules verify and read no payload out of (carrick#1841,
|
|
2301
|
+
* carrick#1843): `unknown` with `machinery_envelope` at the root and no
|
|
2302
|
+
* anchor. The root reason is what keeps the capture's own locator from
|
|
2303
|
+
* re-reading the raw call (`inference_decided_no_contract`,
|
|
2304
|
+
* engine/type_compat_v2.rs).
|
|
2305
|
+
*/
|
|
2306
|
+
transportAbstain(request, callExpr) {
|
|
2307
|
+
const abstain = this.createInferredType(request, 'unknown', false, this.getNodeLocation(callExpr));
|
|
2308
|
+
abstain.any_provenance = [
|
|
2309
|
+
{
|
|
2310
|
+
path: '',
|
|
2311
|
+
kind: 'unknown',
|
|
2312
|
+
reason: 'machinery_envelope',
|
|
2313
|
+
detail: "what this call's result carries is transport that the service's wrapper rules " +
|
|
2314
|
+
'verify and read no payload out of (a library response object around the body), ' +
|
|
2315
|
+
'so this site states no response contract',
|
|
2316
|
+
},
|
|
2317
|
+
];
|
|
2318
|
+
return abstain;
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* `Future<T>` -> `T` for a thenable, read off the await protocol rather
|
|
2322
|
+
* than a name: a `then` whose first parameter is a callback, whose own
|
|
2323
|
+
* first parameter is the value awaiting it yields. `Promise` and
|
|
2324
|
+
* `PromiseLike` are peeled by `unwrapPromiseType` before this.
|
|
2325
|
+
*
|
|
2326
|
+
* The callback is read through `null` and `undefined` (carrick#1877). A
|
|
2327
|
+
* `then` of the source's own making declares `(value: T) => void`; the
|
|
2328
|
+
* platform's declares `onfulfilled?: ((value: T) => ...) | null`, and that
|
|
2329
|
+
* is the `then` a subclass of `Promise` inherits. Read as written, an
|
|
2330
|
+
* optional, nullable callback has no call signature, and the subclass was
|
|
2331
|
+
* not seen as a thenable at all.
|
|
2332
|
+
*
|
|
2333
|
+
* A `then` that cannot be called, or whose first parameter is no callback,
|
|
2334
|
+
* is no protocol, and the type is returned as it is.
|
|
2335
|
+
*
|
|
2336
|
+
* The compiler's own awaited type arbitrates. This walk reads the first
|
|
2337
|
+
* signature of `then`; the language reads all of them. Where awaiting the
|
|
2338
|
+
* type and awaiting what this walk found are not the same thing to the
|
|
2339
|
+
* compiler (an overloaded `then` whose first signature is not the one
|
|
2340
|
+
* `await` takes), the walk did not read the protocol, and the type is
|
|
2341
|
+
* returned as it is rather than published as a guess.
|
|
2205
2342
|
*/
|
|
2206
2343
|
unwrapThenableType(type) {
|
|
2344
|
+
const yielded = this.firstThenValue(type);
|
|
2345
|
+
if (yielded === type)
|
|
2346
|
+
return type;
|
|
2347
|
+
try {
|
|
2348
|
+
const checker = this.project.getTypeChecker().compilerObject;
|
|
2349
|
+
const awaited = checker.getAwaitedType(type.compilerType);
|
|
2350
|
+
return awaited !== undefined && awaited === checker.getAwaitedType(yielded.compilerType)
|
|
2351
|
+
? yielded
|
|
2352
|
+
: type;
|
|
2353
|
+
}
|
|
2354
|
+
catch {
|
|
2355
|
+
return type;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
/** The value `then`'s first signature hands its callback, read until it stops changing. */
|
|
2359
|
+
firstThenValue(type) {
|
|
2207
2360
|
let current = type;
|
|
2208
2361
|
for (let depth = 0; depth < 8; depth++) {
|
|
2209
2362
|
const then = current.getProperty('then');
|
|
@@ -2216,7 +2369,8 @@ export class TypeInferrer {
|
|
|
2216
2369
|
.getTypeAtLocation(declaration)
|
|
2217
2370
|
.getCallSignatures()[0]
|
|
2218
2371
|
?.getParameters()[0]
|
|
2219
|
-
?.getTypeAtLocation(declaration)
|
|
2372
|
+
?.getTypeAtLocation(declaration)
|
|
2373
|
+
.getNonNullableType();
|
|
2220
2374
|
}
|
|
2221
2375
|
catch {
|
|
2222
2376
|
return current;
|
|
@@ -2231,6 +2385,18 @@ export class TypeInferrer {
|
|
|
2231
2385
|
}
|
|
2232
2386
|
return current;
|
|
2233
2387
|
}
|
|
2388
|
+
/**
|
|
2389
|
+
* What `await` yields for `type` where the names do not say: `type` is,
|
|
2390
|
+
* once `Promise` and `PromiseLike` are peeled, a thenable by the protocol
|
|
2391
|
+
* (a subclass of `Promise`, a class with a `then` of its own). `undefined`
|
|
2392
|
+
* where the names say it all or `type` is no thenable, so a caller keeps
|
|
2393
|
+
* the reading it had (carrick#1877).
|
|
2394
|
+
*/
|
|
2395
|
+
awaitedBeyondPromise(type) {
|
|
2396
|
+
const named = this.unwrapPromiseType(type);
|
|
2397
|
+
const yielded = this.unwrapThenableType(named);
|
|
2398
|
+
return yielded === named ? undefined : yielded;
|
|
2399
|
+
}
|
|
2234
2400
|
/**
|
|
2235
2401
|
* The platform's error shape, in full: `name` and `message` strings AND a
|
|
2236
2402
|
* `stack`, which is what the `Error` interface declares and every subclass
|
|
@@ -2513,16 +2679,40 @@ export class TypeInferrer {
|
|
|
2513
2679
|
});
|
|
2514
2680
|
}
|
|
2515
2681
|
/**
|
|
2516
|
-
* The zero-argument whole-body read
|
|
2682
|
+
* The zero-argument whole-body read taken in place on the value `callExpr`
|
|
2683
|
+
* yields, or `undefined` (carrick#1851): `(await fetch(url)).text()`. The
|
|
2684
|
+
* receiver is the call itself, through the wrappers that leave a value as
|
|
2685
|
+
* it is (parentheses, `await`, `!`), so it is the same read
|
|
2686
|
+
* `bodyReadOnReceiver` finds on a binding of that value. A call that is not
|
|
2687
|
+
* awaited first is read the same way: a request that is a promise and reads
|
|
2688
|
+
* its own body (`send(url).json()`) yields the body from that read too.
|
|
2689
|
+
*/
|
|
2690
|
+
bodyReadOnCallValue(callExpr) {
|
|
2691
|
+
let value = callExpr;
|
|
2692
|
+
for (;;) {
|
|
2693
|
+
const parent = value.getParent();
|
|
2694
|
+
if (!parent ||
|
|
2695
|
+
!(Node.isParenthesizedExpression(parent) ||
|
|
2696
|
+
Node.isAwaitExpression(parent) ||
|
|
2697
|
+
Node.isNonNullExpression(parent)) ||
|
|
2698
|
+
parent.getExpression() !== value) {
|
|
2699
|
+
break;
|
|
2700
|
+
}
|
|
2701
|
+
value = parent;
|
|
2702
|
+
}
|
|
2703
|
+
return this.bodyReadOnReceiver(value);
|
|
2704
|
+
}
|
|
2705
|
+
/**
|
|
2706
|
+
* The zero-argument whole-body read that takes `receiver` as its receiver,
|
|
2517
2707
|
* `res.json()` or `res.text()`, or `undefined`. A text read is a body read
|
|
2518
2708
|
* like a json one (carrick#1842): without it, `return res.text()` left the
|
|
2519
2709
|
* walk on the response binding and published the transport object.
|
|
2520
2710
|
*/
|
|
2521
|
-
bodyReadOnReceiver(
|
|
2522
|
-
const access =
|
|
2711
|
+
bodyReadOnReceiver(receiver) {
|
|
2712
|
+
const access = receiver.getParent();
|
|
2523
2713
|
if (!access ||
|
|
2524
2714
|
!Node.isPropertyAccessExpression(access) ||
|
|
2525
|
-
access.getExpression() !==
|
|
2715
|
+
access.getExpression() !== receiver ||
|
|
2526
2716
|
!WHOLE_BODY_READS.has(access.getName())) {
|
|
2527
2717
|
return undefined;
|
|
2528
2718
|
}
|
|
@@ -2787,26 +2977,31 @@ export class TypeInferrer {
|
|
|
2787
2977
|
if (depth >= maxDepth) {
|
|
2788
2978
|
return { kind: 'no-match' };
|
|
2789
2979
|
}
|
|
2790
|
-
const
|
|
2791
|
-
|
|
2792
|
-
//
|
|
2793
|
-
//
|
|
2794
|
-
//
|
|
2795
|
-
//
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2980
|
+
const readings = this.wrapperReadings(type);
|
|
2981
|
+
// 1. Check exact wrapperSymbols match, against either name the type goes
|
|
2982
|
+
// by (carrick#1843). When the rule also carries originModuleGlobs, the
|
|
2983
|
+
// declaration of the name that matched must come from a matching module
|
|
2984
|
+
// — names like `Response` are shared by the DOM, frameworks, and HTTP
|
|
2985
|
+
// clients, so a bare name match would unwrap unrelated types. A name
|
|
2986
|
+
// that fails that gate is no match, and the rule's test of members and
|
|
2987
|
+
// origin below still has its turn: a service's alias that shares the
|
|
2988
|
+
// rule's name can stand around the very type the rule verifies.
|
|
2989
|
+
const named = readings.filter((reading) => rule.wrapperSymbols?.includes(reading.symbol.getName()));
|
|
2990
|
+
const globs = rule.originModuleGlobs ?? [];
|
|
2991
|
+
const originGated = globs.length > 0;
|
|
2992
|
+
const matched = originGated
|
|
2993
|
+
? named.find((reading) => this.symbolOriginatesFromModules(reading.symbol, globs))
|
|
2994
|
+
: named[0];
|
|
2995
|
+
if (matched) {
|
|
2996
|
+
const extracted = this.extractPayloadFromWrapper(type, matched, node, rule, config, depth);
|
|
2997
|
+
if (extracted) {
|
|
2998
|
+
return { kind: 'extracted', result: extracted };
|
|
2808
2999
|
}
|
|
2809
|
-
|
|
3000
|
+
// A name-only match is not proof of machinery: a local type that
|
|
3001
|
+
// happens to share the name must keep its real structural type when
|
|
3002
|
+
// nothing was extracted. Only origin-verified matches may collapse
|
|
3003
|
+
// to `unknown`.
|
|
3004
|
+
return originGated ? { kind: 'verified-no-payload' } : { kind: 'no-match' };
|
|
2810
3005
|
}
|
|
2811
3006
|
// 2. Check machineryIndicators + originModuleGlobs. Indicators alone are
|
|
2812
3007
|
// too many false positives, so the origin gate is mandatory here — which
|
|
@@ -2818,10 +3013,18 @@ export class TypeInferrer {
|
|
|
2818
3013
|
if (!this.typeHasMachineryIndicators(type, rule.machineryIndicators)) {
|
|
2819
3014
|
return { kind: 'no-match' };
|
|
2820
3015
|
}
|
|
2821
|
-
|
|
3016
|
+
// The rule named neither of the type's names, so this branch reads
|
|
3017
|
+
// what it always read: the first symbol the type has, and the type's
|
|
3018
|
+
// own arguments. An alias the rule did not name says nothing about
|
|
3019
|
+
// where a payload is, whoever declares it (carrick#1843):
|
|
3020
|
+
// `Omit<Response, 'json'>` is written through a library alias whose
|
|
3021
|
+
// first argument is the transport itself, and a service's own alias
|
|
3022
|
+
// around a library's object orders its parameters as it likes.
|
|
3023
|
+
const symbol = type.getSymbol() || type.getAliasSymbol();
|
|
3024
|
+
if (!symbol || !this.symbolOriginatesFromModules(symbol, rule.originModuleGlobs)) {
|
|
2822
3025
|
return { kind: 'no-match' };
|
|
2823
3026
|
}
|
|
2824
|
-
const extracted = this.extractPayloadFromWrapper(type, node, rule, config, depth);
|
|
3027
|
+
const extracted = this.extractPayloadFromWrapper(type, { symbol, typeArguments: type.getTypeArguments(), viaAlias: false }, node, rule, config, depth);
|
|
2825
3028
|
if (extracted) {
|
|
2826
3029
|
return { kind: 'extracted', result: extracted };
|
|
2827
3030
|
}
|
|
@@ -2829,14 +3032,44 @@ export class TypeInferrer {
|
|
|
2829
3032
|
}
|
|
2830
3033
|
return { kind: 'no-match' };
|
|
2831
3034
|
}
|
|
3035
|
+
/**
|
|
3036
|
+
* The names `type` goes by, own symbol first (carrick#1843).
|
|
3037
|
+
*
|
|
3038
|
+
* `type Task<A> = __Task<A>` has the class's symbol and arguments, and the
|
|
3039
|
+
* alias's beside them. `type Reply<T> = { ... }` has the anonymous `__type`
|
|
3040
|
+
* with no arguments of its own. `type Outcome<A, E> = Done<A, E> |
|
|
3041
|
+
* Failed<A, E>` has no symbol of its own at all. In each, the alias and its
|
|
3042
|
+
* arguments are what a rule naming `Task`, `Reply` or `Outcome` describes.
|
|
3043
|
+
*/
|
|
3044
|
+
wrapperReadings(type) {
|
|
3045
|
+
const readings = [];
|
|
3046
|
+
const own = type.getSymbol();
|
|
3047
|
+
if (own) {
|
|
3048
|
+
readings.push({ symbol: own, typeArguments: type.getTypeArguments(), viaAlias: false });
|
|
3049
|
+
}
|
|
3050
|
+
const alias = type.getAliasSymbol();
|
|
3051
|
+
if (alias && alias !== own) {
|
|
3052
|
+
readings.push({
|
|
3053
|
+
symbol: alias,
|
|
3054
|
+
typeArguments: type.getAliasTypeArguments(),
|
|
3055
|
+
viaAlias: true,
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
return readings;
|
|
3059
|
+
}
|
|
2832
3060
|
/**
|
|
2833
3061
|
* Extract the payload type from a matched wrapper. Returns null when the
|
|
2834
3062
|
* rule matched the wrapper but no payload is recoverable from generics or
|
|
2835
3063
|
* property paths — the caller decides what a payload-less match means
|
|
2836
3064
|
* (verified machinery collapses to `unknown` after every rule has run;
|
|
2837
3065
|
* a name-only match leaves the type untouched).
|
|
3066
|
+
*
|
|
3067
|
+
* `payloadGenericIndex` counts the arguments of `reading`, the name the
|
|
3068
|
+
* rule matched (carrick#1843): an alias is free to order its parameters
|
|
3069
|
+
* differently from the type it stands for, so the same index into the
|
|
3070
|
+
* other list is a different argument.
|
|
2838
3071
|
*/
|
|
2839
|
-
extractPayloadFromWrapper(type, node, rule, config, depth) {
|
|
3072
|
+
extractPayloadFromWrapper(type, reading, node, rule, config, depth) {
|
|
2840
3073
|
// The outer extraction already succeeded on the paths below; a recursive
|
|
2841
3074
|
// inner pass that finds nothing more must not demote the result back to
|
|
2842
3075
|
// "not unwrapped" (which would discard the recovered payload). Only when
|
|
@@ -2855,7 +3088,7 @@ export class TypeInferrer {
|
|
|
2855
3088
|
};
|
|
2856
3089
|
// 1. Try generic type argument at payloadGenericIndex
|
|
2857
3090
|
const genericIndex = rule.payloadGenericIndex ?? 0;
|
|
2858
|
-
const typeArgs =
|
|
3091
|
+
const typeArgs = reading.typeArguments;
|
|
2859
3092
|
if (typeArgs.length > genericIndex) {
|
|
2860
3093
|
const payloadArg = typeArgs[genericIndex];
|
|
2861
3094
|
// Check if it's a useful type (not any/unknown/never)
|
|
@@ -2872,8 +3105,11 @@ export class TypeInferrer {
|
|
|
2872
3105
|
payloadType: payloadArg,
|
|
2873
3106
|
};
|
|
2874
3107
|
}
|
|
2875
|
-
// Try "first useful generic" heuristic
|
|
2876
|
-
|
|
3108
|
+
// Try "first useful generic" heuristic. It is a guess about a type's
|
|
3109
|
+
// own arguments and is not extended to an alias's (carrick#1843): the
|
|
3110
|
+
// alias this reaches most often is a result carrier, `Outcome<A, E>`,
|
|
3111
|
+
// whose next argument after an open payload is the error side.
|
|
3112
|
+
for (let i = 0; !reading.viaAlias && i < typeArgs.length; i++) {
|
|
2877
3113
|
const argType = typeArgs[i];
|
|
2878
3114
|
const text = typeText(argType, node);
|
|
2879
3115
|
if (!this.isUselessType(text)) {
|
|
@@ -3927,11 +4163,19 @@ export class TypeInferrer {
|
|
|
3927
4163
|
/**
|
|
3928
4164
|
* Declaration file (absolute path) of the anchor symbol
|
|
3929
4165
|
* `primaryTypeSymbol` reports for this type, or `undefined` when the type
|
|
3930
|
-
* has no user-facing anchor or no source declaration.
|
|
3931
|
-
*
|
|
3932
|
-
*
|
|
3933
|
-
*
|
|
3934
|
-
*
|
|
4166
|
+
* has no user-facing anchor or no source declaration. Every path that
|
|
4167
|
+
* reports the symbol reports this beside it (carrick#1819): it is where a
|
|
4168
|
+
* reader finds the type an anchor names. The scanner's pub/sub two-anchor
|
|
4169
|
+
* arbitration (carrick#413) also uses it to re-aim a demoted explicit
|
|
4170
|
+
* bundle request: the bundler resolves a `SymbolRequest` only against
|
|
4171
|
+
* declarations IN its `source_file`, so the request must point at the file
|
|
4172
|
+
* that actually declares the tsc-witnessed payload type.
|
|
4173
|
+
*
|
|
4174
|
+
* The declarations read are those of the type's own symbol, so a name
|
|
4175
|
+
* imported through a barrel reports the file that declares it, and a name
|
|
4176
|
+
* two files declare reports the one this type resolves to. A declaration in
|
|
4177
|
+
* an installed package or a TypeScript lib is reported as it is found, as
|
|
4178
|
+
* an absolute path; what a reader is shown for it is the scanner's call.
|
|
3935
4179
|
*
|
|
3936
4180
|
* Only declaration kinds the bundler's `validateSymbols` can resolve
|
|
3937
4181
|
* (interface, type alias, class, enum, function, variable) count. A
|
|
@@ -4251,44 +4495,7 @@ export class TypeInferrer {
|
|
|
4251
4495
|
* which is the honest answer and the one a consumer check can act on.
|
|
4252
4496
|
*/
|
|
4253
4497
|
findFunctionByLine(sourceFile, line) {
|
|
4254
|
-
|
|
4255
|
-
const functions = [];
|
|
4256
|
-
/** Statements opening inside the forward window, in source order. */
|
|
4257
|
-
const windowStatements = [];
|
|
4258
|
-
for (const node of sourceFile.getDescendants()) {
|
|
4259
|
-
if (Node.isFunctionDeclaration(node) ||
|
|
4260
|
-
Node.isArrowFunction(node) ||
|
|
4261
|
-
Node.isFunctionExpression(node) ||
|
|
4262
|
-
Node.isMethodDeclaration(node)) {
|
|
4263
|
-
functions.push(node);
|
|
4264
|
-
}
|
|
4265
|
-
if (Node.isStatement(node)) {
|
|
4266
|
-
const start = node.getStartLineNumber();
|
|
4267
|
-
if (start >= line && start <= line + LINE_TOLERANCE) {
|
|
4268
|
-
windowStatements.push(node);
|
|
4269
|
-
}
|
|
4270
|
-
}
|
|
4271
|
-
}
|
|
4272
|
-
const separatedFromAnchor = (fn) => windowStatements.some((statement) => statement.getStartLineNumber() < fn.getStartLineNumber() &&
|
|
4273
|
-
!(statement.getStart() <= fn.getStart() && statement.getEnd() >= fn.getEnd()));
|
|
4274
|
-
let best;
|
|
4275
|
-
let bestDelta = Infinity;
|
|
4276
|
-
for (const fn of functions) {
|
|
4277
|
-
const delta = Math.abs(fn.getStartLineNumber() - line);
|
|
4278
|
-
if (delta > LINE_TOLERANCE)
|
|
4279
|
-
continue;
|
|
4280
|
-
if (fn.getStartLineNumber() > line && separatedFromAnchor(fn))
|
|
4281
|
-
continue;
|
|
4282
|
-
const isCloser = delta < bestDelta;
|
|
4283
|
-
const isInnermostTie = delta === bestDelta &&
|
|
4284
|
-
best !== undefined &&
|
|
4285
|
-
fn.getEnd() - fn.getStart() < best.getEnd() - best.getStart();
|
|
4286
|
-
if (isCloser || isInnermostTie) {
|
|
4287
|
-
best = fn;
|
|
4288
|
-
bestDelta = delta;
|
|
4289
|
-
}
|
|
4290
|
-
}
|
|
4291
|
-
return best;
|
|
4498
|
+
return functionAtLine(sourceFile, line);
|
|
4292
4499
|
}
|
|
4293
4500
|
/**
|
|
4294
4501
|
* Walk up from a node to find its innermost containing function.
|