carrick 0.3.89 → 0.3.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/auth/oauth.d.ts +8 -0
- package/dist/auth/oauth.js +65 -11
- package/dist/auth/oauth.js.map +1 -1
- package/dist/init/connect.d.ts +42 -3
- package/dist/init/connect.js +172 -113
- package/dist/init/connect.js.map +1 -1
- package/dist/init/doctor.js +2 -2
- package/dist/init/doctor.js.map +1 -1
- package/dist/init/mcp.d.ts +11 -0
- package/dist/init/mcp.js +14 -8
- package/dist/init/mcp.js.map +1 -1
- package/dist/init/output.d.ts +6 -0
- package/dist/init/output.js +37 -2
- package/dist/init/output.js.map +1 -1
- package/dist/init/projects.d.ts +56 -5
- package/dist/init/projects.js +124 -17
- package/dist/init/projects.js.map +1 -1
- package/dist/init/remove.js +2 -2
- package/dist/init/remove.js.map +1 -1
- package/dist/init/repos.d.ts +33 -0
- package/dist/init/repos.js +8 -1
- package/dist/init/repos.js.map +1 -1
- package/dist/init/run.d.ts +64 -36
- package/dist/init/run.js +268 -166
- package/dist/init/run.js.map +1 -1
- package/package.json +6 -6
- package/sidecar/dist/src/capture/api.d.ts +13 -1
- package/sidecar/dist/src/capture/check-classify.js +12 -7
- package/sidecar/dist/src/capture/check-probe.d.ts +10 -0
- package/sidecar/dist/src/capture/check-probe.js +21 -3
- package/sidecar/dist/src/capture/check.js +1 -0
- package/sidecar/dist/src/capture/index.d.ts +1 -0
- package/sidecar/dist/src/capture/index.js +1 -0
- package/sidecar/dist/src/index.js +37 -9
- package/sidecar/dist/src/retype.d.ts +67 -0
- package/sidecar/dist/src/retype.js +813 -0
- package/sidecar/dist/src/type-inferrer.d.ts +85 -1
- package/sidecar/dist/src/type-inferrer.js +384 -1
- package/sidecar/dist/src/types.d.ts +58 -2
- package/sidecar/dist/src/validators.d.ts +153 -20
- package/sidecar/dist/src/validators.js +17 -0
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* (redirects, 204s), the LLM emits null and we fall back to the containing
|
|
18
18
|
* function's return type.
|
|
19
19
|
*/
|
|
20
|
-
import { Project } from 'ts-morph';
|
|
20
|
+
import { Project, SourceFile, type CallExpression } from 'ts-morph';
|
|
21
21
|
import type { InferRequestItem, InferResult, ExtractionConfig } from './types.js';
|
|
22
22
|
/**
|
|
23
23
|
* Strongly-discriminating member names of HTTP transport machinery — the
|
|
@@ -89,6 +89,16 @@ export declare class TypeInferrer {
|
|
|
89
89
|
* Infer a single type from a request
|
|
90
90
|
*/
|
|
91
91
|
private inferSingle;
|
|
92
|
+
/**
|
|
93
|
+
* The call a `call_result` locator names, located exactly as `infer` locates
|
|
94
|
+
* it (span, else expression text near a line). The retype check
|
|
95
|
+
* (carrick#1491) rewrites this node, so it must be the node the consumer's
|
|
96
|
+
* published type came from, not a guess at the line.
|
|
97
|
+
*/
|
|
98
|
+
locateCall(request: InferRequestItem): {
|
|
99
|
+
sourceFile: SourceFile;
|
|
100
|
+
call: CallExpression;
|
|
101
|
+
} | undefined;
|
|
92
102
|
/**
|
|
93
103
|
* Get or add a source file to the project
|
|
94
104
|
*/
|
|
@@ -1088,6 +1098,80 @@ export declare class TypeInferrer {
|
|
|
1088
1098
|
* the route declares its request nowhere we can read.
|
|
1089
1099
|
*/
|
|
1090
1100
|
private requestContractFromRegistration;
|
|
1101
|
+
/**
|
|
1102
|
+
* carrick-cloud#1366, shape 1: the request contract of a parameter a KEYED
|
|
1103
|
+
* parameter decorator binds to one field of the body (`@Body('key') x: T`).
|
|
1104
|
+
*
|
|
1105
|
+
* The decorator is recognised by shape, not by name: a call decorator whose
|
|
1106
|
+
* first argument is a string literal. Every parameter of the same handler
|
|
1107
|
+
* that the SAME decorator binds by key contributes one member, so two keyed
|
|
1108
|
+
* reads of one body publish one object. A sibling the same decorator binds
|
|
1109
|
+
* WITHOUT a key (no argument, or a non-string one such as a pipe) receives
|
|
1110
|
+
* the whole body, so the contract is that sibling's type intersected with
|
|
1111
|
+
* the keyed members.
|
|
1112
|
+
*
|
|
1113
|
+
* Returns null when the located node is not a keyed parameter, so every
|
|
1114
|
+
* other request path is untouched.
|
|
1115
|
+
*/
|
|
1116
|
+
private keyedBodyParamContract;
|
|
1117
|
+
/**
|
|
1118
|
+
* The handler parameter a request locator names: the parameter itself, its
|
|
1119
|
+
* name, a decorator on it, or an identifier whose declaration is one. A
|
|
1120
|
+
* member read of a parameter (`x.length`) is not the parameter and is left
|
|
1121
|
+
* to the expression path.
|
|
1122
|
+
*/
|
|
1123
|
+
private parameterNamedBy;
|
|
1124
|
+
/**
|
|
1125
|
+
* Every call decorator on the parameter, with the string key its first
|
|
1126
|
+
* argument names (`@Body('key')` -> `{ name: 'Body', key: 'key' }`). A call
|
|
1127
|
+
* decorator with no argument, or a first argument that is not a string (a
|
|
1128
|
+
* pipe instance), binds without a key.
|
|
1129
|
+
*/
|
|
1130
|
+
private callDecoratorBindings;
|
|
1131
|
+
/**
|
|
1132
|
+
* carrick-cloud#1366, shapes 2 and 3: a request locator that names the
|
|
1133
|
+
* request CALL, or a request CONFIG argument, instead of the body.
|
|
1134
|
+
*
|
|
1135
|
+
* The body is found from the callee's DECLARED signature:
|
|
1136
|
+
*
|
|
1137
|
+
* - a body slot is a parameter named as a body (`data`, `body`, `json`)
|
|
1138
|
+
* and typed by one of the signature's own type parameters (`data?: D`);
|
|
1139
|
+
* - a config slot is a parameter typed by a generic config type whose
|
|
1140
|
+
* body-named member is typed by the config's own type parameter
|
|
1141
|
+
* (`config?: Config<D>` with `data?: D`). A generic config with no such
|
|
1142
|
+
* member (its type parameter types something else, a response mode say)
|
|
1143
|
+
* is opaque: nothing can be said about its body.
|
|
1144
|
+
*
|
|
1145
|
+
* A located CALL is taken as the request call only when its first argument
|
|
1146
|
+
* is string-typed (the URL) and one argument supplies a body through a slot;
|
|
1147
|
+
* anything else is left alone, so an awaited payload builder keeps its own
|
|
1148
|
+
* type. A located argument in a config slot yields the payload member: from
|
|
1149
|
+
* an object literal directly, from a variable through its type. A literal
|
|
1150
|
+
* that does not set the member means the call sends no body; a variable
|
|
1151
|
+
* whose type does not resolve the member, or an opaque config, abstains.
|
|
1152
|
+
*/
|
|
1153
|
+
private requestBodyInCall;
|
|
1154
|
+
/**
|
|
1155
|
+
* The body a config-slot argument supplies through `member`: an object
|
|
1156
|
+
* literal's own member, or a variable's member read off its type.
|
|
1157
|
+
*/
|
|
1158
|
+
private configArgumentBody;
|
|
1159
|
+
/**
|
|
1160
|
+
* Per parameter position of the call's resolved declaration: a body slot, a
|
|
1161
|
+
* config slot (with the member that carries the body), an opaque generic
|
|
1162
|
+
* config, or neither (undefined). Empty when the callee has no declaration.
|
|
1163
|
+
*/
|
|
1164
|
+
private requestBodySlots;
|
|
1165
|
+
/**
|
|
1166
|
+
* A generic config type's slot: `config` with the body-named member typed by
|
|
1167
|
+
* the config's own type parameter (`interface Config<D> { data?: D }`), or
|
|
1168
|
+
* `opaque_config` when the config is generic but no body-named member is
|
|
1169
|
+
* (`interface Options<R> { responseType?: R }`). Undefined for a type that
|
|
1170
|
+
* is not a generic config, and for two candidate members (ambiguous).
|
|
1171
|
+
*/
|
|
1172
|
+
private configSlot;
|
|
1173
|
+
/** The value an object literal gives `name`: an assignment's initializer or a shorthand's identifier. */
|
|
1174
|
+
private objectLiteralMemberValue;
|
|
1091
1175
|
/**
|
|
1092
1176
|
* An explicit request `InferredType` for a contract the route declares,
|
|
1093
1177
|
* carrying the provenance the reading recorded.
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* (redirects, 204s), the LLM emits null and we fall back to the containing
|
|
18
18
|
* function's return type.
|
|
19
19
|
*/
|
|
20
|
+
import * as path from 'node:path';
|
|
20
21
|
import { Node, SyntaxKind, ts, } from 'ts-morph';
|
|
21
22
|
import { validateInferRequestItem } from './validators.js';
|
|
22
23
|
import { isExternalOrigin } from './origin.js';
|
|
@@ -206,6 +207,17 @@ function classifyStatusCodes(codes) {
|
|
|
206
207
|
* request contract.
|
|
207
208
|
*/
|
|
208
209
|
const REQUEST_BODY_PARTS = new Set(['json', 'form', 'body']);
|
|
210
|
+
/**
|
|
211
|
+
* A contract a route declares, as printed text, plus the provenance the reading
|
|
212
|
+
* recorded (a coerced request member, carrick#1101). Provenance is absent, not
|
|
213
|
+
* empty, when there is nothing to say.
|
|
214
|
+
*/
|
|
215
|
+
/**
|
|
216
|
+
* carrick-cloud#1366: the parameter and config-member names a client takes a
|
|
217
|
+
* request body under. A generic slot is read as the body only under one of
|
|
218
|
+
* these names; a type parameter alone also types response modes and fallbacks.
|
|
219
|
+
*/
|
|
220
|
+
const BODY_MEMBER_NAMES = new Set(['data', 'body', 'json']);
|
|
209
221
|
/**
|
|
210
222
|
* Print a `Type` to its string form WITHOUT the compiler's default truncation.
|
|
211
223
|
*
|
|
@@ -325,6 +337,22 @@ export class TypeInferrer {
|
|
|
325
337
|
return null;
|
|
326
338
|
}
|
|
327
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* The call a `call_result` locator names, located exactly as `infer` locates
|
|
342
|
+
* it (span, else expression text near a line). The retype check
|
|
343
|
+
* (carrick#1491) rewrites this node, so it must be the node the consumer's
|
|
344
|
+
* published type came from, not a guess at the line.
|
|
345
|
+
*/
|
|
346
|
+
locateCall(request) {
|
|
347
|
+
const filePath = path.isAbsolute(request.file_path)
|
|
348
|
+
? request.file_path
|
|
349
|
+
: path.join(this.repoRoot, request.file_path);
|
|
350
|
+
const sourceFile = this.getSourceFile(filePath);
|
|
351
|
+
if (!sourceFile)
|
|
352
|
+
return undefined;
|
|
353
|
+
const call = this.resolveTargetCallExpression(sourceFile, request);
|
|
354
|
+
return call ? { sourceFile, call } : undefined;
|
|
355
|
+
}
|
|
328
356
|
/**
|
|
329
357
|
* Get or add a source file to the project
|
|
330
358
|
*/
|
|
@@ -1042,6 +1070,21 @@ export class TypeInferrer {
|
|
|
1042
1070
|
// LLM's `primary_type_symbol` schema contract extracts, and the Rust
|
|
1043
1071
|
// depth-copy's symbol-agreement guard makes a mismatched fallback inert.
|
|
1044
1072
|
// Multi-generic calls are ambiguous and anchor nothing here.
|
|
1073
|
+
//
|
|
1074
|
+
// carrick#1491: why a typed call like `client.post<{ x: number }>(url)`
|
|
1075
|
+
// publishes no comparable type. Two facts, both pinned by
|
|
1076
|
+
// test/infer-inline-call-generic.test.ts:
|
|
1077
|
+
// - when the client resolves and no wrapper rule unwrapped its envelope,
|
|
1078
|
+
// the anchor above is the ENVELOPE's own symbol, so this fallback never
|
|
1079
|
+
// runs and the text stays the whole envelope
|
|
1080
|
+
// (`ClientResponse<{ x: number; }, any>`), whose defaulted request-data
|
|
1081
|
+
// parameter is `any`;
|
|
1082
|
+
// - when the client does not resolve, this fallback runs, and it anchors
|
|
1083
|
+
// a NAMED generic only: an inline literal has no symbol, so
|
|
1084
|
+
// `primaryTypeSymbol` returns nothing and no anchor is taken.
|
|
1085
|
+
// Either way the text carries `any`, cannot anchor a literal capture, and
|
|
1086
|
+
// the pair is left unverified; the check phase's retype of the consumer
|
|
1087
|
+
// call is what judges such a call today.
|
|
1045
1088
|
if (!anchor || !this.primaryTypeSymbol(anchor.element)) {
|
|
1046
1089
|
const typeArgs = callExpr.getTypeArguments();
|
|
1047
1090
|
if (typeArgs.length === 1) {
|
|
@@ -1192,9 +1235,55 @@ export class TypeInferrer {
|
|
|
1192
1235
|
const parent = located.getParent();
|
|
1193
1236
|
located = parent.getInitializer() ?? located;
|
|
1194
1237
|
}
|
|
1238
|
+
// carrick-cloud#1366: the locator named a handler parameter that a keyed
|
|
1239
|
+
// parameter decorator binds to ONE field of the body (`@Body('key') x: T`).
|
|
1240
|
+
// The parameter's type is that field's, so publishing it states the whole
|
|
1241
|
+
// body is a `T`. The body the handler reads is an object with one member
|
|
1242
|
+
// per keyed parameter the same decorator binds.
|
|
1243
|
+
const keyedBody = this.keyedBodyParamContract(located);
|
|
1244
|
+
if (keyedBody) {
|
|
1245
|
+
this.log(`Request locator at ${request.file_path}:${request.line_number} names a parameter bound ` +
|
|
1246
|
+
'to one body field by a keyed decorator; publishing the body those fields make up');
|
|
1247
|
+
return this.declaredRequestInferredType(request, keyedBody, this.getNodeLocation(located));
|
|
1248
|
+
}
|
|
1195
1249
|
// Mirror inferCallResult: strip `await`/`as`/parens/`!` so the inner
|
|
1196
1250
|
// expression's type (not the surrounding `Promise<any>`) is read.
|
|
1197
|
-
|
|
1251
|
+
let unwrapped = this.unwrapExpressionNode(located);
|
|
1252
|
+
// carrick-cloud#1366: the locator landed on the request CALL, or on the
|
|
1253
|
+
// request CONFIG the call takes, rather than on the body. The call's type
|
|
1254
|
+
// is its response, and a config's type is the config, so either one read
|
|
1255
|
+
// as the body publishes the wrong contract. Follow the call's signature to
|
|
1256
|
+
// the argument it sends as the body.
|
|
1257
|
+
const inCall = this.requestBodyInCall(unwrapped);
|
|
1258
|
+
if (inCall.kind === 'body') {
|
|
1259
|
+
this.log(`Request locator at ${request.file_path}:${request.line_number} names ${inCall.from}; ` +
|
|
1260
|
+
'reading the body argument its signature declares');
|
|
1261
|
+
unwrapped = this.unwrapExpressionNode(inCall.node);
|
|
1262
|
+
}
|
|
1263
|
+
else if (inCall.kind === 'body_type') {
|
|
1264
|
+
this.log(`Request locator at ${request.file_path}:${request.line_number} names ${inCall.from}; ` +
|
|
1265
|
+
"reading the body off the config variable's type");
|
|
1266
|
+
return this.createInferredType(request, this.expandResolvedTypeStructural(inCall.type, typeText(inCall.type, inCall.at)), false, this.getNodeLocation(inCall.at));
|
|
1267
|
+
}
|
|
1268
|
+
else if (inCall.kind === 'abstain') {
|
|
1269
|
+
this.log(`Request locator at ${request.file_path}:${request.line_number}: ${inCall.why}; leaving unresolved`);
|
|
1270
|
+
return null;
|
|
1271
|
+
}
|
|
1272
|
+
else if (inCall.kind === 'no_body') {
|
|
1273
|
+
this.log(`Request locator at ${request.file_path}:${request.line_number} is a request config ` +
|
|
1274
|
+
'that carries no body member; the call sends no request body');
|
|
1275
|
+
const abstain = this.createInferredType(request, 'unknown', false, this.getNodeLocation(unwrapped));
|
|
1276
|
+
abstain.any_provenance = [
|
|
1277
|
+
{
|
|
1278
|
+
path: '',
|
|
1279
|
+
kind: 'unknown',
|
|
1280
|
+
reason: 'no_request_body',
|
|
1281
|
+
detail: `the located argument is the call's request config, and it sets no '${inCall.member}' ` +
|
|
1282
|
+
'member, which is where the call takes its body, so the call sends no request body',
|
|
1283
|
+
},
|
|
1284
|
+
];
|
|
1285
|
+
return abstain;
|
|
1286
|
+
}
|
|
1198
1287
|
// A `fetch` body is almost always `JSON.stringify(payload)`, whose own type
|
|
1199
1288
|
// is the useless `string`. Drill to the serialized argument so the consumer
|
|
1200
1289
|
// request shape is the payload's type, not `string`. General: any
|
|
@@ -3946,6 +4035,300 @@ export class TypeInferrer {
|
|
|
3946
4035
|
const read = this.inferRequestReadFromHandler(handler);
|
|
3947
4036
|
return read ? { text: read } : null;
|
|
3948
4037
|
}
|
|
4038
|
+
/**
|
|
4039
|
+
* carrick-cloud#1366, shape 1: the request contract of a parameter a KEYED
|
|
4040
|
+
* parameter decorator binds to one field of the body (`@Body('key') x: T`).
|
|
4041
|
+
*
|
|
4042
|
+
* The decorator is recognised by shape, not by name: a call decorator whose
|
|
4043
|
+
* first argument is a string literal. Every parameter of the same handler
|
|
4044
|
+
* that the SAME decorator binds by key contributes one member, so two keyed
|
|
4045
|
+
* reads of one body publish one object. A sibling the same decorator binds
|
|
4046
|
+
* WITHOUT a key (no argument, or a non-string one such as a pipe) receives
|
|
4047
|
+
* the whole body, so the contract is that sibling's type intersected with
|
|
4048
|
+
* the keyed members.
|
|
4049
|
+
*
|
|
4050
|
+
* Returns null when the located node is not a keyed parameter, so every
|
|
4051
|
+
* other request path is untouched.
|
|
4052
|
+
*/
|
|
4053
|
+
keyedBodyParamContract(located) {
|
|
4054
|
+
const param = this.parameterNamedBy(located);
|
|
4055
|
+
if (!param)
|
|
4056
|
+
return null;
|
|
4057
|
+
const binding = this.callDecoratorBindings(param).find((b) => b.key !== undefined);
|
|
4058
|
+
if (!binding)
|
|
4059
|
+
return null;
|
|
4060
|
+
const func = param.getParent();
|
|
4061
|
+
if (!func || !('getParameters' in func))
|
|
4062
|
+
return null;
|
|
4063
|
+
const siblings = func.getParameters();
|
|
4064
|
+
const members = [];
|
|
4065
|
+
const seen = new Set();
|
|
4066
|
+
let wholeBody;
|
|
4067
|
+
for (const sibling of siblings) {
|
|
4068
|
+
const same = this.callDecoratorBindings(sibling).filter((b) => b.name === binding.name);
|
|
4069
|
+
if (same.length === 0)
|
|
4070
|
+
continue;
|
|
4071
|
+
const keyed = same.find((b) => b.key !== undefined);
|
|
4072
|
+
if (!keyed || keyed.key === undefined) {
|
|
4073
|
+
wholeBody ??= sibling;
|
|
4074
|
+
continue;
|
|
4075
|
+
}
|
|
4076
|
+
if (seen.has(keyed.key))
|
|
4077
|
+
continue;
|
|
4078
|
+
seen.add(keyed.key);
|
|
4079
|
+
const optional = sibling.hasQuestionToken() || sibling.hasInitializer();
|
|
4080
|
+
let type = sibling.getType();
|
|
4081
|
+
if (optional)
|
|
4082
|
+
type = type.getNonNullableType();
|
|
4083
|
+
const text = this.expandResolvedTypeStructural(type, typeText(type, sibling));
|
|
4084
|
+
const key = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(keyed.key)
|
|
4085
|
+
? keyed.key
|
|
4086
|
+
: JSON.stringify(keyed.key);
|
|
4087
|
+
members.push(`${key}${optional ? '?' : ''}: ${text};`);
|
|
4088
|
+
}
|
|
4089
|
+
if (members.length === 0)
|
|
4090
|
+
return null;
|
|
4091
|
+
const keyedText = `{ ${members.join(' ')} }`;
|
|
4092
|
+
if (!wholeBody)
|
|
4093
|
+
return { text: keyedText };
|
|
4094
|
+
const wholeType = wholeBody.getType();
|
|
4095
|
+
const wholeText = this.expandResolvedTypeStructural(wholeType, typeText(wholeType, wholeBody));
|
|
4096
|
+
const operand = /^\{[\s\S]*\}$/.test(wholeText) ? wholeText : `(${wholeText})`;
|
|
4097
|
+
return { text: `${operand} & ${keyedText}` };
|
|
4098
|
+
}
|
|
4099
|
+
/**
|
|
4100
|
+
* The handler parameter a request locator names: the parameter itself, its
|
|
4101
|
+
* name, a decorator on it, or an identifier whose declaration is one. A
|
|
4102
|
+
* member read of a parameter (`x.length`) is not the parameter and is left
|
|
4103
|
+
* to the expression path.
|
|
4104
|
+
*/
|
|
4105
|
+
parameterNamedBy(located) {
|
|
4106
|
+
if (Node.isParameterDeclaration(located))
|
|
4107
|
+
return located;
|
|
4108
|
+
const decoratedParam = located.getFirstAncestor((n) => Node.isParameterDeclaration(n) &&
|
|
4109
|
+
n.getDecorators().some((d) => d === located || d.containsRange(located.getPos(), located.getEnd())));
|
|
4110
|
+
if (decoratedParam)
|
|
4111
|
+
return decoratedParam;
|
|
4112
|
+
if (!Node.isIdentifier(located))
|
|
4113
|
+
return undefined;
|
|
4114
|
+
const parent = located.getParent();
|
|
4115
|
+
if (Node.isParameterDeclaration(parent) && parent.getNameNode() === located) {
|
|
4116
|
+
return parent;
|
|
4117
|
+
}
|
|
4118
|
+
for (const def of located.getDefinitionNodes()) {
|
|
4119
|
+
if (Node.isParameterDeclaration(def))
|
|
4120
|
+
return def;
|
|
4121
|
+
}
|
|
4122
|
+
return undefined;
|
|
4123
|
+
}
|
|
4124
|
+
/**
|
|
4125
|
+
* Every call decorator on the parameter, with the string key its first
|
|
4126
|
+
* argument names (`@Body('key')` -> `{ name: 'Body', key: 'key' }`). A call
|
|
4127
|
+
* decorator with no argument, or a first argument that is not a string (a
|
|
4128
|
+
* pipe instance), binds without a key.
|
|
4129
|
+
*/
|
|
4130
|
+
callDecoratorBindings(param) {
|
|
4131
|
+
const bindings = [];
|
|
4132
|
+
for (const decorator of param.getDecorators()) {
|
|
4133
|
+
const call = decorator.getCallExpression();
|
|
4134
|
+
if (!call)
|
|
4135
|
+
continue;
|
|
4136
|
+
const first = call.getArguments()[0];
|
|
4137
|
+
if (first && (Node.isStringLiteral(first) || Node.isNoSubstitutionTemplateLiteral(first))) {
|
|
4138
|
+
bindings.push({ name: decorator.getName(), key: first.getLiteralValue() });
|
|
4139
|
+
}
|
|
4140
|
+
else {
|
|
4141
|
+
bindings.push({ name: decorator.getName() });
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
return bindings;
|
|
4145
|
+
}
|
|
4146
|
+
/**
|
|
4147
|
+
* carrick-cloud#1366, shapes 2 and 3: a request locator that names the
|
|
4148
|
+
* request CALL, or a request CONFIG argument, instead of the body.
|
|
4149
|
+
*
|
|
4150
|
+
* The body is found from the callee's DECLARED signature:
|
|
4151
|
+
*
|
|
4152
|
+
* - a body slot is a parameter named as a body (`data`, `body`, `json`)
|
|
4153
|
+
* and typed by one of the signature's own type parameters (`data?: D`);
|
|
4154
|
+
* - a config slot is a parameter typed by a generic config type whose
|
|
4155
|
+
* body-named member is typed by the config's own type parameter
|
|
4156
|
+
* (`config?: Config<D>` with `data?: D`). A generic config with no such
|
|
4157
|
+
* member (its type parameter types something else, a response mode say)
|
|
4158
|
+
* is opaque: nothing can be said about its body.
|
|
4159
|
+
*
|
|
4160
|
+
* A located CALL is taken as the request call only when its first argument
|
|
4161
|
+
* is string-typed (the URL) and one argument supplies a body through a slot;
|
|
4162
|
+
* anything else is left alone, so an awaited payload builder keeps its own
|
|
4163
|
+
* type. A located argument in a config slot yields the payload member: from
|
|
4164
|
+
* an object literal directly, from a variable through its type. A literal
|
|
4165
|
+
* that does not set the member means the call sends no body; a variable
|
|
4166
|
+
* whose type does not resolve the member, or an opaque config, abstains.
|
|
4167
|
+
*/
|
|
4168
|
+
requestBodyInCall(node) {
|
|
4169
|
+
if (Node.isCallExpression(node)) {
|
|
4170
|
+
const args = node.getArguments();
|
|
4171
|
+
if (args.length < 2)
|
|
4172
|
+
return { kind: 'none' };
|
|
4173
|
+
const first = args[0].getType();
|
|
4174
|
+
const stringLike = first.isString() ||
|
|
4175
|
+
first.isStringLiteral() ||
|
|
4176
|
+
first.isTemplateLiteral() ||
|
|
4177
|
+
(first.isUnion() && first.getUnionTypes().every((t) => t.isString() || t.isStringLiteral()));
|
|
4178
|
+
if (!stringLike)
|
|
4179
|
+
return { kind: 'none' };
|
|
4180
|
+
const slots = this.requestBodySlots(node);
|
|
4181
|
+
for (let i = 1; i < args.length; i++) {
|
|
4182
|
+
const slot = slots[i];
|
|
4183
|
+
if (!slot || slot.kind === 'opaque_config')
|
|
4184
|
+
continue;
|
|
4185
|
+
if (slot.kind === 'body') {
|
|
4186
|
+
return { kind: 'body', node: args[i], from: 'the request call' };
|
|
4187
|
+
}
|
|
4188
|
+
const fromConfig = this.configArgumentBody(args[i], slot.member, 'the request call');
|
|
4189
|
+
if (fromConfig.kind === 'body' || fromConfig.kind === 'body_type')
|
|
4190
|
+
return fromConfig;
|
|
4191
|
+
}
|
|
4192
|
+
return { kind: 'none' };
|
|
4193
|
+
}
|
|
4194
|
+
if (Node.isObjectLiteralExpression(node) || Node.isIdentifier(node)) {
|
|
4195
|
+
const call = node.getParent();
|
|
4196
|
+
if (!call || !Node.isCallExpression(call))
|
|
4197
|
+
return { kind: 'none' };
|
|
4198
|
+
const index = call.getArguments().indexOf(node);
|
|
4199
|
+
if (index < 1)
|
|
4200
|
+
return { kind: 'none' };
|
|
4201
|
+
const slot = this.requestBodySlots(call)[index];
|
|
4202
|
+
if (!slot || slot.kind === 'body')
|
|
4203
|
+
return { kind: 'none' };
|
|
4204
|
+
if (slot.kind === 'opaque_config') {
|
|
4205
|
+
return {
|
|
4206
|
+
kind: 'abstain',
|
|
4207
|
+
why: "the located argument is a request config whose declared type names no body member",
|
|
4208
|
+
};
|
|
4209
|
+
}
|
|
4210
|
+
return this.configArgumentBody(node, slot.member, "the call's request config");
|
|
4211
|
+
}
|
|
4212
|
+
return { kind: 'none' };
|
|
4213
|
+
}
|
|
4214
|
+
/**
|
|
4215
|
+
* The body a config-slot argument supplies through `member`: an object
|
|
4216
|
+
* literal's own member, or a variable's member read off its type.
|
|
4217
|
+
*/
|
|
4218
|
+
configArgumentBody(arg, member, from) {
|
|
4219
|
+
const unwrapped = this.unwrapExpressionNode(arg);
|
|
4220
|
+
if (Node.isObjectLiteralExpression(unwrapped)) {
|
|
4221
|
+
const value = this.objectLiteralMemberValue(unwrapped, member);
|
|
4222
|
+
if (value)
|
|
4223
|
+
return { kind: 'body', node: value, from };
|
|
4224
|
+
return { kind: 'no_body', member };
|
|
4225
|
+
}
|
|
4226
|
+
const property = unwrapped.getType().getProperty(member);
|
|
4227
|
+
if (property) {
|
|
4228
|
+
const type = property.getTypeAtLocation(unwrapped);
|
|
4229
|
+
// Top types first: the non-nullable form of `unknown` is `{}`.
|
|
4230
|
+
const bare = type.isAny() || type.isUnknown() ? type : type.getNonNullableType();
|
|
4231
|
+
if (!bare.isAny() && !bare.isUnknown() && !bare.isTypeParameter()) {
|
|
4232
|
+
return { kind: 'body_type', type: bare, at: unwrapped, from };
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
return {
|
|
4236
|
+
kind: 'abstain',
|
|
4237
|
+
why: `the located request config's type does not resolve its '${member}' member`,
|
|
4238
|
+
};
|
|
4239
|
+
}
|
|
4240
|
+
/**
|
|
4241
|
+
* Per parameter position of the call's resolved declaration: a body slot, a
|
|
4242
|
+
* config slot (with the member that carries the body), an opaque generic
|
|
4243
|
+
* config, or neither (undefined). Empty when the callee has no declaration.
|
|
4244
|
+
*/
|
|
4245
|
+
requestBodySlots(call) {
|
|
4246
|
+
const checker = this.project.getTypeChecker().compilerObject;
|
|
4247
|
+
let declaration;
|
|
4248
|
+
try {
|
|
4249
|
+
declaration = checker.getResolvedSignature(call.compilerNode)?.getDeclaration();
|
|
4250
|
+
}
|
|
4251
|
+
catch {
|
|
4252
|
+
return [];
|
|
4253
|
+
}
|
|
4254
|
+
if (!declaration || !('parameters' in declaration))
|
|
4255
|
+
return [];
|
|
4256
|
+
const ownTypeParams = new Set((declaration.typeParameters ?? []).map((tp) => tp.name.text));
|
|
4257
|
+
return declaration.parameters.map((param) => {
|
|
4258
|
+
if (param.dotDotDotToken)
|
|
4259
|
+
return undefined;
|
|
4260
|
+
const typeNode = param.type;
|
|
4261
|
+
if (!typeNode || !ts.isTypeReferenceNode(typeNode))
|
|
4262
|
+
return undefined;
|
|
4263
|
+
if (ts.isIdentifier(typeNode.typeName) &&
|
|
4264
|
+
!typeNode.typeArguments &&
|
|
4265
|
+
ownTypeParams.has(typeNode.typeName.text)) {
|
|
4266
|
+
return ts.isIdentifier(param.name) && BODY_MEMBER_NAMES.has(param.name.text)
|
|
4267
|
+
? { kind: 'body' }
|
|
4268
|
+
: undefined;
|
|
4269
|
+
}
|
|
4270
|
+
return this.configSlot(checker, typeNode);
|
|
4271
|
+
});
|
|
4272
|
+
}
|
|
4273
|
+
/**
|
|
4274
|
+
* A generic config type's slot: `config` with the body-named member typed by
|
|
4275
|
+
* the config's own type parameter (`interface Config<D> { data?: D }`), or
|
|
4276
|
+
* `opaque_config` when the config is generic but no body-named member is
|
|
4277
|
+
* (`interface Options<R> { responseType?: R }`). Undefined for a type that
|
|
4278
|
+
* is not a generic config, and for two candidate members (ambiguous).
|
|
4279
|
+
*/
|
|
4280
|
+
configSlot(checker, typeNode) {
|
|
4281
|
+
let symbol = checker.getSymbolAtLocation(typeNode.typeName);
|
|
4282
|
+
if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
|
|
4283
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
4284
|
+
}
|
|
4285
|
+
let generic = false;
|
|
4286
|
+
const found = new Set();
|
|
4287
|
+
for (const decl of symbol?.getDeclarations() ?? []) {
|
|
4288
|
+
if (!ts.isInterfaceDeclaration(decl) && !ts.isTypeAliasDeclaration(decl))
|
|
4289
|
+
continue;
|
|
4290
|
+
const own = new Set((decl.typeParameters ?? []).map((tp) => tp.name.text));
|
|
4291
|
+
if (own.size === 0)
|
|
4292
|
+
continue;
|
|
4293
|
+
const members = ts.isInterfaceDeclaration(decl)
|
|
4294
|
+
? decl.members
|
|
4295
|
+
: ts.isTypeLiteralNode(decl.type)
|
|
4296
|
+
? decl.type.members
|
|
4297
|
+
: undefined;
|
|
4298
|
+
if (!members)
|
|
4299
|
+
continue;
|
|
4300
|
+
generic = true;
|
|
4301
|
+
for (const member of members) {
|
|
4302
|
+
if (!ts.isPropertySignature(member) || !member.type || !member.name)
|
|
4303
|
+
continue;
|
|
4304
|
+
if (!ts.isIdentifier(member.name) && !ts.isStringLiteral(member.name))
|
|
4305
|
+
continue;
|
|
4306
|
+
if (!BODY_MEMBER_NAMES.has(member.name.text))
|
|
4307
|
+
continue;
|
|
4308
|
+
const t = member.type;
|
|
4309
|
+
if (ts.isTypeReferenceNode(t) &&
|
|
4310
|
+
ts.isIdentifier(t.typeName) &&
|
|
4311
|
+
!t.typeArguments &&
|
|
4312
|
+
own.has(t.typeName.text)) {
|
|
4313
|
+
found.add(member.name.text);
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
if (found.size === 1)
|
|
4318
|
+
return { kind: 'config', member: [...found][0] };
|
|
4319
|
+
return generic && found.size === 0 ? { kind: 'opaque_config' } : undefined;
|
|
4320
|
+
}
|
|
4321
|
+
/** The value an object literal gives `name`: an assignment's initializer or a shorthand's identifier. */
|
|
4322
|
+
objectLiteralMemberValue(literal, name) {
|
|
4323
|
+
const property = literal.getProperty(name);
|
|
4324
|
+
if (!property)
|
|
4325
|
+
return undefined;
|
|
4326
|
+
if (Node.isPropertyAssignment(property))
|
|
4327
|
+
return property.getInitializer();
|
|
4328
|
+
if (Node.isShorthandPropertyAssignment(property))
|
|
4329
|
+
return property.getNameNode();
|
|
4330
|
+
return undefined;
|
|
4331
|
+
}
|
|
3949
4332
|
/**
|
|
3950
4333
|
* An explicit request `InferredType` for a contract the route declares,
|
|
3951
4334
|
* carrying the provenance the reading recorded.
|
|
@@ -283,10 +283,41 @@ export interface ResolveDefinitionsRequest extends BaseRequest {
|
|
|
283
283
|
/** Surface type alias names to resolve */
|
|
284
284
|
aliases: string[];
|
|
285
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* One consumer call to retype with the producer's response type (carrick#1491).
|
|
288
|
+
* The locator is the one the consumer's `call_result` inference used: a span,
|
|
289
|
+
* or expression text near a line.
|
|
290
|
+
*/
|
|
291
|
+
export interface RetypeItem {
|
|
292
|
+
/** Caller key, echoed on the outcome. */
|
|
293
|
+
item_id: string;
|
|
294
|
+
/** Absolute path, or relative to the init'd root. */
|
|
295
|
+
file_path: string;
|
|
296
|
+
line_number: number;
|
|
297
|
+
span_start?: number;
|
|
298
|
+
span_end?: number;
|
|
299
|
+
expression_text?: string;
|
|
300
|
+
expression_line?: number;
|
|
301
|
+
/** The producer's response type as TypeScript text, fully inlined. */
|
|
302
|
+
producer_type: string;
|
|
303
|
+
/** Judge the form JSON puts on the wire (an `http` response). */
|
|
304
|
+
wire: boolean;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Retype consumer calls with the producer's response type and report what the
|
|
308
|
+
* consumer file's own type-check says about it. Needs `init`: it runs in the
|
|
309
|
+
* consumer's real program.
|
|
310
|
+
*/
|
|
311
|
+
export interface RetypeCheckRequest extends BaseRequest {
|
|
312
|
+
action: 'retype_check';
|
|
313
|
+
items: RetypeItem[];
|
|
314
|
+
/** Time the request may spend; items it does not reach abstain. */
|
|
315
|
+
budget_ms?: number;
|
|
316
|
+
}
|
|
286
317
|
/**
|
|
287
318
|
* Union type for all possible sidecar requests
|
|
288
319
|
*/
|
|
289
|
-
export type SidecarRequest = InitRequest | BundleRequest | EmitSurfaceRequest | CaptureV2Request | CheckV2Request | InferRequest | BuildWorkspaceRequest | CheckCompatibilityRequest | ResolveDefinitionsRequest | HealthRequest | ShutdownRequest;
|
|
320
|
+
export type SidecarRequest = RetypeCheckRequest | InitRequest | BundleRequest | EmitSurfaceRequest | CaptureV2Request | CheckV2Request | InferRequest | BuildWorkspaceRequest | CheckCompatibilityRequest | ResolveDefinitionsRequest | HealthRequest | ShutdownRequest;
|
|
290
321
|
/**
|
|
291
322
|
* Request for a specific symbol to be bundled
|
|
292
323
|
*/
|
|
@@ -467,10 +498,35 @@ export interface ErrorResponse extends BaseResponse {
|
|
|
467
498
|
status: 'error';
|
|
468
499
|
errors: string[];
|
|
469
500
|
}
|
|
501
|
+
/** A diagnostic the retype ADDED to the consumer file, at its original line. */
|
|
502
|
+
export interface RetypeDiagnostic {
|
|
503
|
+
line: number;
|
|
504
|
+
code: number;
|
|
505
|
+
message: string;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* What the consumer file's type-check said once the call stated the
|
|
509
|
+
* producer's response type.
|
|
510
|
+
*
|
|
511
|
+
* - `mismatch`: the rewrite added diagnostics; each is a place the consumer
|
|
512
|
+
* uses something the producer's response does not provide.
|
|
513
|
+
* - `agrees`: it added none.
|
|
514
|
+
* - `abstain`: the check could not be made; `reason` says why.
|
|
515
|
+
*/
|
|
516
|
+
export interface RetypeOutcome {
|
|
517
|
+
item_id: string;
|
|
518
|
+
outcome: 'mismatch' | 'agrees' | 'abstain';
|
|
519
|
+
diagnostics: RetypeDiagnostic[];
|
|
520
|
+
reason?: string;
|
|
521
|
+
}
|
|
522
|
+
export interface RetypeCheckResponse extends BaseResponse {
|
|
523
|
+
outcomes?: RetypeOutcome[];
|
|
524
|
+
errors?: string[];
|
|
525
|
+
}
|
|
470
526
|
/**
|
|
471
527
|
* Union type for all possible sidecar responses
|
|
472
528
|
*/
|
|
473
|
-
export type SidecarResponse = InitResponse | BundleResponse | EmitSurfaceResponse | CaptureV2Response | CheckV2Response | InferResponse | BuildWorkspaceResponse | CheckCompatibilityResponse | ResolveDefinitionsResponse | HealthResponse | ShutdownResponse | ErrorResponse;
|
|
529
|
+
export type SidecarResponse = RetypeCheckResponse | InitResponse | BundleResponse | EmitSurfaceResponse | CaptureV2Response | CheckV2Response | InferResponse | BuildWorkspaceResponse | CheckCompatibilityResponse | ResolveDefinitionsResponse | HealthResponse | ShutdownResponse | ErrorResponse;
|
|
474
530
|
/**
|
|
475
531
|
* An entry in the type manifest
|
|
476
532
|
*/
|