carrick 0.3.107 → 0.3.109
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/package.json +9 -10
- package/plugin/.claude-plugin/plugin.json +1 -1
- 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-deep.js +2 -1
- 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-union.d.ts +40 -0
- package/sidecar/dist/src/capture/check-union.js +92 -0
- package/sidecar/dist/src/capture/check-workspace.d.ts +1 -0
- package/sidecar/dist/src/capture/check-workspace.js +13 -2
- package/sidecar/dist/src/capture/check.js +5 -0
- package/sidecar/dist/src/capture/compiler-options.d.ts +39 -0
- package/sidecar/dist/src/capture/compiler-options.js +100 -0
- package/sidecar/dist/src/capture/deno-project.js +3 -2
- package/sidecar/dist/src/capture/index.d.ts +1 -0
- package/sidecar/dist/src/capture/index.js +90 -48
- package/sidecar/dist/src/capture/installed-package.js +3 -2
- package/sidecar/dist/src/capture/self-check.js +3 -2
- package/sidecar/dist/src/definition-resolver.d.ts +5 -8
- package/sidecar/dist/src/definition-resolver.js +8 -9
- package/sidecar/dist/src/index.js +124 -11
- package/sidecar/dist/src/infer-timing.d.ts +54 -0
- package/sidecar/dist/src/infer-timing.js +124 -0
- package/sidecar/dist/src/print-type.d.ts +48 -0
- package/sidecar/dist/src/print-type.js +71 -0
- package/sidecar/dist/src/printed-names.js +2 -1
- package/sidecar/dist/src/program-files.d.ts +44 -0
- package/sidecar/dist/src/program-files.js +64 -0
- package/sidecar/dist/src/project-loader.d.ts +5 -0
- package/sidecar/dist/src/project-loader.js +14 -5
- package/sidecar/dist/src/retype.d.ts +4 -2
- package/sidecar/dist/src/retype.js +16 -5
- package/sidecar/dist/src/type-inferrer.d.ts +157 -1
- package/sidecar/dist/src/type-inferrer.js +444 -20
- package/sidecar/dist/src/types.d.ts +104 -2
- package/sidecar/dist/src/validators.d.ts +166 -80
- package/sidecar/dist/src/validators.js +13 -0
- package/templates/skills/carrick-reuse.md +6 -5
|
@@ -19,13 +19,15 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { Node, SyntaxKind, ts, } from 'ts-morph';
|
|
22
|
+
import { typeToStringAsDeclared } from './print-type.js';
|
|
22
23
|
import { validateInferRequestItem } from './validators.js';
|
|
23
24
|
import { notePrintedType, PrintedTypes } from './printed-names.js';
|
|
24
25
|
import { externalImportsOf, isExternalOrigin } from './origin.js';
|
|
25
26
|
import { reachedOnlyOnFailure } from './failure-path.js';
|
|
26
27
|
import { functionAtLine } from './function-line-index.js';
|
|
28
|
+
import { elapsedMs, inferTiming, phaseClock, timedPhase } from './infer-timing.js';
|
|
27
29
|
import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
|
|
28
|
-
import { expandTypeStructural, } from './type-structural-expander.js';
|
|
30
|
+
import { expandTypeStructural, jsonWireType, } from './type-structural-expander.js';
|
|
29
31
|
/**
|
|
30
32
|
* TS/lib globals and primitives that must never be emitted as a deterministic
|
|
31
33
|
* type anchor (`primary_type_symbol`). A payload whose resolved symbol is one of
|
|
@@ -240,7 +242,13 @@ const RAW_TEXT_FORMAT = 'text';
|
|
|
240
242
|
*/
|
|
241
243
|
const TYPE_TEXT_FLAGS = ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.InTypeAlias;
|
|
242
244
|
function typeText(type, enclosingNode) {
|
|
243
|
-
|
|
245
|
+
// Timed as the print (carrick#1985): the type is already computed when it
|
|
246
|
+
// gets here, so this is what its length costs. A module the print names is
|
|
247
|
+
// written as its declaring file, whichever file it is printed from
|
|
248
|
+
// (carrick#2019).
|
|
249
|
+
const text = timedPhase('print', () => enclosingNode
|
|
250
|
+
? typeToStringAsDeclared(enclosingNode.getProject().getTypeChecker().compilerObject, type.compilerType, enclosingNode.compilerNode, TYPE_TEXT_FLAGS)
|
|
251
|
+
: type.getText(undefined, TYPE_TEXT_FLAGS));
|
|
244
252
|
notePrintedType(type, enclosingNode, text);
|
|
245
253
|
return text;
|
|
246
254
|
}
|
|
@@ -287,6 +295,12 @@ export class TypeInferrer {
|
|
|
287
295
|
*/
|
|
288
296
|
readNodes = new WeakMap();
|
|
289
297
|
unwidenedBudgetMs;
|
|
298
|
+
/**
|
|
299
|
+
* The files a request has named to this inferrer, as the requests named
|
|
300
|
+
* them (carrick#1985). It lives as long as the project does, so the first
|
|
301
|
+
* request of a file is the first of the process, not of a batch.
|
|
302
|
+
*/
|
|
303
|
+
filesAsked = new Set();
|
|
290
304
|
constructor(options) {
|
|
291
305
|
this.project = options.project;
|
|
292
306
|
this.packageOf = options.packageOf;
|
|
@@ -320,7 +334,14 @@ export class TypeInferrer {
|
|
|
320
334
|
const errors = [];
|
|
321
335
|
/** Response inferences the unwidened reading re-reads (carrick#1516). */
|
|
322
336
|
const responses = [];
|
|
337
|
+
/** How long each request took, in the order they were done (carrick#1985). */
|
|
338
|
+
const timings = [];
|
|
323
339
|
for (const request of requests) {
|
|
340
|
+
const slotStarted = performance.now();
|
|
341
|
+
const phasesStarted = phaseClock();
|
|
342
|
+
const firstInFile = !this.filesAsked.has(request.file_path);
|
|
343
|
+
this.filesAsked.add(request.file_path);
|
|
344
|
+
let printedLength = 0;
|
|
324
345
|
try {
|
|
325
346
|
// Plain JavaScript has no type annotations to extract, and `checkJs` is
|
|
326
347
|
// off, so inferring against a `.js` file yields nothing useful — it only
|
|
@@ -343,6 +364,7 @@ export class TypeInferrer {
|
|
|
343
364
|
const prints = new PrintedTypes();
|
|
344
365
|
const result = prints.during(() => this.inferSingle(request, extractionConfig));
|
|
345
366
|
if (result) {
|
|
367
|
+
printedLength = result.type_string.length;
|
|
346
368
|
this.recordPrintedNames(result, request, prints);
|
|
347
369
|
inferredTypes.push(result);
|
|
348
370
|
if (request.infer_kind === 'response_body' || request.infer_kind === 'function_return') {
|
|
@@ -359,6 +381,20 @@ export class TypeInferrer {
|
|
|
359
381
|
errors.push(`Error inferring type at ${request.file_path}:${loc}: ${error}`);
|
|
360
382
|
}
|
|
361
383
|
finally {
|
|
384
|
+
// Before the caller is told: what it does with the news (a progress
|
|
385
|
+
// frame) is not this request's time.
|
|
386
|
+
const phases = phaseClock();
|
|
387
|
+
timings.push({
|
|
388
|
+
...(request.alias === undefined ? {} : { alias: request.alias }),
|
|
389
|
+
file_path: request.file_path,
|
|
390
|
+
line_number: request.line_number,
|
|
391
|
+
infer_kind: request.infer_kind,
|
|
392
|
+
ms: elapsedMs(slotStarted, performance.now()),
|
|
393
|
+
type_ms: elapsedMs(phasesStarted.type, phases.type),
|
|
394
|
+
print_ms: elapsedMs(phasesStarted.print, phases.print),
|
|
395
|
+
printed_length: printedLength,
|
|
396
|
+
first_in_file: firstInFile,
|
|
397
|
+
});
|
|
362
398
|
onRequestDone?.();
|
|
363
399
|
}
|
|
364
400
|
}
|
|
@@ -373,6 +409,7 @@ export class TypeInferrer {
|
|
|
373
409
|
return {
|
|
374
410
|
success: errors.length === 0 || inferredTypes.length > 0,
|
|
375
411
|
inferred_types: inferredTypes.length > 0 ? inferredTypes : undefined,
|
|
412
|
+
timing: inferTiming(timings),
|
|
376
413
|
errors: errors.length > 0 ? errors : undefined,
|
|
377
414
|
};
|
|
378
415
|
}
|
|
@@ -799,7 +836,10 @@ export class TypeInferrer {
|
|
|
799
836
|
return null;
|
|
800
837
|
}
|
|
801
838
|
const isExplicit = func.getReturnTypeNode() !== undefined;
|
|
802
|
-
|
|
839
|
+
// The compiler call is timed apart from the print of what it returns
|
|
840
|
+
// (carrick#1985).
|
|
841
|
+
const returnType = timedPhase('type', () => func.getReturnType());
|
|
842
|
+
const typeString = typeText(returnType, func);
|
|
803
843
|
return this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(func));
|
|
804
844
|
}
|
|
805
845
|
/**
|
|
@@ -826,7 +866,7 @@ export class TypeInferrer {
|
|
|
826
866
|
return null;
|
|
827
867
|
}
|
|
828
868
|
const isExplicit = target.param.getTypeNode() !== undefined;
|
|
829
|
-
const paramType = target.node.getType();
|
|
869
|
+
const paramType = timedPhase('type', () => target.node.getType());
|
|
830
870
|
const typeString = typeText(paramType, target.node);
|
|
831
871
|
// Deterministic anchor for the pub/sub two-anchor arbitration
|
|
832
872
|
// (carrick#413): report the payload type's root symbol, its declaration
|
|
@@ -901,11 +941,50 @@ export class TypeInferrer {
|
|
|
901
941
|
this.log(`Line ${request.line_number} is a route registration; following handler return`);
|
|
902
942
|
return this.buildFunctionReturnInferredType(request, atLine.handler, extractionConfig, true);
|
|
903
943
|
}
|
|
944
|
+
// carrick#807: the request named an expression and the file holds none
|
|
945
|
+
// that matches. Where its line declares the handler, that handler is
|
|
946
|
+
// still the row's, and what it returns is still the question. Without
|
|
947
|
+
// this the unmatched text also fails the function lookup below, and a
|
|
948
|
+
// row a line-only request answers is lost to a locator that added
|
|
949
|
+
// nothing.
|
|
950
|
+
const declaredHandler = request.expression_text
|
|
951
|
+
? this.handlerDeclaredAtLine(sourceFile, request.line_number)
|
|
952
|
+
: undefined;
|
|
953
|
+
if (declaredHandler) {
|
|
954
|
+
this.log(`No node matches the located expression at ${request.file_path}:${request.line_number}; ` +
|
|
955
|
+
'reading what the handler this line declares returns');
|
|
956
|
+
return this.buildFunctionReturnInferredType(request, declaredHandler, extractionConfig, true);
|
|
957
|
+
}
|
|
904
958
|
// No locator, or locator didn't resolve — likely a payload-less handler
|
|
905
959
|
// (redirect, 204, streaming). Infer the containing function's return type.
|
|
906
960
|
this.log(`No payload node found for request at ${request.file_path}:${request.line_number}; falling back to function return`);
|
|
907
961
|
return this.inferFunctionReturn(sourceFile, request, extractionConfig);
|
|
908
962
|
}
|
|
963
|
+
// carrick#807: the row's line declares the handler itself, as it does for
|
|
964
|
+
// a route its module's place in the project states, and the located
|
|
965
|
+
// expression sits inside that handler. What the handler returns is the
|
|
966
|
+
// answer a line-only request gets, and a locator must not change it.
|
|
967
|
+
// The ancestor test comes first because it is free: only a node inside a
|
|
968
|
+
// function that opens on the row's line can be inside the one the line
|
|
969
|
+
// declares, and most rows are not.
|
|
970
|
+
const insideFunctionOnLine = node
|
|
971
|
+
.getAncestors()
|
|
972
|
+
.some((ancestor) => (Node.isFunctionDeclaration(ancestor) ||
|
|
973
|
+
Node.isArrowFunction(ancestor) ||
|
|
974
|
+
Node.isFunctionExpression(ancestor) ||
|
|
975
|
+
Node.isMethodDeclaration(ancestor)) &&
|
|
976
|
+
ancestor.getStartLineNumber() === request.line_number);
|
|
977
|
+
const declaredHandler = insideFunctionOnLine
|
|
978
|
+
? this.handlerDeclaredAtLine(sourceFile, request.line_number)
|
|
979
|
+
: undefined;
|
|
980
|
+
if (declaredHandler &&
|
|
981
|
+
declaredHandler.getStart() <= node.getStart() &&
|
|
982
|
+
node.getEnd() <= declaredHandler.getEnd()) {
|
|
983
|
+
const returned = this.responseOfDeclaredHandler(request, declaredHandler, node, extractionConfig);
|
|
984
|
+
if (returned !== undefined) {
|
|
985
|
+
return returned;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
909
988
|
// Route-registry object literal: the locator lands on the registry entry
|
|
910
989
|
// `{ method, path, handler: healthCheckHandler }`, whose response contract
|
|
911
990
|
// is the handler's RETURN type — one indirection away, NOT the object's own
|
|
@@ -1024,6 +1103,140 @@ export class TypeInferrer {
|
|
|
1024
1103
|
const anchor = this.unwrapArrayLevels(this.unwrapPromiseType(payloadType));
|
|
1025
1104
|
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
1105
|
}
|
|
1106
|
+
/**
|
|
1107
|
+
* The handler a module exports on `line`: a function that opens on exactly
|
|
1108
|
+
* that line, exported at the top of its module, where no call on the line
|
|
1109
|
+
* registers a route (carrick#807).
|
|
1110
|
+
*
|
|
1111
|
+
* That is the anchor of a route its module's place in the project states:
|
|
1112
|
+
* the row's line is the handler's own export, and the module's exports are
|
|
1113
|
+
* how the framework finds it. Each condition keeps another kind of row out:
|
|
1114
|
+
*
|
|
1115
|
+
* - the line has to be the function's first, with no tolerance. A model
|
|
1116
|
+
* row that names no site of its own can sit on any line of a handler,
|
|
1117
|
+
* and the nearest function to such a line is whatever is declared
|
|
1118
|
+
* beside it;
|
|
1119
|
+
* - a method of a class is a controller's handler, whose rows are
|
|
1120
|
+
* anchored on what a model located before the method's own return;
|
|
1121
|
+
* - a line that registers a route has a registration to read.
|
|
1122
|
+
*/
|
|
1123
|
+
handlerDeclaredAtLine(sourceFile, line) {
|
|
1124
|
+
const declared = this.exportedFunctionOpeningOn(sourceFile, line);
|
|
1125
|
+
return declared && !this.registrationAtLine(sourceFile, line) ? declared : undefined;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* `handlerDeclaredAtLine` without its registration test, for a caller that
|
|
1129
|
+
* has already looked for a registration on the line.
|
|
1130
|
+
*/
|
|
1131
|
+
exportedFunctionOpeningOn(sourceFile, line) {
|
|
1132
|
+
const declared = this.findFunctionByLine(sourceFile, line);
|
|
1133
|
+
if (!declared || declared.getStartLineNumber() !== line)
|
|
1134
|
+
return undefined;
|
|
1135
|
+
if (Node.isFunctionDeclaration(declared)) {
|
|
1136
|
+
return Node.isSourceFile(declared.getParent()) && declared.isExported()
|
|
1137
|
+
? declared
|
|
1138
|
+
: undefined;
|
|
1139
|
+
}
|
|
1140
|
+
if (!Node.isArrowFunction(declared) && !Node.isFunctionExpression(declared)) {
|
|
1141
|
+
return undefined;
|
|
1142
|
+
}
|
|
1143
|
+
const binding = declared.getParent();
|
|
1144
|
+
if (!Node.isVariableDeclaration(binding) || binding.getInitializer() !== declared) {
|
|
1145
|
+
return undefined;
|
|
1146
|
+
}
|
|
1147
|
+
const statement = binding.getVariableStatement();
|
|
1148
|
+
return statement && Node.isSourceFile(statement.getParent()) && statement.isExported()
|
|
1149
|
+
? declared
|
|
1150
|
+
: undefined;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* The response of a handler the request's line declares, for a request
|
|
1154
|
+
* that also located an expression inside it (carrick#807).
|
|
1155
|
+
*
|
|
1156
|
+
* The handler's own return is read first, exactly as a `function_return`
|
|
1157
|
+
* request at the line reads it, and that reading stands: a body it
|
|
1158
|
+
* recovered, and a decision it made not to publish one. The located
|
|
1159
|
+
* expression is consulted in one case only. Where the handler returns a
|
|
1160
|
+
* call nothing resolves (its library is not installed), the return walk
|
|
1161
|
+
* reads no argument the source does not annotate, because it cannot tell a
|
|
1162
|
+
* response sender from a query. A located payload that is an ARGUMENT of
|
|
1163
|
+
* such a returned call says which it is, so the same walk runs again with
|
|
1164
|
+
* that callee taken as a sender: every success body the handler returns
|
|
1165
|
+
* through it, error branches dropped as before. The located expression's
|
|
1166
|
+
* own type is never what is published here.
|
|
1167
|
+
*
|
|
1168
|
+
* Returns `undefined` when the located expression is something the return
|
|
1169
|
+
* walk never looked at, so it is left to the readings below:
|
|
1170
|
+
* - the handler returns nothing (`void`): it sends through a parameter;
|
|
1171
|
+
* - the handler returns transport the walk read no body out of, and the
|
|
1172
|
+
* located expression is not part of what it returns. A body built before
|
|
1173
|
+
* the response is returned (`const res = send(body); ...; return res`)
|
|
1174
|
+
* is one. Named on a send that states an error or redirect status, it is
|
|
1175
|
+
* no success body and the row abstains.
|
|
1176
|
+
*/
|
|
1177
|
+
responseOfDeclaredHandler(request, handler, located, extractionConfig) {
|
|
1178
|
+
const walked = this.buildFunctionReturnInferredType(request, handler, extractionConfig);
|
|
1179
|
+
const returned = this.responseReturnedExpressions(handler);
|
|
1180
|
+
if (walked === null) {
|
|
1181
|
+
// The walk read everything the handler returns and published nothing.
|
|
1182
|
+
// A located expression inside one of those returns was part of that.
|
|
1183
|
+
if (this.returnedSendHolding(returned, located)) {
|
|
1184
|
+
this.log(`Response locator at ${request.file_path}:${request.line_number} names a send ` +
|
|
1185
|
+
'its handler returns, which carries no body a contract can describe; abstaining');
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
// Outside the return, it is either a send itself or what one was handed.
|
|
1189
|
+
const sends = [
|
|
1190
|
+
this.peelTransparentExpression(located),
|
|
1191
|
+
this.receivingCallOf(located, () => true),
|
|
1192
|
+
];
|
|
1193
|
+
const statesNoBody = sends.some((send) => {
|
|
1194
|
+
const status = send ? this.responseSiteStatus(send) : 'undecided';
|
|
1195
|
+
return status === 'error' || status === 'redirect';
|
|
1196
|
+
});
|
|
1197
|
+
if (statesNoBody) {
|
|
1198
|
+
this.log(`Response locator at ${request.file_path}:${request.line_number} names an error ` +
|
|
1199
|
+
'or redirect send and its handler returns no success body; abstaining');
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
return undefined;
|
|
1203
|
+
}
|
|
1204
|
+
const text = walked.type_string.trim();
|
|
1205
|
+
if (text === 'void' || text === 'undefined' || text === 'never' || text === '') {
|
|
1206
|
+
return undefined;
|
|
1207
|
+
}
|
|
1208
|
+
const unresolvedCallee = (walked.any_provenance ?? []).some((entry) => entry.reason === 'no_payload_evidence');
|
|
1209
|
+
if (!unresolvedCallee) {
|
|
1210
|
+
return walked;
|
|
1211
|
+
}
|
|
1212
|
+
const sender = this.returnedSendHolding(returned, located);
|
|
1213
|
+
const identity = sender ? this.calleeIdentity(sender) : undefined;
|
|
1214
|
+
if (identity) {
|
|
1215
|
+
const recovered = this.recoverPayloadFromResponseExpressions(returned, true, this.wireFormatFor(request), new Set([identity]));
|
|
1216
|
+
if (recovered) {
|
|
1217
|
+
this.log(`Return type at ${request.file_path}:${request.line_number} resolves to nothing; ` +
|
|
1218
|
+
'the located payload is an argument of a call the handler returns, so that ' +
|
|
1219
|
+
"call is a response sender and its success bodies are the route's");
|
|
1220
|
+
return this.inferredFromRecoveredPayload(request, recovered);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return walked;
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* The call inside one of `returned` that the located expression is, or is
|
|
1227
|
+
* an argument of: the send a located payload was handed to. `undefined`
|
|
1228
|
+
* when the located expression is not part of what the handler returns.
|
|
1229
|
+
*/
|
|
1230
|
+
returnedSendHolding(returned, located) {
|
|
1231
|
+
const branches = returned.flatMap((expression) => this.expandResponseBranches(expression, 0));
|
|
1232
|
+
const isReturned = (candidate) => branches.some((branch) => branch.getStart() <= candidate.getStart() && candidate.getEnd() <= branch.getEnd());
|
|
1233
|
+
const peeled = this.peelTransparentExpression(located);
|
|
1234
|
+
if ((Node.isCallExpression(peeled) || Node.isNewExpression(peeled)) &&
|
|
1235
|
+
branches.includes(peeled)) {
|
|
1236
|
+
return peeled;
|
|
1237
|
+
}
|
|
1238
|
+
return this.receivingCallOf(located, isReturned);
|
|
1239
|
+
}
|
|
1027
1240
|
/**
|
|
1028
1241
|
* True when a located call's own result is the route's payload, so the
|
|
1029
1242
|
* transitional drill into its first argument must not run (carrick#1732).
|
|
@@ -1047,7 +1260,10 @@ export class TypeInferrer {
|
|
|
1047
1260
|
if (this.symbolIsLibOrExternalOrigin(element.getSymbol() ?? element.getAliasSymbol())) {
|
|
1048
1261
|
return false;
|
|
1049
1262
|
}
|
|
1050
|
-
|
|
1263
|
+
// Asked of the result as DECLARED. A value of the repo's own that
|
|
1264
|
+
// serialises to a string is still what the route sends, and drilling into
|
|
1265
|
+
// the call that built it would publish what it was built from.
|
|
1266
|
+
return this.nodeCarriesPayloadContract(call, false, false);
|
|
1051
1267
|
}
|
|
1052
1268
|
/**
|
|
1053
1269
|
* The wire representation a request's printed type takes. A route response
|
|
@@ -1601,6 +1817,17 @@ export class TypeInferrer {
|
|
|
1601
1817
|
* else, and is not reported.
|
|
1602
1818
|
*/
|
|
1603
1819
|
statedBodyAtRead(terminal) {
|
|
1820
|
+
const typeNode = this.typeNodeStatedAtRead(terminal);
|
|
1821
|
+
return typeNode ? this.statedRoot(typeNode) : undefined;
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* The type node `statedBodyAtRead` reads, for a caller that wants the
|
|
1825
|
+
* annotation itself: a consumer's response read reports its root, and a
|
|
1826
|
+
* handler's request read prints it (carrick#807). One reading of "stated at
|
|
1827
|
+
* the read" for both, so the two cannot disagree about which annotation
|
|
1828
|
+
* belongs to a read.
|
|
1829
|
+
*/
|
|
1830
|
+
typeNodeStatedAtRead(terminal) {
|
|
1604
1831
|
const typeNode = this.explicitTypeNodeFromAncestor(terminal);
|
|
1605
1832
|
const owner = typeNode?.getParent();
|
|
1606
1833
|
if (!typeNode || !owner || this.leavesAPositionOpen(typeNode))
|
|
@@ -1627,7 +1854,7 @@ export class TypeInferrer {
|
|
|
1627
1854
|
}
|
|
1628
1855
|
current = parent;
|
|
1629
1856
|
}
|
|
1630
|
-
return
|
|
1857
|
+
return typeNode;
|
|
1631
1858
|
}
|
|
1632
1859
|
/**
|
|
1633
1860
|
* A stated type with `any` or `unknown` written anywhere in it (`unknown`,
|
|
@@ -1767,12 +1994,31 @@ export class TypeInferrer {
|
|
|
1767
1994
|
// expression in the body (`c.req.json<T>()`, `req.body as T`).
|
|
1768
1995
|
const atLine = this.registrationAtLine(sourceFile, request.line_number);
|
|
1769
1996
|
if (atLine) {
|
|
1770
|
-
const
|
|
1771
|
-
if (
|
|
1772
|
-
return this.declaredRequestInferredType(request,
|
|
1997
|
+
const declared = this.declaredRequestContract(atLine.registration, atLine.handler);
|
|
1998
|
+
if (declared) {
|
|
1999
|
+
return this.declaredRequestInferredType(request, declared, this.getNodeLocation(atLine.handler));
|
|
1773
2000
|
}
|
|
1774
2001
|
}
|
|
1775
|
-
|
|
2002
|
+
// With no call registering a route on this line, the line is the
|
|
2003
|
+
// handler's own export: all that anchors a route its module's place in
|
|
2004
|
+
// the project states (carrick#807). Either way the body is what the
|
|
2005
|
+
// handler reads off the platform request it was handed, where the
|
|
2006
|
+
// source says what it read.
|
|
2007
|
+
const handler = atLine?.handler ?? this.exportedFunctionOpeningOn(sourceFile, request.line_number);
|
|
2008
|
+
if (!handler) {
|
|
2009
|
+
return null;
|
|
2010
|
+
}
|
|
2011
|
+
const read = this.platformBodyReadIn(handler);
|
|
2012
|
+
const stated = read ? this.requestStatedAtBodyRead(request, read) : null;
|
|
2013
|
+
if (stated) {
|
|
2014
|
+
return stated;
|
|
2015
|
+
}
|
|
2016
|
+
// The first typed read anywhere in the handler is a looser reading, and
|
|
2017
|
+
// is kept only where a registration already had it.
|
|
2018
|
+
const typedRead = atLine ? this.inferRequestReadFromHandler(atLine.handler) : null;
|
|
2019
|
+
return typedRead
|
|
2020
|
+
? this.declaredRequestInferredType(request, { text: typedRead }, this.getNodeLocation(handler))
|
|
2021
|
+
: null;
|
|
1776
2022
|
}
|
|
1777
2023
|
// Inline-handler registration (`app.post('/x', async (c) => { … })`) or a
|
|
1778
2024
|
// route-registry object literal: the locator lands on the registration, not
|
|
@@ -3626,10 +3872,15 @@ export class TypeInferrer {
|
|
|
3626
3872
|
* stated status, and what survives is joined as a union exactly as several
|
|
3627
3873
|
* return statements are.
|
|
3628
3874
|
*/
|
|
3629
|
-
recoverPayloadFromResponseExpressions(expressions, statedOnly, wire) {
|
|
3875
|
+
recoverPayloadFromResponseExpressions(expressions, statedOnly, wire, alsoSerialisers) {
|
|
3630
3876
|
const candidates = [];
|
|
3631
3877
|
const returned = expressions.flatMap((expression) => this.expandResponseBranches(expression, 0));
|
|
3878
|
+
// `alsoSerialisers` are callees a caller has its own evidence for
|
|
3879
|
+
// (`calleeIdentity` keys), beside the ones these expressions prove.
|
|
3632
3880
|
const serialisers = this.calleesProvenSerialiser(returned);
|
|
3881
|
+
for (const identity of alsoSerialisers ?? []) {
|
|
3882
|
+
serialisers.add(identity);
|
|
3883
|
+
}
|
|
3633
3884
|
for (const expression of returned) {
|
|
3634
3885
|
const payloadNode = this.responseHelperPayloadNode(expression, 0, statedOnly, serialisers);
|
|
3635
3886
|
if (!payloadNode)
|
|
@@ -3882,8 +4133,11 @@ export class TypeInferrer {
|
|
|
3882
4133
|
*
|
|
3883
4134
|
* Under `statedOnly` the annotation is the ONLY thing that counts, so an
|
|
3884
4135
|
* unresolvable callee's arguments never become a contract by accident.
|
|
4136
|
+
*
|
|
4137
|
+
* `asSent` judges the object in the form it is sent in, which is what an
|
|
4138
|
+
* ARGUMENT handed to a sender is asked; see `typeIsObjectShaped`.
|
|
3885
4139
|
*/
|
|
3886
|
-
nodeCarriesPayloadContract(node, statedOnly) {
|
|
4140
|
+
nodeCarriesPayloadContract(node, statedOnly, asSent = true) {
|
|
3887
4141
|
if (this.statedTypeNodeOf(node))
|
|
3888
4142
|
return true;
|
|
3889
4143
|
if (statedOnly)
|
|
@@ -3901,24 +4155,37 @@ export class TypeInferrer {
|
|
|
3901
4155
|
return false;
|
|
3902
4156
|
if (this.typeIsOrContainsResponseMachinery(type))
|
|
3903
4157
|
return false;
|
|
3904
|
-
return this.typeIsObjectShaped(type, 0);
|
|
4158
|
+
return this.typeIsObjectShaped(type, 0, asSent);
|
|
3905
4159
|
}
|
|
3906
|
-
/**
|
|
3907
|
-
|
|
4160
|
+
/**
|
|
4161
|
+
* Object, array-of-object, or a union/intersection containing one.
|
|
4162
|
+
*
|
|
4163
|
+
* With `asSent` the object is judged in the form it is SENT in
|
|
4164
|
+
* (carrick#1163): a value that declares `toJSON()` travels as what that
|
|
4165
|
+
* returns. A `URL` or a `Date` is therefore the string it serialises to,
|
|
4166
|
+
* the bare primitive this rule already refuses, and `redirect(new URL(path,
|
|
4167
|
+
* base))` hands over a location exactly as `redirect("/next")` does
|
|
4168
|
+
* (carrick#807). A value whose JSON form is itself an object is a body like
|
|
4169
|
+
* any other.
|
|
4170
|
+
*/
|
|
4171
|
+
typeIsObjectShaped(type, depth, asSent) {
|
|
3908
4172
|
if (depth > 2)
|
|
3909
4173
|
return false;
|
|
3910
4174
|
if (type.isUnion()) {
|
|
3911
|
-
return type.getUnionTypes().some((m) => this.typeIsObjectShaped(m, depth + 1));
|
|
4175
|
+
return type.getUnionTypes().some((m) => this.typeIsObjectShaped(m, depth + 1, asSent));
|
|
3912
4176
|
}
|
|
3913
4177
|
if (type.isIntersection()) {
|
|
3914
4178
|
return type
|
|
3915
4179
|
.getIntersectionTypes()
|
|
3916
|
-
.some((m) => this.typeIsObjectShaped(m, depth + 1));
|
|
4180
|
+
.some((m) => this.typeIsObjectShaped(m, depth + 1, asSent));
|
|
3917
4181
|
}
|
|
3918
4182
|
if (type.isArray()) {
|
|
3919
4183
|
const element = type.getArrayElementType();
|
|
3920
|
-
return element ? this.typeIsObjectShaped(element, depth + 1) : false;
|
|
4184
|
+
return element ? this.typeIsObjectShaped(element, depth + 1, asSent) : false;
|
|
3921
4185
|
}
|
|
4186
|
+
const sent = asSent ? jsonWireType(type) : undefined;
|
|
4187
|
+
if (sent)
|
|
4188
|
+
return this.typeIsObjectShaped(sent, depth + 1, asSent);
|
|
3922
4189
|
return type.isObject() && !type.isTuple();
|
|
3923
4190
|
}
|
|
3924
4191
|
/**
|
|
@@ -4363,7 +4630,7 @@ export class TypeInferrer {
|
|
|
4363
4630
|
*/
|
|
4364
4631
|
declaringPackageOf(type) {
|
|
4365
4632
|
const symbol = type.getSymbol() ?? type.getAliasSymbol();
|
|
4366
|
-
const declaration = symbol?.getDeclarations()
|
|
4633
|
+
const declaration = this.attributedDeclaration(symbol?.getDeclarations() ?? []);
|
|
4367
4634
|
if (!declaration) {
|
|
4368
4635
|
return undefined;
|
|
4369
4636
|
}
|
|
@@ -4386,6 +4653,28 @@ export class TypeInferrer {
|
|
|
4386
4653
|
}
|
|
4387
4654
|
return rest[0];
|
|
4388
4655
|
}
|
|
4656
|
+
/**
|
|
4657
|
+
* The declaration a symbol is attributed to (carrick#2031). One declaration
|
|
4658
|
+
* is itself. Where several files merge one symbol (a global interface the
|
|
4659
|
+
* compiler's library declares and a typings package augments), their order
|
|
4660
|
+
* is the order the checker merged them in, and on TypeScript 6 that changes
|
|
4661
|
+
* once the program is rebuilt, so the first one is not an answer. The
|
|
4662
|
+
* compiler's default library wins where it declares the symbol; otherwise,
|
|
4663
|
+
* and among the library's own files, the declaring file whose path sorts
|
|
4664
|
+
* first, then the earliest declaration in it.
|
|
4665
|
+
*/
|
|
4666
|
+
attributedDeclaration(declarations) {
|
|
4667
|
+
if (declarations.length <= 1)
|
|
4668
|
+
return declarations[0];
|
|
4669
|
+
const program = this.project.getProgram().compilerObject;
|
|
4670
|
+
const fromLibrary = declarations.filter((declaration) => program.isSourceFileDefaultLibrary(declaration.getSourceFile().compilerNode));
|
|
4671
|
+
const candidates = fromLibrary.length > 0 ? fromLibrary : declarations;
|
|
4672
|
+
return [...candidates].sort((a, b) => {
|
|
4673
|
+
const pathA = a.getSourceFile().getFilePath();
|
|
4674
|
+
const pathB = b.getSourceFile().getFilePath();
|
|
4675
|
+
return pathA < pathB ? -1 : pathA > pathB ? 1 : a.getStart() - b.getStart();
|
|
4676
|
+
})[0];
|
|
4677
|
+
}
|
|
4389
4678
|
/**
|
|
4390
4679
|
* The awaited return type of the member invoked on the receiver. Reported as
|
|
4391
4680
|
* a fact next to the receiver; a caller that classified on it alone would be
|
|
@@ -4768,6 +5057,98 @@ export class TypeInferrer {
|
|
|
4768
5057
|
});
|
|
4769
5058
|
return found;
|
|
4770
5059
|
}
|
|
5060
|
+
/**
|
|
5061
|
+
* The call in a handler that reads the request's body off the platform
|
|
5062
|
+
* request the handler was handed, or `undefined` (carrick#807).
|
|
5063
|
+
*
|
|
5064
|
+
* Read by shape, with no method name consulted:
|
|
5065
|
+
*
|
|
5066
|
+
* - a call that takes nothing, on a member of a value whose type is request
|
|
5067
|
+
* machinery (`typeIsFrameworkMachinery`: declared by the platform or an
|
|
5068
|
+
* installed library, and carrying its body readers);
|
|
5069
|
+
* - that value is rooted at one of the handler's OWN parameters, named or
|
|
5070
|
+
* destructured. A response read off an outbound call inside the handler
|
|
5071
|
+
* (`(await upstream.json()) as Rate`) has the same shape one variable
|
|
5072
|
+
* away, and is the opposite side of a different exchange;
|
|
5073
|
+
* - and the call's result, awaited, is `any` or `unknown`: the platform's
|
|
5074
|
+
* untyped parse. A read that states its own type (`formData()`, a typed
|
|
5075
|
+
* `json<T>()`) is not this shape and is left to the readers that
|
|
5076
|
+
* already handle it.
|
|
5077
|
+
*
|
|
5078
|
+
* The first such call in source order: a body is read once.
|
|
5079
|
+
*/
|
|
5080
|
+
platformBodyReadIn(handler) {
|
|
5081
|
+
const parameters = new Set(handler.getParameters());
|
|
5082
|
+
const rootedAtParameter = (expression) => {
|
|
5083
|
+
let root = expression;
|
|
5084
|
+
while (Node.isPropertyAccessExpression(root) ||
|
|
5085
|
+
Node.isNonNullExpression(root) ||
|
|
5086
|
+
Node.isParenthesizedExpression(root)) {
|
|
5087
|
+
root = root.getExpression();
|
|
5088
|
+
}
|
|
5089
|
+
if (!Node.isIdentifier(root))
|
|
5090
|
+
return false;
|
|
5091
|
+
return (root.getSymbol()?.getDeclarations() ?? []).some((declaration) => {
|
|
5092
|
+
const parameter = Node.isParameterDeclaration(declaration)
|
|
5093
|
+
? declaration
|
|
5094
|
+
: Node.isBindingElement(declaration)
|
|
5095
|
+
? declaration.getFirstAncestorByKind(SyntaxKind.Parameter)
|
|
5096
|
+
: undefined;
|
|
5097
|
+
return parameter !== undefined && parameters.has(parameter);
|
|
5098
|
+
});
|
|
5099
|
+
};
|
|
5100
|
+
for (const call of handler.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
5101
|
+
if (call.getArguments().length > 0)
|
|
5102
|
+
continue;
|
|
5103
|
+
const callee = call.getExpression();
|
|
5104
|
+
if (!Node.isPropertyAccessExpression(callee))
|
|
5105
|
+
continue;
|
|
5106
|
+
const receiver = callee.getExpression();
|
|
5107
|
+
if (!rootedAtParameter(receiver))
|
|
5108
|
+
continue;
|
|
5109
|
+
const yielded = this.unwrapPromiseType(call.getType());
|
|
5110
|
+
if (!yielded.isAny() && !yielded.isUnknown())
|
|
5111
|
+
continue;
|
|
5112
|
+
if (!this.typeIsFrameworkMachinery(receiver.getType()))
|
|
5113
|
+
continue;
|
|
5114
|
+
return call;
|
|
5115
|
+
}
|
|
5116
|
+
return undefined;
|
|
5117
|
+
}
|
|
5118
|
+
/**
|
|
5119
|
+
* The request contract the source states AT a body read, or null when it
|
|
5120
|
+
* states none there (carrick#807).
|
|
5121
|
+
*
|
|
5122
|
+
* Three statements count, each of them on the read itself:
|
|
5123
|
+
*
|
|
5124
|
+
* - a cast on it: `(await request.json()) as NewWidget`;
|
|
5125
|
+
* - the annotation of the binding it initialises:
|
|
5126
|
+
* `const input: NewWidget = await request.json()`;
|
|
5127
|
+
* - a schema the read, or the binding that holds it, is handed to
|
|
5128
|
+
* (`schemaConsumingRead`): the schema's input is what a caller may send.
|
|
5129
|
+
*
|
|
5130
|
+
* The first two are `typeNodeStatedAtRead`, the reading a consumer's
|
|
5131
|
+
* response read already gets. An annotation further out is not one of them:
|
|
5132
|
+
* `const saved: Saved = await save(await request.json())` types what `save`
|
|
5133
|
+
* returned, and reading it as the body would publish the wrong side of the
|
|
5134
|
+
* handler. Nor is a placeholder (`as unknown`, `Record<string, unknown>`).
|
|
5135
|
+
*/
|
|
5136
|
+
requestStatedAtBodyRead(request, read) {
|
|
5137
|
+
const statedNode = this.typeNodeStatedAtRead(read);
|
|
5138
|
+
const statedText = statedNode ? this.structuralTextFromTypeNode(statedNode) : null;
|
|
5139
|
+
if (statedText) {
|
|
5140
|
+
this.log(`Request at ${request.file_path}:${request.line_number} is the body its handler ` +
|
|
5141
|
+
'reads off the platform request, as the source types that read');
|
|
5142
|
+
return this.createInferredType(request, statedText, true, this.getNodeLocation(read));
|
|
5143
|
+
}
|
|
5144
|
+
const validated = this.schemaConsumingRead(read);
|
|
5145
|
+
if (validated) {
|
|
5146
|
+
this.log(`Request at ${request.file_path}:${request.line_number} is the body its handler ` +
|
|
5147
|
+
"reads off the platform request and validates with a schema; publishing the schema's input");
|
|
5148
|
+
return this.declaredRequestInferredType(request, validated, this.getNodeLocation(read));
|
|
5149
|
+
}
|
|
5150
|
+
return null;
|
|
5151
|
+
}
|
|
4771
5152
|
/**
|
|
4772
5153
|
* Resolve a type-annotation/type-argument node to fully-structural text,
|
|
4773
5154
|
* dropping any `Promise<…>` wrapper (`c.req.json<T>()` returns `Promise<T>`,
|
|
@@ -5220,6 +5601,14 @@ export class TypeInferrer {
|
|
|
5220
5601
|
* request type resolves `body` to `unknown`/`any`, which the useless-type
|
|
5221
5602
|
* guard rejects. So a handler that declares nothing yields null and the next
|
|
5222
5603
|
* anchor runs.
|
|
5604
|
+
*
|
|
5605
|
+
* The member has to be one the route's own annotation can have filled
|
|
5606
|
+
* (carrick#807). The platform `Request` declares `body` too, as the byte
|
|
5607
|
+
* stream every request carries, and so does each library type that extends
|
|
5608
|
+
* it. That is concrete, so it passed the useless-type guard and a handler
|
|
5609
|
+
* taking the platform request published a stream as its request contract,
|
|
5610
|
+
* ahead of the body read inside it. A `body` a library declares with one
|
|
5611
|
+
* fixed type says nothing about this route and is skipped.
|
|
5223
5612
|
*/
|
|
5224
5613
|
requestBodyFromHandlerParams(func) {
|
|
5225
5614
|
for (const param of func.getParameters()) {
|
|
@@ -5231,7 +5620,7 @@ export class TypeInferrer {
|
|
|
5231
5620
|
continue;
|
|
5232
5621
|
}
|
|
5233
5622
|
const bodySymbol = paramType.getProperty('body');
|
|
5234
|
-
if (!bodySymbol) {
|
|
5623
|
+
if (!bodySymbol || this.memberIsFixedByItsLibrary(bodySymbol)) {
|
|
5235
5624
|
continue;
|
|
5236
5625
|
}
|
|
5237
5626
|
let bodyType;
|
|
@@ -5248,6 +5637,41 @@ export class TypeInferrer {
|
|
|
5248
5637
|
}
|
|
5249
5638
|
return null;
|
|
5250
5639
|
}
|
|
5640
|
+
/**
|
|
5641
|
+
* True when a member is declared by a library, or by the platform, with a
|
|
5642
|
+
* type that names none of its declaring type's parameters: the same type on
|
|
5643
|
+
* every value, whichever route the value belongs to.
|
|
5644
|
+
*
|
|
5645
|
+
* `body: ReqBody` on a request type generic in its body is a slot, and the
|
|
5646
|
+
* handler's annotation fills it. `readonly body: ReadableStream<Uint8Array>
|
|
5647
|
+
* | null` on the platform request is not. A member the repo declares itself
|
|
5648
|
+
* is never fixed in this sense: writing `body: NewWidget` on the handler's
|
|
5649
|
+
* own request type is the annotation, and one declaration of the repo's
|
|
5650
|
+
* among several (an intersection with the platform type) decides it. A
|
|
5651
|
+
* declaration that states no type at all is left to the guards that read
|
|
5652
|
+
* the type.
|
|
5653
|
+
*/
|
|
5654
|
+
memberIsFixedByItsLibrary(member) {
|
|
5655
|
+
const declarations = member.getDeclarations();
|
|
5656
|
+
if (declarations.length === 0)
|
|
5657
|
+
return false;
|
|
5658
|
+
const program = this.project.getProgram().compilerObject;
|
|
5659
|
+
const imports = externalImportsOf(this.project);
|
|
5660
|
+
return declarations.every((declaration) => {
|
|
5661
|
+
const declaredByLibrary = isExternalOrigin(program, declaration.getSourceFile().compilerNode, this.repoRoot, imports);
|
|
5662
|
+
if (!declaredByLibrary)
|
|
5663
|
+
return false;
|
|
5664
|
+
const typeNode = Node.isPropertySignature(declaration) || Node.isPropertyDeclaration(declaration)
|
|
5665
|
+
? declaration.getTypeNode()
|
|
5666
|
+
: Node.isGetAccessorDeclaration(declaration)
|
|
5667
|
+
? declaration.getReturnTypeNode()
|
|
5668
|
+
: undefined;
|
|
5669
|
+
if (!typeNode)
|
|
5670
|
+
return false;
|
|
5671
|
+
const names = [typeNode, ...typeNode.getDescendants()].filter(Node.isIdentifier);
|
|
5672
|
+
return !names.some((name) => (name.getSymbol()?.getDeclarations() ?? []).some(Node.isTypeParameterDeclaration));
|
|
5673
|
+
});
|
|
5674
|
+
}
|
|
5251
5675
|
/**
|
|
5252
5676
|
* Anchor (b2): the contract declared by a VALIDATOR MIDDLEWARE on the
|
|
5253
5677
|
* registration (carrick#964).
|