carrick 0.3.106 → 0.3.108
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/api.d.ts +15 -0
- package/sidecar/dist/src/capture/check-classify.d.ts +9 -0
- package/sidecar/dist/src/capture/check-classify.js +20 -0
- package/sidecar/dist/src/capture/check-fields.d.ts +24 -0
- package/sidecar/dist/src/capture/check-fields.js +54 -36
- package/sidecar/dist/src/capture/check-poison.js +4 -14
- package/sidecar/dist/src/capture/check-union.d.ts +40 -0
- package/sidecar/dist/src/capture/check-union.js +92 -0
- package/sidecar/dist/src/capture/check.js +5 -0
- package/sidecar/dist/src/capture/index.js +138 -80
- 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/definition-resolver.d.ts +5 -8
- package/sidecar/dist/src/definition-resolver.js +5 -7
- 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 +62 -9
- package/sidecar/dist/src/infer-timing.d.ts +54 -0
- package/sidecar/dist/src/infer-timing.js +124 -0
- 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.d.ts +4 -2
- package/sidecar/dist/src/retype.js +10 -23
- package/sidecar/dist/src/type-inferrer.d.ts +238 -12
- package/sidecar/dist/src/type-inferrer.js +772 -168
- package/sidecar/dist/src/types.d.ts +83 -6
- package/sidecar/dist/src/validators.d.ts +54 -16
- package/sidecar/dist/src/validators.js +4 -0
- package/templates/skills/carrick-reuse.md +6 -5
|
@@ -23,8 +23,10 @@ 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';
|
|
27
|
+
import { elapsedMs, inferTiming, phaseClock, timedPhase } from './infer-timing.js';
|
|
26
28
|
import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
|
|
27
|
-
import { expandTypeStructural, } from './type-structural-expander.js';
|
|
29
|
+
import { expandTypeStructural, jsonWireType, } from './type-structural-expander.js';
|
|
28
30
|
/**
|
|
29
31
|
* TS/lib globals and primitives that must never be emitted as a deterministic
|
|
30
32
|
* type anchor (`primary_type_symbol`). A payload whose resolved symbol is one of
|
|
@@ -239,7 +241,9 @@ const RAW_TEXT_FORMAT = 'text';
|
|
|
239
241
|
*/
|
|
240
242
|
const TYPE_TEXT_FLAGS = ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.InTypeAlias;
|
|
241
243
|
function typeText(type, enclosingNode) {
|
|
242
|
-
|
|
244
|
+
// Timed as the print (carrick#1985): the type is already computed when it
|
|
245
|
+
// gets here, so this is what its length costs.
|
|
246
|
+
const text = timedPhase('print', () => type.getText(enclosingNode, TYPE_TEXT_FLAGS));
|
|
243
247
|
notePrintedType(type, enclosingNode, text);
|
|
244
248
|
return text;
|
|
245
249
|
}
|
|
@@ -255,13 +259,17 @@ function isBareStringText(text) {
|
|
|
255
259
|
/**
|
|
256
260
|
* How long the unwidened reading of one batch may take by default. The
|
|
257
261
|
* reading adds a field and never an answer, so running out costs only the
|
|
258
|
-
* readings not yet made; the rewrite is always undone.
|
|
262
|
+
* readings not yet made; the rewrite is always undone. It reports no
|
|
263
|
+
* progress, so this is also the longest a batch goes silent after its last
|
|
264
|
+
* request: well inside the 900s the scanner allows between two signs of life.
|
|
259
265
|
*/
|
|
260
266
|
const UNWIDENED_BUDGET_MS = 120_000;
|
|
261
267
|
/**
|
|
262
|
-
* No reading starts or continues past this long after the batch began
|
|
263
|
-
*
|
|
264
|
-
*
|
|
268
|
+
* No reading starts or continues past this long after the batch began. The
|
|
269
|
+
* number dates from when the scanner allowed a whole request 900s; it now
|
|
270
|
+
* allows that long between progress reports (carrick#1914), so a batch can
|
|
271
|
+
* run past this and still be read. A batch that does publishes no unwidened
|
|
272
|
+
* reading, as before.
|
|
265
273
|
*/
|
|
266
274
|
const UNWIDENED_LATEST_MS = 600_000;
|
|
267
275
|
/**
|
|
@@ -282,6 +290,12 @@ export class TypeInferrer {
|
|
|
282
290
|
*/
|
|
283
291
|
readNodes = new WeakMap();
|
|
284
292
|
unwidenedBudgetMs;
|
|
293
|
+
/**
|
|
294
|
+
* The files a request has named to this inferrer, as the requests named
|
|
295
|
+
* them (carrick#1985). It lives as long as the project does, so the first
|
|
296
|
+
* request of a file is the first of the process, not of a batch.
|
|
297
|
+
*/
|
|
298
|
+
filesAsked = new Set();
|
|
285
299
|
constructor(options) {
|
|
286
300
|
this.project = options.project;
|
|
287
301
|
this.packageOf = options.packageOf;
|
|
@@ -306,24 +320,32 @@ export class TypeInferrer {
|
|
|
306
320
|
*
|
|
307
321
|
* @param requests - Array of inference requests
|
|
308
322
|
* @param extractionConfig - Agent-generated extraction config for payload unwrapping
|
|
323
|
+
* @param onRequestDone - Called once per request, as the batch is done with it
|
|
309
324
|
* @returns InferResult with inferred types or errors
|
|
310
325
|
*/
|
|
311
|
-
infer(requests, extractionConfig) {
|
|
326
|
+
infer(requests, extractionConfig, onRequestDone) {
|
|
312
327
|
const started = performance.now();
|
|
313
328
|
const inferredTypes = [];
|
|
314
329
|
const errors = [];
|
|
315
330
|
/** Response inferences the unwidened reading re-reads (carrick#1516). */
|
|
316
331
|
const responses = [];
|
|
332
|
+
/** How long each request took, in the order they were done (carrick#1985). */
|
|
333
|
+
const timings = [];
|
|
317
334
|
for (const request of requests) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (/\.(js|jsx|mjs|cjs)$/i.test(request.file_path)) {
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
335
|
+
const slotStarted = performance.now();
|
|
336
|
+
const phasesStarted = phaseClock();
|
|
337
|
+
const firstInFile = !this.filesAsked.has(request.file_path);
|
|
338
|
+
this.filesAsked.add(request.file_path);
|
|
339
|
+
let printedLength = 0;
|
|
326
340
|
try {
|
|
341
|
+
// Plain JavaScript has no type annotations to extract, and `checkJs` is
|
|
342
|
+
// off, so inferring against a `.js` file yields nothing useful — it only
|
|
343
|
+
// crashes deep in the compiler API on undefined symbols (`escapedName`,
|
|
344
|
+
// `flags`) and floods the log with the resulting error strings. Skip it.
|
|
345
|
+
// `allowJs` stays on so `.ts` files can still resolve `.js` imports.
|
|
346
|
+
if (/\.(js|jsx|mjs|cjs)$/i.test(request.file_path)) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
327
349
|
const loc = this.formatRequestLocation(request);
|
|
328
350
|
const itemError = validateInferRequestItem(request);
|
|
329
351
|
if (itemError) {
|
|
@@ -337,6 +359,7 @@ export class TypeInferrer {
|
|
|
337
359
|
const prints = new PrintedTypes();
|
|
338
360
|
const result = prints.during(() => this.inferSingle(request, extractionConfig));
|
|
339
361
|
if (result) {
|
|
362
|
+
printedLength = result.type_string.length;
|
|
340
363
|
this.recordPrintedNames(result, request, prints);
|
|
341
364
|
inferredTypes.push(result);
|
|
342
365
|
if (request.infer_kind === 'response_body' || request.infer_kind === 'function_return') {
|
|
@@ -352,6 +375,23 @@ export class TypeInferrer {
|
|
|
352
375
|
const loc = this.formatRequestLocation(request);
|
|
353
376
|
errors.push(`Error inferring type at ${request.file_path}:${loc}: ${error}`);
|
|
354
377
|
}
|
|
378
|
+
finally {
|
|
379
|
+
// Before the caller is told: what it does with the news (a progress
|
|
380
|
+
// frame) is not this request's time.
|
|
381
|
+
const phases = phaseClock();
|
|
382
|
+
timings.push({
|
|
383
|
+
...(request.alias === undefined ? {} : { alias: request.alias }),
|
|
384
|
+
file_path: request.file_path,
|
|
385
|
+
line_number: request.line_number,
|
|
386
|
+
infer_kind: request.infer_kind,
|
|
387
|
+
ms: elapsedMs(slotStarted, performance.now()),
|
|
388
|
+
type_ms: elapsedMs(phasesStarted.type, phases.type),
|
|
389
|
+
print_ms: elapsedMs(phasesStarted.print, phases.print),
|
|
390
|
+
printed_length: printedLength,
|
|
391
|
+
first_in_file: firstInFile,
|
|
392
|
+
});
|
|
393
|
+
onRequestDone?.();
|
|
394
|
+
}
|
|
355
395
|
}
|
|
356
396
|
try {
|
|
357
397
|
const deadline = Math.min(performance.now() + this.unwidenedBudgetMs, started + UNWIDENED_LATEST_MS);
|
|
@@ -364,6 +404,7 @@ export class TypeInferrer {
|
|
|
364
404
|
return {
|
|
365
405
|
success: errors.length === 0 || inferredTypes.length > 0,
|
|
366
406
|
inferred_types: inferredTypes.length > 0 ? inferredTypes : undefined,
|
|
407
|
+
timing: inferTiming(timings),
|
|
367
408
|
errors: errors.length > 0 ? errors : undefined,
|
|
368
409
|
};
|
|
369
410
|
}
|
|
@@ -771,7 +812,7 @@ export class TypeInferrer {
|
|
|
771
812
|
return null;
|
|
772
813
|
}
|
|
773
814
|
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);
|
|
815
|
+
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
816
|
if (provenance && provenance.length > 0) {
|
|
776
817
|
inferred.any_provenance = provenance;
|
|
777
818
|
}
|
|
@@ -790,7 +831,10 @@ export class TypeInferrer {
|
|
|
790
831
|
return null;
|
|
791
832
|
}
|
|
792
833
|
const isExplicit = func.getReturnTypeNode() !== undefined;
|
|
793
|
-
|
|
834
|
+
// The compiler call is timed apart from the print of what it returns
|
|
835
|
+
// (carrick#1985).
|
|
836
|
+
const returnType = timedPhase('type', () => func.getReturnType());
|
|
837
|
+
const typeString = typeText(returnType, func);
|
|
794
838
|
return this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(func));
|
|
795
839
|
}
|
|
796
840
|
/**
|
|
@@ -817,7 +861,7 @@ export class TypeInferrer {
|
|
|
817
861
|
return null;
|
|
818
862
|
}
|
|
819
863
|
const isExplicit = target.param.getTypeNode() !== undefined;
|
|
820
|
-
const paramType = target.node.getType();
|
|
864
|
+
const paramType = timedPhase('type', () => target.node.getType());
|
|
821
865
|
const typeString = typeText(paramType, target.node);
|
|
822
866
|
// Deterministic anchor for the pub/sub two-anchor arbitration
|
|
823
867
|
// (carrick#413): report the payload type's root symbol, its declaration
|
|
@@ -892,11 +936,50 @@ export class TypeInferrer {
|
|
|
892
936
|
this.log(`Line ${request.line_number} is a route registration; following handler return`);
|
|
893
937
|
return this.buildFunctionReturnInferredType(request, atLine.handler, extractionConfig, true);
|
|
894
938
|
}
|
|
939
|
+
// carrick#807: the request named an expression and the file holds none
|
|
940
|
+
// that matches. Where its line declares the handler, that handler is
|
|
941
|
+
// still the row's, and what it returns is still the question. Without
|
|
942
|
+
// this the unmatched text also fails the function lookup below, and a
|
|
943
|
+
// row a line-only request answers is lost to a locator that added
|
|
944
|
+
// nothing.
|
|
945
|
+
const declaredHandler = request.expression_text
|
|
946
|
+
? this.handlerDeclaredAtLine(sourceFile, request.line_number)
|
|
947
|
+
: undefined;
|
|
948
|
+
if (declaredHandler) {
|
|
949
|
+
this.log(`No node matches the located expression at ${request.file_path}:${request.line_number}; ` +
|
|
950
|
+
'reading what the handler this line declares returns');
|
|
951
|
+
return this.buildFunctionReturnInferredType(request, declaredHandler, extractionConfig, true);
|
|
952
|
+
}
|
|
895
953
|
// No locator, or locator didn't resolve — likely a payload-less handler
|
|
896
954
|
// (redirect, 204, streaming). Infer the containing function's return type.
|
|
897
955
|
this.log(`No payload node found for request at ${request.file_path}:${request.line_number}; falling back to function return`);
|
|
898
956
|
return this.inferFunctionReturn(sourceFile, request, extractionConfig);
|
|
899
957
|
}
|
|
958
|
+
// carrick#807: the row's line declares the handler itself, as it does for
|
|
959
|
+
// a route its module's place in the project states, and the located
|
|
960
|
+
// expression sits inside that handler. What the handler returns is the
|
|
961
|
+
// answer a line-only request gets, and a locator must not change it.
|
|
962
|
+
// The ancestor test comes first because it is free: only a node inside a
|
|
963
|
+
// function that opens on the row's line can be inside the one the line
|
|
964
|
+
// declares, and most rows are not.
|
|
965
|
+
const insideFunctionOnLine = node
|
|
966
|
+
.getAncestors()
|
|
967
|
+
.some((ancestor) => (Node.isFunctionDeclaration(ancestor) ||
|
|
968
|
+
Node.isArrowFunction(ancestor) ||
|
|
969
|
+
Node.isFunctionExpression(ancestor) ||
|
|
970
|
+
Node.isMethodDeclaration(ancestor)) &&
|
|
971
|
+
ancestor.getStartLineNumber() === request.line_number);
|
|
972
|
+
const declaredHandler = insideFunctionOnLine
|
|
973
|
+
? this.handlerDeclaredAtLine(sourceFile, request.line_number)
|
|
974
|
+
: undefined;
|
|
975
|
+
if (declaredHandler &&
|
|
976
|
+
declaredHandler.getStart() <= node.getStart() &&
|
|
977
|
+
node.getEnd() <= declaredHandler.getEnd()) {
|
|
978
|
+
const returned = this.responseOfDeclaredHandler(request, declaredHandler, node, extractionConfig);
|
|
979
|
+
if (returned !== undefined) {
|
|
980
|
+
return returned;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
900
983
|
// Route-registry object literal: the locator lands on the registry entry
|
|
901
984
|
// `{ method, path, handler: healthCheckHandler }`, whose response contract
|
|
902
985
|
// is the handler's RETURN type — one indirection away, NOT the object's own
|
|
@@ -1013,7 +1096,141 @@ export class TypeInferrer {
|
|
|
1013
1096
|
typeString = this.expandResolvedTypeStructural(resolved, typeString, this.wireFormatFor(request));
|
|
1014
1097
|
}
|
|
1015
1098
|
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);
|
|
1099
|
+
return this.createInferredType(request, typeString, false, this.getNodeLocation(payloadNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth, this.primaryTypeSymbolSource(anchor.element));
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* The handler a module exports on `line`: a function that opens on exactly
|
|
1103
|
+
* that line, exported at the top of its module, where no call on the line
|
|
1104
|
+
* registers a route (carrick#807).
|
|
1105
|
+
*
|
|
1106
|
+
* That is the anchor of a route its module's place in the project states:
|
|
1107
|
+
* the row's line is the handler's own export, and the module's exports are
|
|
1108
|
+
* how the framework finds it. Each condition keeps another kind of row out:
|
|
1109
|
+
*
|
|
1110
|
+
* - the line has to be the function's first, with no tolerance. A model
|
|
1111
|
+
* row that names no site of its own can sit on any line of a handler,
|
|
1112
|
+
* and the nearest function to such a line is whatever is declared
|
|
1113
|
+
* beside it;
|
|
1114
|
+
* - a method of a class is a controller's handler, whose rows are
|
|
1115
|
+
* anchored on what a model located before the method's own return;
|
|
1116
|
+
* - a line that registers a route has a registration to read.
|
|
1117
|
+
*/
|
|
1118
|
+
handlerDeclaredAtLine(sourceFile, line) {
|
|
1119
|
+
const declared = this.exportedFunctionOpeningOn(sourceFile, line);
|
|
1120
|
+
return declared && !this.registrationAtLine(sourceFile, line) ? declared : undefined;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* `handlerDeclaredAtLine` without its registration test, for a caller that
|
|
1124
|
+
* has already looked for a registration on the line.
|
|
1125
|
+
*/
|
|
1126
|
+
exportedFunctionOpeningOn(sourceFile, line) {
|
|
1127
|
+
const declared = this.findFunctionByLine(sourceFile, line);
|
|
1128
|
+
if (!declared || declared.getStartLineNumber() !== line)
|
|
1129
|
+
return undefined;
|
|
1130
|
+
if (Node.isFunctionDeclaration(declared)) {
|
|
1131
|
+
return Node.isSourceFile(declared.getParent()) && declared.isExported()
|
|
1132
|
+
? declared
|
|
1133
|
+
: undefined;
|
|
1134
|
+
}
|
|
1135
|
+
if (!Node.isArrowFunction(declared) && !Node.isFunctionExpression(declared)) {
|
|
1136
|
+
return undefined;
|
|
1137
|
+
}
|
|
1138
|
+
const binding = declared.getParent();
|
|
1139
|
+
if (!Node.isVariableDeclaration(binding) || binding.getInitializer() !== declared) {
|
|
1140
|
+
return undefined;
|
|
1141
|
+
}
|
|
1142
|
+
const statement = binding.getVariableStatement();
|
|
1143
|
+
return statement && Node.isSourceFile(statement.getParent()) && statement.isExported()
|
|
1144
|
+
? declared
|
|
1145
|
+
: undefined;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* The response of a handler the request's line declares, for a request
|
|
1149
|
+
* that also located an expression inside it (carrick#807).
|
|
1150
|
+
*
|
|
1151
|
+
* The handler's own return is read first, exactly as a `function_return`
|
|
1152
|
+
* request at the line reads it, and that reading stands: a body it
|
|
1153
|
+
* recovered, and a decision it made not to publish one. The located
|
|
1154
|
+
* expression is consulted in one case only. Where the handler returns a
|
|
1155
|
+
* call nothing resolves (its library is not installed), the return walk
|
|
1156
|
+
* reads no argument the source does not annotate, because it cannot tell a
|
|
1157
|
+
* response sender from a query. A located payload that is an ARGUMENT of
|
|
1158
|
+
* such a returned call says which it is, so the same walk runs again with
|
|
1159
|
+
* that callee taken as a sender: every success body the handler returns
|
|
1160
|
+
* through it, error branches dropped as before. The located expression's
|
|
1161
|
+
* own type is never what is published here.
|
|
1162
|
+
*
|
|
1163
|
+
* Returns `undefined` when the located expression is something the return
|
|
1164
|
+
* walk never looked at, so it is left to the readings below:
|
|
1165
|
+
* - the handler returns nothing (`void`): it sends through a parameter;
|
|
1166
|
+
* - the handler returns transport the walk read no body out of, and the
|
|
1167
|
+
* located expression is not part of what it returns. A body built before
|
|
1168
|
+
* the response is returned (`const res = send(body); ...; return res`)
|
|
1169
|
+
* is one. Named on a send that states an error or redirect status, it is
|
|
1170
|
+
* no success body and the row abstains.
|
|
1171
|
+
*/
|
|
1172
|
+
responseOfDeclaredHandler(request, handler, located, extractionConfig) {
|
|
1173
|
+
const walked = this.buildFunctionReturnInferredType(request, handler, extractionConfig);
|
|
1174
|
+
const returned = this.responseReturnedExpressions(handler);
|
|
1175
|
+
if (walked === null) {
|
|
1176
|
+
// The walk read everything the handler returns and published nothing.
|
|
1177
|
+
// A located expression inside one of those returns was part of that.
|
|
1178
|
+
if (this.returnedSendHolding(returned, located)) {
|
|
1179
|
+
this.log(`Response locator at ${request.file_path}:${request.line_number} names a send ` +
|
|
1180
|
+
'its handler returns, which carries no body a contract can describe; abstaining');
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
// Outside the return, it is either a send itself or what one was handed.
|
|
1184
|
+
const sends = [
|
|
1185
|
+
this.peelTransparentExpression(located),
|
|
1186
|
+
this.receivingCallOf(located, () => true),
|
|
1187
|
+
];
|
|
1188
|
+
const statesNoBody = sends.some((send) => {
|
|
1189
|
+
const status = send ? this.responseSiteStatus(send) : 'undecided';
|
|
1190
|
+
return status === 'error' || status === 'redirect';
|
|
1191
|
+
});
|
|
1192
|
+
if (statesNoBody) {
|
|
1193
|
+
this.log(`Response locator at ${request.file_path}:${request.line_number} names an error ` +
|
|
1194
|
+
'or redirect send and its handler returns no success body; abstaining');
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
return undefined;
|
|
1198
|
+
}
|
|
1199
|
+
const text = walked.type_string.trim();
|
|
1200
|
+
if (text === 'void' || text === 'undefined' || text === 'never' || text === '') {
|
|
1201
|
+
return undefined;
|
|
1202
|
+
}
|
|
1203
|
+
const unresolvedCallee = (walked.any_provenance ?? []).some((entry) => entry.reason === 'no_payload_evidence');
|
|
1204
|
+
if (!unresolvedCallee) {
|
|
1205
|
+
return walked;
|
|
1206
|
+
}
|
|
1207
|
+
const sender = this.returnedSendHolding(returned, located);
|
|
1208
|
+
const identity = sender ? this.calleeIdentity(sender) : undefined;
|
|
1209
|
+
if (identity) {
|
|
1210
|
+
const recovered = this.recoverPayloadFromResponseExpressions(returned, true, this.wireFormatFor(request), new Set([identity]));
|
|
1211
|
+
if (recovered) {
|
|
1212
|
+
this.log(`Return type at ${request.file_path}:${request.line_number} resolves to nothing; ` +
|
|
1213
|
+
'the located payload is an argument of a call the handler returns, so that ' +
|
|
1214
|
+
"call is a response sender and its success bodies are the route's");
|
|
1215
|
+
return this.inferredFromRecoveredPayload(request, recovered);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return walked;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* The call inside one of `returned` that the located expression is, or is
|
|
1222
|
+
* an argument of: the send a located payload was handed to. `undefined`
|
|
1223
|
+
* when the located expression is not part of what the handler returns.
|
|
1224
|
+
*/
|
|
1225
|
+
returnedSendHolding(returned, located) {
|
|
1226
|
+
const branches = returned.flatMap((expression) => this.expandResponseBranches(expression, 0));
|
|
1227
|
+
const isReturned = (candidate) => branches.some((branch) => branch.getStart() <= candidate.getStart() && candidate.getEnd() <= branch.getEnd());
|
|
1228
|
+
const peeled = this.peelTransparentExpression(located);
|
|
1229
|
+
if ((Node.isCallExpression(peeled) || Node.isNewExpression(peeled)) &&
|
|
1230
|
+
branches.includes(peeled)) {
|
|
1231
|
+
return peeled;
|
|
1232
|
+
}
|
|
1233
|
+
return this.receivingCallOf(located, isReturned);
|
|
1017
1234
|
}
|
|
1018
1235
|
/**
|
|
1019
1236
|
* True when a located call's own result is the route's payload, so the
|
|
@@ -1038,7 +1255,10 @@ export class TypeInferrer {
|
|
|
1038
1255
|
if (this.symbolIsLibOrExternalOrigin(element.getSymbol() ?? element.getAliasSymbol())) {
|
|
1039
1256
|
return false;
|
|
1040
1257
|
}
|
|
1041
|
-
|
|
1258
|
+
// Asked of the result as DECLARED. A value of the repo's own that
|
|
1259
|
+
// serialises to a string is still what the route sends, and drilling into
|
|
1260
|
+
// the call that built it would publish what it was built from.
|
|
1261
|
+
return this.nodeCarriesPayloadContract(call, false, false);
|
|
1042
1262
|
}
|
|
1043
1263
|
/**
|
|
1044
1264
|
* The wire representation a request's printed type takes. A route response
|
|
@@ -1069,7 +1289,12 @@ export class TypeInferrer {
|
|
|
1069
1289
|
// resolved symbol is the better answer and keeps its precedence.
|
|
1070
1290
|
const stated = recovered.statedTypeNode;
|
|
1071
1291
|
const writtenAnchor = resolvedSymbol === undefined && stated ? this.writtenAnchorOf(stated) : undefined;
|
|
1072
|
-
|
|
1292
|
+
// carrick#1819: a symbol the resolved type carries has a declaration file
|
|
1293
|
+
// as much as one read off the annotation does.
|
|
1294
|
+
const resolvedSource = resolvedSymbol !== undefined && recoveredAnchor
|
|
1295
|
+
? this.primaryTypeSymbolSource(recoveredAnchor.element)
|
|
1296
|
+
: undefined;
|
|
1297
|
+
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
1298
|
}
|
|
1074
1299
|
/**
|
|
1075
1300
|
* carrick#1161: type a route response from every send in the handler the
|
|
@@ -1234,6 +1459,38 @@ export class TypeInferrer {
|
|
|
1234
1459
|
typeString = explicitType;
|
|
1235
1460
|
isExplicit = true;
|
|
1236
1461
|
}
|
|
1462
|
+
// carrick#1843: a rule can now match a thenable or a carrier by its alias,
|
|
1463
|
+
// so on a result that is a carrier (`Task<Outcome<Reply<T>, E>>`) the
|
|
1464
|
+
// rules reach what the carrier holds before the carrier read below does.
|
|
1465
|
+
// That changes who finds the payload, never what is published: where the
|
|
1466
|
+
// rules read through the carrier, their answer is given the way the
|
|
1467
|
+
// carrier read gives its own. A result that is no carrier keeps the
|
|
1468
|
+
// answer it had.
|
|
1469
|
+
const where = `${request.file_path}:${request.line_number}`;
|
|
1470
|
+
const resultIsCarrier = !explicitType && this.resultCarrierArguments(returnType, terminalNode) !== undefined;
|
|
1471
|
+
// carrick#1877: the terminal holds a thenable the names do not peel, a
|
|
1472
|
+
// subclass of `Promise` or a class with a `then` of its own. What a
|
|
1473
|
+
// caller receives is what `await` yields for it, as it is for the
|
|
1474
|
+
// `Promise<T>` the text unwrap at the end of this function peels: the
|
|
1475
|
+
// class is transport. A rule that matched the type as written, a type the
|
|
1476
|
+
// source states and a carrier behind the thenable have each answered
|
|
1477
|
+
// already and keep their answers.
|
|
1478
|
+
if (!unwrapResult.wasUnwrapped && !explicitType && !resultIsCarrier) {
|
|
1479
|
+
const yielded = this.awaitedBeyondPromise(returnType);
|
|
1480
|
+
if (yielded) {
|
|
1481
|
+
typeString = typeText(yielded, terminalNode);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
// The rules verified what the carrier holds as transport and read no
|
|
1485
|
+
// payload out of it (`Reply<unknown>`). That is the decision carrick#1841
|
|
1486
|
+
// makes below, and it is decided here for the same reason: left
|
|
1487
|
+
// undecided, the capture's own locator re-reads the raw call and
|
|
1488
|
+
// publishes the library's objects.
|
|
1489
|
+
if (unwrapResult.verifiedMachinery && resultIsCarrier) {
|
|
1490
|
+
this.log(`Call result at ${where} is a carrier of transport the service's wrapper rules ` +
|
|
1491
|
+
'verify and read no payload out of; this site states no response contract');
|
|
1492
|
+
return this.transportAbstain(request, callExpr);
|
|
1493
|
+
}
|
|
1237
1494
|
// carrick#1376: the call answers a RESULT CARRIER — a generic union whose
|
|
1238
1495
|
// branches say whether the call worked and carry, on the success side, the
|
|
1239
1496
|
// value it produced. The carrier is the transport's own bookkeeping; the
|
|
@@ -1241,12 +1498,30 @@ export class TypeInferrer {
|
|
|
1241
1498
|
// the carrier instead reports an envelope as a wire contract, which is the
|
|
1242
1499
|
// same class of answer the machinery guard refuses on the producer side.
|
|
1243
1500
|
//
|
|
1244
|
-
// A
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1247
|
-
|
|
1501
|
+
// A type the source itself states outranks this: it is what the author
|
|
1502
|
+
// said. A wrapper rule that unwrapped is read first, and on a carrier it
|
|
1503
|
+
// no longer ends the matter (carrick#1843):
|
|
1504
|
+
//
|
|
1505
|
+
// - a service with a rule for `Task` and none for `Outcome` leaves
|
|
1506
|
+
// `Outcome<Reply<T>, E>`, which is as much a carrier as it was before
|
|
1507
|
+
// the rule could match. The carrier read takes what the rule left.
|
|
1508
|
+
// - a service with a rule for `Outcome` too reads the payload out of the
|
|
1509
|
+
// carrier itself. That payload is what the carrier holds, and is
|
|
1510
|
+
// published as the carrier read publishes it, so a site answers the
|
|
1511
|
+
// same whichever of the two found its payload.
|
|
1512
|
+
//
|
|
1513
|
+
// A rule that left no single payload (a union join) leaves nothing to
|
|
1514
|
+
// read.
|
|
1515
|
+
const afterRules = unwrapResult.wasUnwrapped ? unwrapResult.payloadType : returnType;
|
|
1516
|
+
const readThroughCarrier = unwrapResult.wasUnwrapped &&
|
|
1517
|
+
resultIsCarrier &&
|
|
1518
|
+
!!afterRules &&
|
|
1519
|
+
this.resultCarrierArguments(afterRules, terminalNode) === undefined;
|
|
1520
|
+
const carrierCandidate = explicitType || !afterRules
|
|
1248
1521
|
? undefined
|
|
1249
|
-
:
|
|
1522
|
+
: readThroughCarrier
|
|
1523
|
+
? afterRules
|
|
1524
|
+
: this.resultCarrierPayload(afterRules, terminalNode, use.projections, where);
|
|
1250
1525
|
// carrick#1841: what the carrier holds goes through the service's wrapper
|
|
1251
1526
|
// rules before it is published, as the call's own result did. The carrier
|
|
1252
1527
|
// is found by its shape, so what it holds can still be a library's
|
|
@@ -1269,21 +1544,9 @@ export class TypeInferrer {
|
|
|
1269
1544
|
carrierUnwrap?.wasUnwrapped &&
|
|
1270
1545
|
carrierUnwrap.typeString.trim() === 'unknown') {
|
|
1271
1546
|
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;
|
|
1547
|
+
this.log(`Call result at ${where} carries ${carried}, which the service's wrapper rules ` +
|
|
1548
|
+
'verify as transport and read no payload out of; this site states no response contract');
|
|
1549
|
+
return this.transportAbstain(request, callExpr);
|
|
1287
1550
|
}
|
|
1288
1551
|
const carried = carrierUnwrap?.wasUnwrapped && carrierUnwrap.payloadType
|
|
1289
1552
|
? carrierUnwrap.payloadType
|
|
@@ -1358,19 +1621,40 @@ export class TypeInferrer {
|
|
|
1358
1621
|
// to no single payload (union join, verified machinery) carries no
|
|
1359
1622
|
// payloadType and anchors nothing; the wrapper's own symbol must never
|
|
1360
1623
|
// anchor via that path.
|
|
1361
|
-
|
|
1362
|
-
|
|
1624
|
+
//
|
|
1625
|
+
// carrick#1877: the call's payload is what `await` yields for the call's
|
|
1626
|
+
// type. Where no rule matched that type as the names peel it, the same
|
|
1627
|
+
// protocol reading the text takes is read here, so a subclass of
|
|
1628
|
+
// `Promise` anchors the row on what it resolves to and never on itself.
|
|
1629
|
+
let callPayloadType = this.unwrapPromiseType(callExpr.getType());
|
|
1630
|
+
let callUnwrap = this.unwrapTypeWithConfig(callPayloadType, callExpr, extractionConfig);
|
|
1631
|
+
let readByProtocol = false;
|
|
1632
|
+
if (!callUnwrap.wasUnwrapped) {
|
|
1633
|
+
const yielded = this.awaitedBeyondPromise(callPayloadType);
|
|
1634
|
+
if (yielded) {
|
|
1635
|
+
callPayloadType = yielded;
|
|
1636
|
+
callUnwrap = this.unwrapTypeWithConfig(yielded, callExpr, extractionConfig);
|
|
1637
|
+
readByProtocol = true;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1363
1640
|
// carrick#1376: the carrier's own symbol must never anchor either. The
|
|
1364
1641
|
// surface pre-claims the alias from the anchor, so anchoring on the
|
|
1365
1642
|
// carrier makes the capture emit the transport's bookkeeping as the
|
|
1366
1643
|
// operation's declaration — and where the carrier is a local interface the
|
|
1367
1644
|
// service does not export, nothing is emitted at all and the row reads
|
|
1368
1645
|
// null. The payload the carrier was found to hold is the anchor.
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1646
|
+
//
|
|
1647
|
+
// carrick#1843: that holds for a carrier a rule left of the call's own
|
|
1648
|
+
// result too. A rule for `Task` alone turns `Task<Outcome<T, E>>` into
|
|
1649
|
+
// `Outcome<T, E>` here, and where the terminal's read took no payload out
|
|
1650
|
+
// of it (an open payload, or a terminal further down a chain) the
|
|
1651
|
+
// carrier's alias would otherwise be the anchor. The same holds for a
|
|
1652
|
+
// carrier the protocol reading left (carrick#1877).
|
|
1653
|
+
const callAfterRules = callUnwrap.wasUnwrapped ? callUnwrap.payloadType : callPayloadType;
|
|
1654
|
+
const leftCarrier = (callUnwrap.wasUnwrapped || readByProtocol) &&
|
|
1655
|
+
!!callAfterRules &&
|
|
1656
|
+
this.resultCarrierArguments(callAfterRules, callExpr) !== undefined;
|
|
1657
|
+
const anchorSource = carrierPayload ?? (leftCarrier ? undefined : callAfterRules);
|
|
1374
1658
|
let anchor = anchorSource
|
|
1375
1659
|
? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
|
|
1376
1660
|
: undefined;
|
|
@@ -1415,7 +1699,7 @@ export class TypeInferrer {
|
|
|
1415
1699
|
}
|
|
1416
1700
|
}
|
|
1417
1701
|
}
|
|
1418
|
-
const inferred = this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(terminalNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, anchor ? this.primaryTypeSymbol(anchor.element) : undefined, anchor?.depth);
|
|
1702
|
+
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
1703
|
// carrick#1749: say when the text is the source's own statement of the
|
|
1420
1704
|
// body, and what that statement is rooted at, so the scanner can tell a
|
|
1421
1705
|
// model symbol that names the body from one that names a part of it.
|
|
@@ -1528,6 +1812,17 @@ export class TypeInferrer {
|
|
|
1528
1812
|
* else, and is not reported.
|
|
1529
1813
|
*/
|
|
1530
1814
|
statedBodyAtRead(terminal) {
|
|
1815
|
+
const typeNode = this.typeNodeStatedAtRead(terminal);
|
|
1816
|
+
return typeNode ? this.statedRoot(typeNode) : undefined;
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* The type node `statedBodyAtRead` reads, for a caller that wants the
|
|
1820
|
+
* annotation itself: a consumer's response read reports its root, and a
|
|
1821
|
+
* handler's request read prints it (carrick#807). One reading of "stated at
|
|
1822
|
+
* the read" for both, so the two cannot disagree about which annotation
|
|
1823
|
+
* belongs to a read.
|
|
1824
|
+
*/
|
|
1825
|
+
typeNodeStatedAtRead(terminal) {
|
|
1531
1826
|
const typeNode = this.explicitTypeNodeFromAncestor(terminal);
|
|
1532
1827
|
const owner = typeNode?.getParent();
|
|
1533
1828
|
if (!typeNode || !owner || this.leavesAPositionOpen(typeNode))
|
|
@@ -1554,7 +1849,7 @@ export class TypeInferrer {
|
|
|
1554
1849
|
}
|
|
1555
1850
|
current = parent;
|
|
1556
1851
|
}
|
|
1557
|
-
return
|
|
1852
|
+
return typeNode;
|
|
1558
1853
|
}
|
|
1559
1854
|
/**
|
|
1560
1855
|
* A stated type with `any` or `unknown` written anywhere in it (`unknown`,
|
|
@@ -1694,12 +1989,31 @@ export class TypeInferrer {
|
|
|
1694
1989
|
// expression in the body (`c.req.json<T>()`, `req.body as T`).
|
|
1695
1990
|
const atLine = this.registrationAtLine(sourceFile, request.line_number);
|
|
1696
1991
|
if (atLine) {
|
|
1697
|
-
const
|
|
1698
|
-
if (
|
|
1699
|
-
return this.declaredRequestInferredType(request,
|
|
1992
|
+
const declared = this.declaredRequestContract(atLine.registration, atLine.handler);
|
|
1993
|
+
if (declared) {
|
|
1994
|
+
return this.declaredRequestInferredType(request, declared, this.getNodeLocation(atLine.handler));
|
|
1700
1995
|
}
|
|
1701
1996
|
}
|
|
1702
|
-
|
|
1997
|
+
// With no call registering a route on this line, the line is the
|
|
1998
|
+
// handler's own export: all that anchors a route its module's place in
|
|
1999
|
+
// the project states (carrick#807). Either way the body is what the
|
|
2000
|
+
// handler reads off the platform request it was handed, where the
|
|
2001
|
+
// source says what it read.
|
|
2002
|
+
const handler = atLine?.handler ?? this.exportedFunctionOpeningOn(sourceFile, request.line_number);
|
|
2003
|
+
if (!handler) {
|
|
2004
|
+
return null;
|
|
2005
|
+
}
|
|
2006
|
+
const read = this.platformBodyReadIn(handler);
|
|
2007
|
+
const stated = read ? this.requestStatedAtBodyRead(request, read) : null;
|
|
2008
|
+
if (stated) {
|
|
2009
|
+
return stated;
|
|
2010
|
+
}
|
|
2011
|
+
// The first typed read anywhere in the handler is a looser reading, and
|
|
2012
|
+
// is kept only where a registration already had it.
|
|
2013
|
+
const typedRead = atLine ? this.inferRequestReadFromHandler(atLine.handler) : null;
|
|
2014
|
+
return typedRead
|
|
2015
|
+
? this.declaredRequestInferredType(request, { text: typedRead }, this.getNodeLocation(handler))
|
|
2016
|
+
: null;
|
|
1703
2017
|
}
|
|
1704
2018
|
// Inline-handler registration (`app.post('/x', async (c) => { … })`) or a
|
|
1705
2019
|
// route-registry object literal: the locator lands on the registration, not
|
|
@@ -1973,6 +2287,21 @@ export class TypeInferrer {
|
|
|
1973
2287
|
return { terminal: returnExpr, projectionOnly: false, projections: [] };
|
|
1974
2288
|
}
|
|
1975
2289
|
}
|
|
2290
|
+
// carrick#1851: the source reads the body in place on the call's own
|
|
2291
|
+
// value, `const body = await (await fetch(url)).text()`. No binding holds
|
|
2292
|
+
// the response, so the walk below has nothing to follow, and the call
|
|
2293
|
+
// itself would stand as the terminal and publish the transport object.
|
|
2294
|
+
// The read is the terminal, as it is when the walk finds it on a binding,
|
|
2295
|
+
// and a call that takes the read and states what it returns says more
|
|
2296
|
+
// than the read does (carrick#1382).
|
|
2297
|
+
const inPlaceRead = this.bodyReadOnCallValue(callExpr);
|
|
2298
|
+
if (inPlaceRead) {
|
|
2299
|
+
return {
|
|
2300
|
+
terminal: this.statedPayloadAroundBodyRead(inPlaceRead) ?? inPlaceRead,
|
|
2301
|
+
projectionOnly: false,
|
|
2302
|
+
projections: [],
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
1976
2305
|
const binding = this.extractBindingFromCall(callExpr);
|
|
1977
2306
|
if (binding && func) {
|
|
1978
2307
|
let currentNames = binding.names;
|
|
@@ -2134,36 +2463,11 @@ export class TypeInferrer {
|
|
|
2134
2463
|
* reader can see is an envelope.
|
|
2135
2464
|
*/
|
|
2136
2465
|
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) {
|
|
2466
|
+
const shape = this.resultCarrierArguments(type, at);
|
|
2467
|
+
if (!shape) {
|
|
2165
2468
|
return undefined;
|
|
2166
2469
|
}
|
|
2470
|
+
const { carrier, carried } = shape;
|
|
2167
2471
|
const argTexts = new Map(carried.map((arg) => [arg.getText(), arg]));
|
|
2168
2472
|
const read = new Set();
|
|
2169
2473
|
for (const projection of projections) {
|
|
@@ -2198,12 +2502,102 @@ export class TypeInferrer {
|
|
|
2198
2502
|
return undefined;
|
|
2199
2503
|
}
|
|
2200
2504
|
/**
|
|
2201
|
-
*
|
|
2202
|
-
*
|
|
2203
|
-
*
|
|
2204
|
-
|
|
2505
|
+
* The shape test of `resultCarrierPayload`: the carrier `type` is, once a
|
|
2506
|
+
* promise-like around it is peeled, and the type arguments a branch of it
|
|
2507
|
+
* holds as a member. `undefined` when `type` is not a carrier.
|
|
2508
|
+
*/
|
|
2509
|
+
resultCarrierArguments(type, at) {
|
|
2510
|
+
const carrier = this.unwrapThenableType(this.unwrapPromiseType(type));
|
|
2511
|
+
if (!carrier.isUnion()) {
|
|
2512
|
+
return undefined;
|
|
2513
|
+
}
|
|
2514
|
+
const branches = carrier.getUnionTypes();
|
|
2515
|
+
if (branches.length < 2 || !branches.every((branch) => this.isObjectShape(branch))) {
|
|
2516
|
+
return undefined;
|
|
2517
|
+
}
|
|
2518
|
+
const args = [
|
|
2519
|
+
...carrier.getAliasTypeArguments(),
|
|
2520
|
+
...carrier.getTypeArguments(),
|
|
2521
|
+
];
|
|
2522
|
+
if (args.length < 2) {
|
|
2523
|
+
return undefined;
|
|
2524
|
+
}
|
|
2525
|
+
// A type argument only names a payload when a branch actually holds it:
|
|
2526
|
+
// a generic that parameterises a status code or a key carries nothing.
|
|
2527
|
+
const carried = args.filter((arg) => branches.some((branch) => branch
|
|
2528
|
+
.getProperties()
|
|
2529
|
+
.some((property) => {
|
|
2530
|
+
try {
|
|
2531
|
+
return property.getTypeAtLocation(at).getText() === arg.getText();
|
|
2532
|
+
}
|
|
2533
|
+
catch {
|
|
2534
|
+
return false;
|
|
2535
|
+
}
|
|
2536
|
+
})));
|
|
2537
|
+
return carried.length === 0 ? undefined : { carrier, carried };
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* The decided abstain of a call whose result carries transport the
|
|
2541
|
+
* service's wrapper rules verify and read no payload out of (carrick#1841,
|
|
2542
|
+
* carrick#1843): `unknown` with `machinery_envelope` at the root and no
|
|
2543
|
+
* anchor. The root reason is what keeps the capture's own locator from
|
|
2544
|
+
* re-reading the raw call (`inference_decided_no_contract`,
|
|
2545
|
+
* engine/type_compat_v2.rs).
|
|
2546
|
+
*/
|
|
2547
|
+
transportAbstain(request, callExpr) {
|
|
2548
|
+
const abstain = this.createInferredType(request, 'unknown', false, this.getNodeLocation(callExpr));
|
|
2549
|
+
abstain.any_provenance = [
|
|
2550
|
+
{
|
|
2551
|
+
path: '',
|
|
2552
|
+
kind: 'unknown',
|
|
2553
|
+
reason: 'machinery_envelope',
|
|
2554
|
+
detail: "what this call's result carries is transport that the service's wrapper rules " +
|
|
2555
|
+
'verify and read no payload out of (a library response object around the body), ' +
|
|
2556
|
+
'so this site states no response contract',
|
|
2557
|
+
},
|
|
2558
|
+
];
|
|
2559
|
+
return abstain;
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* `Future<T>` -> `T` for a thenable, read off the await protocol rather
|
|
2563
|
+
* than a name: a `then` whose first parameter is a callback, whose own
|
|
2564
|
+
* first parameter is the value awaiting it yields. `Promise` and
|
|
2565
|
+
* `PromiseLike` are peeled by `unwrapPromiseType` before this.
|
|
2566
|
+
*
|
|
2567
|
+
* The callback is read through `null` and `undefined` (carrick#1877). A
|
|
2568
|
+
* `then` of the source's own making declares `(value: T) => void`; the
|
|
2569
|
+
* platform's declares `onfulfilled?: ((value: T) => ...) | null`, and that
|
|
2570
|
+
* is the `then` a subclass of `Promise` inherits. Read as written, an
|
|
2571
|
+
* optional, nullable callback has no call signature, and the subclass was
|
|
2572
|
+
* not seen as a thenable at all.
|
|
2573
|
+
*
|
|
2574
|
+
* A `then` that cannot be called, or whose first parameter is no callback,
|
|
2575
|
+
* is no protocol, and the type is returned as it is.
|
|
2576
|
+
*
|
|
2577
|
+
* The compiler's own awaited type arbitrates. This walk reads the first
|
|
2578
|
+
* signature of `then`; the language reads all of them. Where awaiting the
|
|
2579
|
+
* type and awaiting what this walk found are not the same thing to the
|
|
2580
|
+
* compiler (an overloaded `then` whose first signature is not the one
|
|
2581
|
+
* `await` takes), the walk did not read the protocol, and the type is
|
|
2582
|
+
* returned as it is rather than published as a guess.
|
|
2205
2583
|
*/
|
|
2206
2584
|
unwrapThenableType(type) {
|
|
2585
|
+
const yielded = this.firstThenValue(type);
|
|
2586
|
+
if (yielded === type)
|
|
2587
|
+
return type;
|
|
2588
|
+
try {
|
|
2589
|
+
const checker = this.project.getTypeChecker().compilerObject;
|
|
2590
|
+
const awaited = checker.getAwaitedType(type.compilerType);
|
|
2591
|
+
return awaited !== undefined && awaited === checker.getAwaitedType(yielded.compilerType)
|
|
2592
|
+
? yielded
|
|
2593
|
+
: type;
|
|
2594
|
+
}
|
|
2595
|
+
catch {
|
|
2596
|
+
return type;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
/** The value `then`'s first signature hands its callback, read until it stops changing. */
|
|
2600
|
+
firstThenValue(type) {
|
|
2207
2601
|
let current = type;
|
|
2208
2602
|
for (let depth = 0; depth < 8; depth++) {
|
|
2209
2603
|
const then = current.getProperty('then');
|
|
@@ -2216,7 +2610,8 @@ export class TypeInferrer {
|
|
|
2216
2610
|
.getTypeAtLocation(declaration)
|
|
2217
2611
|
.getCallSignatures()[0]
|
|
2218
2612
|
?.getParameters()[0]
|
|
2219
|
-
?.getTypeAtLocation(declaration)
|
|
2613
|
+
?.getTypeAtLocation(declaration)
|
|
2614
|
+
.getNonNullableType();
|
|
2220
2615
|
}
|
|
2221
2616
|
catch {
|
|
2222
2617
|
return current;
|
|
@@ -2231,6 +2626,18 @@ export class TypeInferrer {
|
|
|
2231
2626
|
}
|
|
2232
2627
|
return current;
|
|
2233
2628
|
}
|
|
2629
|
+
/**
|
|
2630
|
+
* What `await` yields for `type` where the names do not say: `type` is,
|
|
2631
|
+
* once `Promise` and `PromiseLike` are peeled, a thenable by the protocol
|
|
2632
|
+
* (a subclass of `Promise`, a class with a `then` of its own). `undefined`
|
|
2633
|
+
* where the names say it all or `type` is no thenable, so a caller keeps
|
|
2634
|
+
* the reading it had (carrick#1877).
|
|
2635
|
+
*/
|
|
2636
|
+
awaitedBeyondPromise(type) {
|
|
2637
|
+
const named = this.unwrapPromiseType(type);
|
|
2638
|
+
const yielded = this.unwrapThenableType(named);
|
|
2639
|
+
return yielded === named ? undefined : yielded;
|
|
2640
|
+
}
|
|
2234
2641
|
/**
|
|
2235
2642
|
* The platform's error shape, in full: `name` and `message` strings AND a
|
|
2236
2643
|
* `stack`, which is what the `Error` interface declares and every subclass
|
|
@@ -2513,16 +2920,40 @@ export class TypeInferrer {
|
|
|
2513
2920
|
});
|
|
2514
2921
|
}
|
|
2515
2922
|
/**
|
|
2516
|
-
* The zero-argument whole-body read
|
|
2923
|
+
* The zero-argument whole-body read taken in place on the value `callExpr`
|
|
2924
|
+
* yields, or `undefined` (carrick#1851): `(await fetch(url)).text()`. The
|
|
2925
|
+
* receiver is the call itself, through the wrappers that leave a value as
|
|
2926
|
+
* it is (parentheses, `await`, `!`), so it is the same read
|
|
2927
|
+
* `bodyReadOnReceiver` finds on a binding of that value. A call that is not
|
|
2928
|
+
* awaited first is read the same way: a request that is a promise and reads
|
|
2929
|
+
* its own body (`send(url).json()`) yields the body from that read too.
|
|
2930
|
+
*/
|
|
2931
|
+
bodyReadOnCallValue(callExpr) {
|
|
2932
|
+
let value = callExpr;
|
|
2933
|
+
for (;;) {
|
|
2934
|
+
const parent = value.getParent();
|
|
2935
|
+
if (!parent ||
|
|
2936
|
+
!(Node.isParenthesizedExpression(parent) ||
|
|
2937
|
+
Node.isAwaitExpression(parent) ||
|
|
2938
|
+
Node.isNonNullExpression(parent)) ||
|
|
2939
|
+
parent.getExpression() !== value) {
|
|
2940
|
+
break;
|
|
2941
|
+
}
|
|
2942
|
+
value = parent;
|
|
2943
|
+
}
|
|
2944
|
+
return this.bodyReadOnReceiver(value);
|
|
2945
|
+
}
|
|
2946
|
+
/**
|
|
2947
|
+
* The zero-argument whole-body read that takes `receiver` as its receiver,
|
|
2517
2948
|
* `res.json()` or `res.text()`, or `undefined`. A text read is a body read
|
|
2518
2949
|
* like a json one (carrick#1842): without it, `return res.text()` left the
|
|
2519
2950
|
* walk on the response binding and published the transport object.
|
|
2520
2951
|
*/
|
|
2521
|
-
bodyReadOnReceiver(
|
|
2522
|
-
const access =
|
|
2952
|
+
bodyReadOnReceiver(receiver) {
|
|
2953
|
+
const access = receiver.getParent();
|
|
2523
2954
|
if (!access ||
|
|
2524
2955
|
!Node.isPropertyAccessExpression(access) ||
|
|
2525
|
-
access.getExpression() !==
|
|
2956
|
+
access.getExpression() !== receiver ||
|
|
2526
2957
|
!WHOLE_BODY_READS.has(access.getName())) {
|
|
2527
2958
|
return undefined;
|
|
2528
2959
|
}
|
|
@@ -2787,26 +3218,31 @@ export class TypeInferrer {
|
|
|
2787
3218
|
if (depth >= maxDepth) {
|
|
2788
3219
|
return { kind: 'no-match' };
|
|
2789
3220
|
}
|
|
2790
|
-
const
|
|
2791
|
-
|
|
2792
|
-
//
|
|
2793
|
-
//
|
|
2794
|
-
//
|
|
2795
|
-
//
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
3221
|
+
const readings = this.wrapperReadings(type);
|
|
3222
|
+
// 1. Check exact wrapperSymbols match, against either name the type goes
|
|
3223
|
+
// by (carrick#1843). When the rule also carries originModuleGlobs, the
|
|
3224
|
+
// declaration of the name that matched must come from a matching module
|
|
3225
|
+
// — names like `Response` are shared by the DOM, frameworks, and HTTP
|
|
3226
|
+
// clients, so a bare name match would unwrap unrelated types. A name
|
|
3227
|
+
// that fails that gate is no match, and the rule's test of members and
|
|
3228
|
+
// origin below still has its turn: a service's alias that shares the
|
|
3229
|
+
// rule's name can stand around the very type the rule verifies.
|
|
3230
|
+
const named = readings.filter((reading) => rule.wrapperSymbols?.includes(reading.symbol.getName()));
|
|
3231
|
+
const globs = rule.originModuleGlobs ?? [];
|
|
3232
|
+
const originGated = globs.length > 0;
|
|
3233
|
+
const matched = originGated
|
|
3234
|
+
? named.find((reading) => this.symbolOriginatesFromModules(reading.symbol, globs))
|
|
3235
|
+
: named[0];
|
|
3236
|
+
if (matched) {
|
|
3237
|
+
const extracted = this.extractPayloadFromWrapper(type, matched, node, rule, config, depth);
|
|
3238
|
+
if (extracted) {
|
|
3239
|
+
return { kind: 'extracted', result: extracted };
|
|
2808
3240
|
}
|
|
2809
|
-
|
|
3241
|
+
// A name-only match is not proof of machinery: a local type that
|
|
3242
|
+
// happens to share the name must keep its real structural type when
|
|
3243
|
+
// nothing was extracted. Only origin-verified matches may collapse
|
|
3244
|
+
// to `unknown`.
|
|
3245
|
+
return originGated ? { kind: 'verified-no-payload' } : { kind: 'no-match' };
|
|
2810
3246
|
}
|
|
2811
3247
|
// 2. Check machineryIndicators + originModuleGlobs. Indicators alone are
|
|
2812
3248
|
// too many false positives, so the origin gate is mandatory here — which
|
|
@@ -2818,10 +3254,18 @@ export class TypeInferrer {
|
|
|
2818
3254
|
if (!this.typeHasMachineryIndicators(type, rule.machineryIndicators)) {
|
|
2819
3255
|
return { kind: 'no-match' };
|
|
2820
3256
|
}
|
|
2821
|
-
|
|
3257
|
+
// The rule named neither of the type's names, so this branch reads
|
|
3258
|
+
// what it always read: the first symbol the type has, and the type's
|
|
3259
|
+
// own arguments. An alias the rule did not name says nothing about
|
|
3260
|
+
// where a payload is, whoever declares it (carrick#1843):
|
|
3261
|
+
// `Omit<Response, 'json'>` is written through a library alias whose
|
|
3262
|
+
// first argument is the transport itself, and a service's own alias
|
|
3263
|
+
// around a library's object orders its parameters as it likes.
|
|
3264
|
+
const symbol = type.getSymbol() || type.getAliasSymbol();
|
|
3265
|
+
if (!symbol || !this.symbolOriginatesFromModules(symbol, rule.originModuleGlobs)) {
|
|
2822
3266
|
return { kind: 'no-match' };
|
|
2823
3267
|
}
|
|
2824
|
-
const extracted = this.extractPayloadFromWrapper(type, node, rule, config, depth);
|
|
3268
|
+
const extracted = this.extractPayloadFromWrapper(type, { symbol, typeArguments: type.getTypeArguments(), viaAlias: false }, node, rule, config, depth);
|
|
2825
3269
|
if (extracted) {
|
|
2826
3270
|
return { kind: 'extracted', result: extracted };
|
|
2827
3271
|
}
|
|
@@ -2829,14 +3273,44 @@ export class TypeInferrer {
|
|
|
2829
3273
|
}
|
|
2830
3274
|
return { kind: 'no-match' };
|
|
2831
3275
|
}
|
|
3276
|
+
/**
|
|
3277
|
+
* The names `type` goes by, own symbol first (carrick#1843).
|
|
3278
|
+
*
|
|
3279
|
+
* `type Task<A> = __Task<A>` has the class's symbol and arguments, and the
|
|
3280
|
+
* alias's beside them. `type Reply<T> = { ... }` has the anonymous `__type`
|
|
3281
|
+
* with no arguments of its own. `type Outcome<A, E> = Done<A, E> |
|
|
3282
|
+
* Failed<A, E>` has no symbol of its own at all. In each, the alias and its
|
|
3283
|
+
* arguments are what a rule naming `Task`, `Reply` or `Outcome` describes.
|
|
3284
|
+
*/
|
|
3285
|
+
wrapperReadings(type) {
|
|
3286
|
+
const readings = [];
|
|
3287
|
+
const own = type.getSymbol();
|
|
3288
|
+
if (own) {
|
|
3289
|
+
readings.push({ symbol: own, typeArguments: type.getTypeArguments(), viaAlias: false });
|
|
3290
|
+
}
|
|
3291
|
+
const alias = type.getAliasSymbol();
|
|
3292
|
+
if (alias && alias !== own) {
|
|
3293
|
+
readings.push({
|
|
3294
|
+
symbol: alias,
|
|
3295
|
+
typeArguments: type.getAliasTypeArguments(),
|
|
3296
|
+
viaAlias: true,
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
return readings;
|
|
3300
|
+
}
|
|
2832
3301
|
/**
|
|
2833
3302
|
* Extract the payload type from a matched wrapper. Returns null when the
|
|
2834
3303
|
* rule matched the wrapper but no payload is recoverable from generics or
|
|
2835
3304
|
* property paths — the caller decides what a payload-less match means
|
|
2836
3305
|
* (verified machinery collapses to `unknown` after every rule has run;
|
|
2837
3306
|
* a name-only match leaves the type untouched).
|
|
3307
|
+
*
|
|
3308
|
+
* `payloadGenericIndex` counts the arguments of `reading`, the name the
|
|
3309
|
+
* rule matched (carrick#1843): an alias is free to order its parameters
|
|
3310
|
+
* differently from the type it stands for, so the same index into the
|
|
3311
|
+
* other list is a different argument.
|
|
2838
3312
|
*/
|
|
2839
|
-
extractPayloadFromWrapper(type, node, rule, config, depth) {
|
|
3313
|
+
extractPayloadFromWrapper(type, reading, node, rule, config, depth) {
|
|
2840
3314
|
// The outer extraction already succeeded on the paths below; a recursive
|
|
2841
3315
|
// inner pass that finds nothing more must not demote the result back to
|
|
2842
3316
|
// "not unwrapped" (which would discard the recovered payload). Only when
|
|
@@ -2855,7 +3329,7 @@ export class TypeInferrer {
|
|
|
2855
3329
|
};
|
|
2856
3330
|
// 1. Try generic type argument at payloadGenericIndex
|
|
2857
3331
|
const genericIndex = rule.payloadGenericIndex ?? 0;
|
|
2858
|
-
const typeArgs =
|
|
3332
|
+
const typeArgs = reading.typeArguments;
|
|
2859
3333
|
if (typeArgs.length > genericIndex) {
|
|
2860
3334
|
const payloadArg = typeArgs[genericIndex];
|
|
2861
3335
|
// Check if it's a useful type (not any/unknown/never)
|
|
@@ -2872,8 +3346,11 @@ export class TypeInferrer {
|
|
|
2872
3346
|
payloadType: payloadArg,
|
|
2873
3347
|
};
|
|
2874
3348
|
}
|
|
2875
|
-
// Try "first useful generic" heuristic
|
|
2876
|
-
|
|
3349
|
+
// Try "first useful generic" heuristic. It is a guess about a type's
|
|
3350
|
+
// own arguments and is not extended to an alias's (carrick#1843): the
|
|
3351
|
+
// alias this reaches most often is a result carrier, `Outcome<A, E>`,
|
|
3352
|
+
// whose next argument after an open payload is the error side.
|
|
3353
|
+
for (let i = 0; !reading.viaAlias && i < typeArgs.length; i++) {
|
|
2877
3354
|
const argType = typeArgs[i];
|
|
2878
3355
|
const text = typeText(argType, node);
|
|
2879
3356
|
if (!this.isUselessType(text)) {
|
|
@@ -3390,10 +3867,15 @@ export class TypeInferrer {
|
|
|
3390
3867
|
* stated status, and what survives is joined as a union exactly as several
|
|
3391
3868
|
* return statements are.
|
|
3392
3869
|
*/
|
|
3393
|
-
recoverPayloadFromResponseExpressions(expressions, statedOnly, wire) {
|
|
3870
|
+
recoverPayloadFromResponseExpressions(expressions, statedOnly, wire, alsoSerialisers) {
|
|
3394
3871
|
const candidates = [];
|
|
3395
3872
|
const returned = expressions.flatMap((expression) => this.expandResponseBranches(expression, 0));
|
|
3873
|
+
// `alsoSerialisers` are callees a caller has its own evidence for
|
|
3874
|
+
// (`calleeIdentity` keys), beside the ones these expressions prove.
|
|
3396
3875
|
const serialisers = this.calleesProvenSerialiser(returned);
|
|
3876
|
+
for (const identity of alsoSerialisers ?? []) {
|
|
3877
|
+
serialisers.add(identity);
|
|
3878
|
+
}
|
|
3397
3879
|
for (const expression of returned) {
|
|
3398
3880
|
const payloadNode = this.responseHelperPayloadNode(expression, 0, statedOnly, serialisers);
|
|
3399
3881
|
if (!payloadNode)
|
|
@@ -3646,8 +4128,11 @@ export class TypeInferrer {
|
|
|
3646
4128
|
*
|
|
3647
4129
|
* Under `statedOnly` the annotation is the ONLY thing that counts, so an
|
|
3648
4130
|
* unresolvable callee's arguments never become a contract by accident.
|
|
4131
|
+
*
|
|
4132
|
+
* `asSent` judges the object in the form it is sent in, which is what an
|
|
4133
|
+
* ARGUMENT handed to a sender is asked; see `typeIsObjectShaped`.
|
|
3649
4134
|
*/
|
|
3650
|
-
nodeCarriesPayloadContract(node, statedOnly) {
|
|
4135
|
+
nodeCarriesPayloadContract(node, statedOnly, asSent = true) {
|
|
3651
4136
|
if (this.statedTypeNodeOf(node))
|
|
3652
4137
|
return true;
|
|
3653
4138
|
if (statedOnly)
|
|
@@ -3665,24 +4150,37 @@ export class TypeInferrer {
|
|
|
3665
4150
|
return false;
|
|
3666
4151
|
if (this.typeIsOrContainsResponseMachinery(type))
|
|
3667
4152
|
return false;
|
|
3668
|
-
return this.typeIsObjectShaped(type, 0);
|
|
4153
|
+
return this.typeIsObjectShaped(type, 0, asSent);
|
|
3669
4154
|
}
|
|
3670
|
-
/**
|
|
3671
|
-
|
|
4155
|
+
/**
|
|
4156
|
+
* Object, array-of-object, or a union/intersection containing one.
|
|
4157
|
+
*
|
|
4158
|
+
* With `asSent` the object is judged in the form it is SENT in
|
|
4159
|
+
* (carrick#1163): a value that declares `toJSON()` travels as what that
|
|
4160
|
+
* returns. A `URL` or a `Date` is therefore the string it serialises to,
|
|
4161
|
+
* the bare primitive this rule already refuses, and `redirect(new URL(path,
|
|
4162
|
+
* base))` hands over a location exactly as `redirect("/next")` does
|
|
4163
|
+
* (carrick#807). A value whose JSON form is itself an object is a body like
|
|
4164
|
+
* any other.
|
|
4165
|
+
*/
|
|
4166
|
+
typeIsObjectShaped(type, depth, asSent) {
|
|
3672
4167
|
if (depth > 2)
|
|
3673
4168
|
return false;
|
|
3674
4169
|
if (type.isUnion()) {
|
|
3675
|
-
return type.getUnionTypes().some((m) => this.typeIsObjectShaped(m, depth + 1));
|
|
4170
|
+
return type.getUnionTypes().some((m) => this.typeIsObjectShaped(m, depth + 1, asSent));
|
|
3676
4171
|
}
|
|
3677
4172
|
if (type.isIntersection()) {
|
|
3678
4173
|
return type
|
|
3679
4174
|
.getIntersectionTypes()
|
|
3680
|
-
.some((m) => this.typeIsObjectShaped(m, depth + 1));
|
|
4175
|
+
.some((m) => this.typeIsObjectShaped(m, depth + 1, asSent));
|
|
3681
4176
|
}
|
|
3682
4177
|
if (type.isArray()) {
|
|
3683
4178
|
const element = type.getArrayElementType();
|
|
3684
|
-
return element ? this.typeIsObjectShaped(element, depth + 1) : false;
|
|
4179
|
+
return element ? this.typeIsObjectShaped(element, depth + 1, asSent) : false;
|
|
3685
4180
|
}
|
|
4181
|
+
const sent = asSent ? jsonWireType(type) : undefined;
|
|
4182
|
+
if (sent)
|
|
4183
|
+
return this.typeIsObjectShaped(sent, depth + 1, asSent);
|
|
3686
4184
|
return type.isObject() && !type.isTuple();
|
|
3687
4185
|
}
|
|
3688
4186
|
/**
|
|
@@ -3927,11 +4425,19 @@ export class TypeInferrer {
|
|
|
3927
4425
|
/**
|
|
3928
4426
|
* Declaration file (absolute path) of the anchor symbol
|
|
3929
4427
|
* `primaryTypeSymbol` reports for this type, or `undefined` when the type
|
|
3930
|
-
* has no user-facing anchor or no source declaration.
|
|
3931
|
-
*
|
|
3932
|
-
*
|
|
3933
|
-
*
|
|
3934
|
-
*
|
|
4428
|
+
* has no user-facing anchor or no source declaration. Every path that
|
|
4429
|
+
* reports the symbol reports this beside it (carrick#1819): it is where a
|
|
4430
|
+
* reader finds the type an anchor names. The scanner's pub/sub two-anchor
|
|
4431
|
+
* arbitration (carrick#413) also uses it to re-aim a demoted explicit
|
|
4432
|
+
* bundle request: the bundler resolves a `SymbolRequest` only against
|
|
4433
|
+
* declarations IN its `source_file`, so the request must point at the file
|
|
4434
|
+
* that actually declares the tsc-witnessed payload type.
|
|
4435
|
+
*
|
|
4436
|
+
* The declarations read are those of the type's own symbol, so a name
|
|
4437
|
+
* imported through a barrel reports the file that declares it, and a name
|
|
4438
|
+
* two files declare reports the one this type resolves to. A declaration in
|
|
4439
|
+
* an installed package or a TypeScript lib is reported as it is found, as
|
|
4440
|
+
* an absolute path; what a reader is shown for it is the scanner's call.
|
|
3935
4441
|
*
|
|
3936
4442
|
* Only declaration kinds the bundler's `validateSymbols` can resolve
|
|
3937
4443
|
* (interface, type alias, class, enum, function, variable) count. A
|
|
@@ -4251,44 +4757,7 @@ export class TypeInferrer {
|
|
|
4251
4757
|
* which is the honest answer and the one a consumer check can act on.
|
|
4252
4758
|
*/
|
|
4253
4759
|
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;
|
|
4760
|
+
return functionAtLine(sourceFile, line);
|
|
4292
4761
|
}
|
|
4293
4762
|
/**
|
|
4294
4763
|
* Walk up from a node to find its innermost containing function.
|
|
@@ -4561,6 +5030,98 @@ export class TypeInferrer {
|
|
|
4561
5030
|
});
|
|
4562
5031
|
return found;
|
|
4563
5032
|
}
|
|
5033
|
+
/**
|
|
5034
|
+
* The call in a handler that reads the request's body off the platform
|
|
5035
|
+
* request the handler was handed, or `undefined` (carrick#807).
|
|
5036
|
+
*
|
|
5037
|
+
* Read by shape, with no method name consulted:
|
|
5038
|
+
*
|
|
5039
|
+
* - a call that takes nothing, on a member of a value whose type is request
|
|
5040
|
+
* machinery (`typeIsFrameworkMachinery`: declared by the platform or an
|
|
5041
|
+
* installed library, and carrying its body readers);
|
|
5042
|
+
* - that value is rooted at one of the handler's OWN parameters, named or
|
|
5043
|
+
* destructured. A response read off an outbound call inside the handler
|
|
5044
|
+
* (`(await upstream.json()) as Rate`) has the same shape one variable
|
|
5045
|
+
* away, and is the opposite side of a different exchange;
|
|
5046
|
+
* - and the call's result, awaited, is `any` or `unknown`: the platform's
|
|
5047
|
+
* untyped parse. A read that states its own type (`formData()`, a typed
|
|
5048
|
+
* `json<T>()`) is not this shape and is left to the readers that
|
|
5049
|
+
* already handle it.
|
|
5050
|
+
*
|
|
5051
|
+
* The first such call in source order: a body is read once.
|
|
5052
|
+
*/
|
|
5053
|
+
platformBodyReadIn(handler) {
|
|
5054
|
+
const parameters = new Set(handler.getParameters());
|
|
5055
|
+
const rootedAtParameter = (expression) => {
|
|
5056
|
+
let root = expression;
|
|
5057
|
+
while (Node.isPropertyAccessExpression(root) ||
|
|
5058
|
+
Node.isNonNullExpression(root) ||
|
|
5059
|
+
Node.isParenthesizedExpression(root)) {
|
|
5060
|
+
root = root.getExpression();
|
|
5061
|
+
}
|
|
5062
|
+
if (!Node.isIdentifier(root))
|
|
5063
|
+
return false;
|
|
5064
|
+
return (root.getSymbol()?.getDeclarations() ?? []).some((declaration) => {
|
|
5065
|
+
const parameter = Node.isParameterDeclaration(declaration)
|
|
5066
|
+
? declaration
|
|
5067
|
+
: Node.isBindingElement(declaration)
|
|
5068
|
+
? declaration.getFirstAncestorByKind(SyntaxKind.Parameter)
|
|
5069
|
+
: undefined;
|
|
5070
|
+
return parameter !== undefined && parameters.has(parameter);
|
|
5071
|
+
});
|
|
5072
|
+
};
|
|
5073
|
+
for (const call of handler.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
5074
|
+
if (call.getArguments().length > 0)
|
|
5075
|
+
continue;
|
|
5076
|
+
const callee = call.getExpression();
|
|
5077
|
+
if (!Node.isPropertyAccessExpression(callee))
|
|
5078
|
+
continue;
|
|
5079
|
+
const receiver = callee.getExpression();
|
|
5080
|
+
if (!rootedAtParameter(receiver))
|
|
5081
|
+
continue;
|
|
5082
|
+
const yielded = this.unwrapPromiseType(call.getType());
|
|
5083
|
+
if (!yielded.isAny() && !yielded.isUnknown())
|
|
5084
|
+
continue;
|
|
5085
|
+
if (!this.typeIsFrameworkMachinery(receiver.getType()))
|
|
5086
|
+
continue;
|
|
5087
|
+
return call;
|
|
5088
|
+
}
|
|
5089
|
+
return undefined;
|
|
5090
|
+
}
|
|
5091
|
+
/**
|
|
5092
|
+
* The request contract the source states AT a body read, or null when it
|
|
5093
|
+
* states none there (carrick#807).
|
|
5094
|
+
*
|
|
5095
|
+
* Three statements count, each of them on the read itself:
|
|
5096
|
+
*
|
|
5097
|
+
* - a cast on it: `(await request.json()) as NewWidget`;
|
|
5098
|
+
* - the annotation of the binding it initialises:
|
|
5099
|
+
* `const input: NewWidget = await request.json()`;
|
|
5100
|
+
* - a schema the read, or the binding that holds it, is handed to
|
|
5101
|
+
* (`schemaConsumingRead`): the schema's input is what a caller may send.
|
|
5102
|
+
*
|
|
5103
|
+
* The first two are `typeNodeStatedAtRead`, the reading a consumer's
|
|
5104
|
+
* response read already gets. An annotation further out is not one of them:
|
|
5105
|
+
* `const saved: Saved = await save(await request.json())` types what `save`
|
|
5106
|
+
* returned, and reading it as the body would publish the wrong side of the
|
|
5107
|
+
* handler. Nor is a placeholder (`as unknown`, `Record<string, unknown>`).
|
|
5108
|
+
*/
|
|
5109
|
+
requestStatedAtBodyRead(request, read) {
|
|
5110
|
+
const statedNode = this.typeNodeStatedAtRead(read);
|
|
5111
|
+
const statedText = statedNode ? this.structuralTextFromTypeNode(statedNode) : null;
|
|
5112
|
+
if (statedText) {
|
|
5113
|
+
this.log(`Request at ${request.file_path}:${request.line_number} is the body its handler ` +
|
|
5114
|
+
'reads off the platform request, as the source types that read');
|
|
5115
|
+
return this.createInferredType(request, statedText, true, this.getNodeLocation(read));
|
|
5116
|
+
}
|
|
5117
|
+
const validated = this.schemaConsumingRead(read);
|
|
5118
|
+
if (validated) {
|
|
5119
|
+
this.log(`Request at ${request.file_path}:${request.line_number} is the body its handler ` +
|
|
5120
|
+
"reads off the platform request and validates with a schema; publishing the schema's input");
|
|
5121
|
+
return this.declaredRequestInferredType(request, validated, this.getNodeLocation(read));
|
|
5122
|
+
}
|
|
5123
|
+
return null;
|
|
5124
|
+
}
|
|
4564
5125
|
/**
|
|
4565
5126
|
* Resolve a type-annotation/type-argument node to fully-structural text,
|
|
4566
5127
|
* dropping any `Promise<…>` wrapper (`c.req.json<T>()` returns `Promise<T>`,
|
|
@@ -5013,6 +5574,14 @@ export class TypeInferrer {
|
|
|
5013
5574
|
* request type resolves `body` to `unknown`/`any`, which the useless-type
|
|
5014
5575
|
* guard rejects. So a handler that declares nothing yields null and the next
|
|
5015
5576
|
* anchor runs.
|
|
5577
|
+
*
|
|
5578
|
+
* The member has to be one the route's own annotation can have filled
|
|
5579
|
+
* (carrick#807). The platform `Request` declares `body` too, as the byte
|
|
5580
|
+
* stream every request carries, and so does each library type that extends
|
|
5581
|
+
* it. That is concrete, so it passed the useless-type guard and a handler
|
|
5582
|
+
* taking the platform request published a stream as its request contract,
|
|
5583
|
+
* ahead of the body read inside it. A `body` a library declares with one
|
|
5584
|
+
* fixed type says nothing about this route and is skipped.
|
|
5016
5585
|
*/
|
|
5017
5586
|
requestBodyFromHandlerParams(func) {
|
|
5018
5587
|
for (const param of func.getParameters()) {
|
|
@@ -5024,7 +5593,7 @@ export class TypeInferrer {
|
|
|
5024
5593
|
continue;
|
|
5025
5594
|
}
|
|
5026
5595
|
const bodySymbol = paramType.getProperty('body');
|
|
5027
|
-
if (!bodySymbol) {
|
|
5596
|
+
if (!bodySymbol || this.memberIsFixedByItsLibrary(bodySymbol)) {
|
|
5028
5597
|
continue;
|
|
5029
5598
|
}
|
|
5030
5599
|
let bodyType;
|
|
@@ -5041,6 +5610,41 @@ export class TypeInferrer {
|
|
|
5041
5610
|
}
|
|
5042
5611
|
return null;
|
|
5043
5612
|
}
|
|
5613
|
+
/**
|
|
5614
|
+
* True when a member is declared by a library, or by the platform, with a
|
|
5615
|
+
* type that names none of its declaring type's parameters: the same type on
|
|
5616
|
+
* every value, whichever route the value belongs to.
|
|
5617
|
+
*
|
|
5618
|
+
* `body: ReqBody` on a request type generic in its body is a slot, and the
|
|
5619
|
+
* handler's annotation fills it. `readonly body: ReadableStream<Uint8Array>
|
|
5620
|
+
* | null` on the platform request is not. A member the repo declares itself
|
|
5621
|
+
* is never fixed in this sense: writing `body: NewWidget` on the handler's
|
|
5622
|
+
* own request type is the annotation, and one declaration of the repo's
|
|
5623
|
+
* among several (an intersection with the platform type) decides it. A
|
|
5624
|
+
* declaration that states no type at all is left to the guards that read
|
|
5625
|
+
* the type.
|
|
5626
|
+
*/
|
|
5627
|
+
memberIsFixedByItsLibrary(member) {
|
|
5628
|
+
const declarations = member.getDeclarations();
|
|
5629
|
+
if (declarations.length === 0)
|
|
5630
|
+
return false;
|
|
5631
|
+
const program = this.project.getProgram().compilerObject;
|
|
5632
|
+
const imports = externalImportsOf(this.project);
|
|
5633
|
+
return declarations.every((declaration) => {
|
|
5634
|
+
const declaredByLibrary = isExternalOrigin(program, declaration.getSourceFile().compilerNode, this.repoRoot, imports);
|
|
5635
|
+
if (!declaredByLibrary)
|
|
5636
|
+
return false;
|
|
5637
|
+
const typeNode = Node.isPropertySignature(declaration) || Node.isPropertyDeclaration(declaration)
|
|
5638
|
+
? declaration.getTypeNode()
|
|
5639
|
+
: Node.isGetAccessorDeclaration(declaration)
|
|
5640
|
+
? declaration.getReturnTypeNode()
|
|
5641
|
+
: undefined;
|
|
5642
|
+
if (!typeNode)
|
|
5643
|
+
return false;
|
|
5644
|
+
const names = [typeNode, ...typeNode.getDescendants()].filter(Node.isIdentifier);
|
|
5645
|
+
return !names.some((name) => (name.getSymbol()?.getDeclarations() ?? []).some(Node.isTypeParameterDeclaration));
|
|
5646
|
+
});
|
|
5647
|
+
}
|
|
5044
5648
|
/**
|
|
5045
5649
|
* Anchor (b2): the contract declared by a VALIDATOR MIDDLEWARE on the
|
|
5046
5650
|
* registration (carrick#964).
|