carrick 0.3.107 → 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.
@@ -24,8 +24,9 @@ import { notePrintedType, PrintedTypes } from './printed-names.js';
24
24
  import { externalImportsOf, isExternalOrigin } from './origin.js';
25
25
  import { reachedOnlyOnFailure } from './failure-path.js';
26
26
  import { functionAtLine } from './function-line-index.js';
27
+ import { elapsedMs, inferTiming, phaseClock, timedPhase } from './infer-timing.js';
27
28
  import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
28
- import { expandTypeStructural, } from './type-structural-expander.js';
29
+ import { expandTypeStructural, jsonWireType, } from './type-structural-expander.js';
29
30
  /**
30
31
  * TS/lib globals and primitives that must never be emitted as a deterministic
31
32
  * type anchor (`primary_type_symbol`). A payload whose resolved symbol is one of
@@ -240,7 +241,9 @@ const RAW_TEXT_FORMAT = 'text';
240
241
  */
241
242
  const TYPE_TEXT_FLAGS = ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.InTypeAlias;
242
243
  function typeText(type, enclosingNode) {
243
- const text = type.getText(enclosingNode, TYPE_TEXT_FLAGS);
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));
244
247
  notePrintedType(type, enclosingNode, text);
245
248
  return text;
246
249
  }
@@ -287,6 +290,12 @@ export class TypeInferrer {
287
290
  */
288
291
  readNodes = new WeakMap();
289
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();
290
299
  constructor(options) {
291
300
  this.project = options.project;
292
301
  this.packageOf = options.packageOf;
@@ -320,7 +329,14 @@ export class TypeInferrer {
320
329
  const errors = [];
321
330
  /** Response inferences the unwidened reading re-reads (carrick#1516). */
322
331
  const responses = [];
332
+ /** How long each request took, in the order they were done (carrick#1985). */
333
+ const timings = [];
323
334
  for (const request of requests) {
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;
324
340
  try {
325
341
  // Plain JavaScript has no type annotations to extract, and `checkJs` is
326
342
  // off, so inferring against a `.js` file yields nothing useful — it only
@@ -343,6 +359,7 @@ export class TypeInferrer {
343
359
  const prints = new PrintedTypes();
344
360
  const result = prints.during(() => this.inferSingle(request, extractionConfig));
345
361
  if (result) {
362
+ printedLength = result.type_string.length;
346
363
  this.recordPrintedNames(result, request, prints);
347
364
  inferredTypes.push(result);
348
365
  if (request.infer_kind === 'response_body' || request.infer_kind === 'function_return') {
@@ -359,6 +376,20 @@ export class TypeInferrer {
359
376
  errors.push(`Error inferring type at ${request.file_path}:${loc}: ${error}`);
360
377
  }
361
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
+ });
362
393
  onRequestDone?.();
363
394
  }
364
395
  }
@@ -373,6 +404,7 @@ export class TypeInferrer {
373
404
  return {
374
405
  success: errors.length === 0 || inferredTypes.length > 0,
375
406
  inferred_types: inferredTypes.length > 0 ? inferredTypes : undefined,
407
+ timing: inferTiming(timings),
376
408
  errors: errors.length > 0 ? errors : undefined,
377
409
  };
378
410
  }
@@ -799,7 +831,10 @@ export class TypeInferrer {
799
831
  return null;
800
832
  }
801
833
  const isExplicit = func.getReturnTypeNode() !== undefined;
802
- const typeString = typeText(func.getReturnType(), func);
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);
803
838
  return this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(func));
804
839
  }
805
840
  /**
@@ -826,7 +861,7 @@ export class TypeInferrer {
826
861
  return null;
827
862
  }
828
863
  const isExplicit = target.param.getTypeNode() !== undefined;
829
- const paramType = target.node.getType();
864
+ const paramType = timedPhase('type', () => target.node.getType());
830
865
  const typeString = typeText(paramType, target.node);
831
866
  // Deterministic anchor for the pub/sub two-anchor arbitration
832
867
  // (carrick#413): report the payload type's root symbol, its declaration
@@ -901,11 +936,50 @@ export class TypeInferrer {
901
936
  this.log(`Line ${request.line_number} is a route registration; following handler return`);
902
937
  return this.buildFunctionReturnInferredType(request, atLine.handler, extractionConfig, true);
903
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
+ }
904
953
  // No locator, or locator didn't resolve — likely a payload-less handler
905
954
  // (redirect, 204, streaming). Infer the containing function's return type.
906
955
  this.log(`No payload node found for request at ${request.file_path}:${request.line_number}; falling back to function return`);
907
956
  return this.inferFunctionReturn(sourceFile, request, extractionConfig);
908
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
+ }
909
983
  // Route-registry object literal: the locator lands on the registry entry
910
984
  // `{ method, path, handler: healthCheckHandler }`, whose response contract
911
985
  // is the handler's RETURN type — one indirection away, NOT the object's own
@@ -1024,6 +1098,140 @@ export class TypeInferrer {
1024
1098
  const anchor = this.unwrapArrayLevels(this.unwrapPromiseType(payloadType));
1025
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));
1026
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);
1234
+ }
1027
1235
  /**
1028
1236
  * True when a located call's own result is the route's payload, so the
1029
1237
  * transitional drill into its first argument must not run (carrick#1732).
@@ -1047,7 +1255,10 @@ export class TypeInferrer {
1047
1255
  if (this.symbolIsLibOrExternalOrigin(element.getSymbol() ?? element.getAliasSymbol())) {
1048
1256
  return false;
1049
1257
  }
1050
- return this.nodeCarriesPayloadContract(call, false);
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);
1051
1262
  }
1052
1263
  /**
1053
1264
  * The wire representation a request's printed type takes. A route response
@@ -1601,6 +1812,17 @@ export class TypeInferrer {
1601
1812
  * else, and is not reported.
1602
1813
  */
1603
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) {
1604
1826
  const typeNode = this.explicitTypeNodeFromAncestor(terminal);
1605
1827
  const owner = typeNode?.getParent();
1606
1828
  if (!typeNode || !owner || this.leavesAPositionOpen(typeNode))
@@ -1627,7 +1849,7 @@ export class TypeInferrer {
1627
1849
  }
1628
1850
  current = parent;
1629
1851
  }
1630
- return this.statedRoot(typeNode);
1852
+ return typeNode;
1631
1853
  }
1632
1854
  /**
1633
1855
  * A stated type with `any` or `unknown` written anywhere in it (`unknown`,
@@ -1767,12 +1989,31 @@ export class TypeInferrer {
1767
1989
  // expression in the body (`c.req.json<T>()`, `req.body as T`).
1768
1990
  const atLine = this.registrationAtLine(sourceFile, request.line_number);
1769
1991
  if (atLine) {
1770
- const requestType = this.requestContractFromRegistration(atLine.registration, atLine.handler);
1771
- if (requestType) {
1772
- return this.declaredRequestInferredType(request, requestType, this.getNodeLocation(atLine.handler));
1992
+ const declared = this.declaredRequestContract(atLine.registration, atLine.handler);
1993
+ if (declared) {
1994
+ return this.declaredRequestInferredType(request, declared, this.getNodeLocation(atLine.handler));
1773
1995
  }
1774
1996
  }
1775
- return null;
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;
1776
2017
  }
1777
2018
  // Inline-handler registration (`app.post('/x', async (c) => { … })`) or a
1778
2019
  // route-registry object literal: the locator lands on the registration, not
@@ -3626,10 +3867,15 @@ export class TypeInferrer {
3626
3867
  * stated status, and what survives is joined as a union exactly as several
3627
3868
  * return statements are.
3628
3869
  */
3629
- recoverPayloadFromResponseExpressions(expressions, statedOnly, wire) {
3870
+ recoverPayloadFromResponseExpressions(expressions, statedOnly, wire, alsoSerialisers) {
3630
3871
  const candidates = [];
3631
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.
3632
3875
  const serialisers = this.calleesProvenSerialiser(returned);
3876
+ for (const identity of alsoSerialisers ?? []) {
3877
+ serialisers.add(identity);
3878
+ }
3633
3879
  for (const expression of returned) {
3634
3880
  const payloadNode = this.responseHelperPayloadNode(expression, 0, statedOnly, serialisers);
3635
3881
  if (!payloadNode)
@@ -3882,8 +4128,11 @@ export class TypeInferrer {
3882
4128
  *
3883
4129
  * Under `statedOnly` the annotation is the ONLY thing that counts, so an
3884
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`.
3885
4134
  */
3886
- nodeCarriesPayloadContract(node, statedOnly) {
4135
+ nodeCarriesPayloadContract(node, statedOnly, asSent = true) {
3887
4136
  if (this.statedTypeNodeOf(node))
3888
4137
  return true;
3889
4138
  if (statedOnly)
@@ -3901,24 +4150,37 @@ export class TypeInferrer {
3901
4150
  return false;
3902
4151
  if (this.typeIsOrContainsResponseMachinery(type))
3903
4152
  return false;
3904
- return this.typeIsObjectShaped(type, 0);
4153
+ return this.typeIsObjectShaped(type, 0, asSent);
3905
4154
  }
3906
- /** Object, array-of-object, or a union/intersection containing one. */
3907
- typeIsObjectShaped(type, depth) {
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) {
3908
4167
  if (depth > 2)
3909
4168
  return false;
3910
4169
  if (type.isUnion()) {
3911
- return type.getUnionTypes().some((m) => this.typeIsObjectShaped(m, depth + 1));
4170
+ return type.getUnionTypes().some((m) => this.typeIsObjectShaped(m, depth + 1, asSent));
3912
4171
  }
3913
4172
  if (type.isIntersection()) {
3914
4173
  return type
3915
4174
  .getIntersectionTypes()
3916
- .some((m) => this.typeIsObjectShaped(m, depth + 1));
4175
+ .some((m) => this.typeIsObjectShaped(m, depth + 1, asSent));
3917
4176
  }
3918
4177
  if (type.isArray()) {
3919
4178
  const element = type.getArrayElementType();
3920
- return element ? this.typeIsObjectShaped(element, depth + 1) : false;
4179
+ return element ? this.typeIsObjectShaped(element, depth + 1, asSent) : false;
3921
4180
  }
4181
+ const sent = asSent ? jsonWireType(type) : undefined;
4182
+ if (sent)
4183
+ return this.typeIsObjectShaped(sent, depth + 1, asSent);
3922
4184
  return type.isObject() && !type.isTuple();
3923
4185
  }
3924
4186
  /**
@@ -4768,6 +5030,98 @@ export class TypeInferrer {
4768
5030
  });
4769
5031
  return found;
4770
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
+ }
4771
5125
  /**
4772
5126
  * Resolve a type-annotation/type-argument node to fully-structural text,
4773
5127
  * dropping any `Promise<…>` wrapper (`c.req.json<T>()` returns `Promise<T>`,
@@ -5220,6 +5574,14 @@ export class TypeInferrer {
5220
5574
  * request type resolves `body` to `unknown`/`any`, which the useless-type
5221
5575
  * guard rejects. So a handler that declares nothing yields null and the next
5222
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.
5223
5585
  */
5224
5586
  requestBodyFromHandlerParams(func) {
5225
5587
  for (const param of func.getParameters()) {
@@ -5231,7 +5593,7 @@ export class TypeInferrer {
5231
5593
  continue;
5232
5594
  }
5233
5595
  const bodySymbol = paramType.getProperty('body');
5234
- if (!bodySymbol) {
5596
+ if (!bodySymbol || this.memberIsFixedByItsLibrary(bodySymbol)) {
5235
5597
  continue;
5236
5598
  }
5237
5599
  let bodyType;
@@ -5248,6 +5610,41 @@ export class TypeInferrer {
5248
5610
  }
5249
5611
  return null;
5250
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
+ }
5251
5648
  /**
5252
5649
  * Anchor (b2): the contract declared by a VALIDATOR MIDDLEWARE on the
5253
5650
  * registration (carrick#964).