carrick 0.3.102 → 0.3.104
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 +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/bundler.d.ts +3 -92
- package/sidecar/dist/src/bundler.js +5 -265
- package/sidecar/dist/src/capture/anchors.js +113 -8
- package/sidecar/dist/src/capture/api.d.ts +6 -0
- package/sidecar/dist/src/capture/check-classify.js +27 -0
- package/sidecar/dist/src/capture/check-fields.js +19 -5
- package/sidecar/dist/src/capture/check-probe.d.ts +6 -1
- package/sidecar/dist/src/capture/check-probe.js +45 -2
- package/sidecar/dist/src/capture/check-workspace.d.ts +3 -0
- package/sidecar/dist/src/capture/check-workspace.js +13 -16
- package/sidecar/dist/src/capture/check.js +5 -5
- package/sidecar/dist/src/capture/deep-walk.js +45 -12
- package/sidecar/dist/src/capture/deno-project.d.ts +2 -1
- package/sidecar/dist/src/capture/deno-project.js +22 -18
- package/sidecar/dist/src/capture/guarded-fs.d.ts +76 -0
- package/sidecar/dist/src/capture/guarded-fs.js +182 -0
- package/sidecar/dist/src/capture/index.d.ts +1 -0
- package/sidecar/dist/src/capture/index.js +65 -27
- package/sidecar/dist/src/capture/outside-root.d.ts +41 -0
- package/sidecar/dist/src/capture/outside-root.js +101 -0
- package/sidecar/dist/src/capture/paths-rewrite.d.ts +8 -0
- package/sidecar/dist/src/capture/paths-rewrite.js +9 -7
- package/sidecar/dist/src/capture/repair-dangling.d.ts +11 -1
- package/sidecar/dist/src/capture/repair-dangling.js +15 -4
- package/sidecar/dist/src/capture/self-check.d.ts +3 -0
- package/sidecar/dist/src/capture/self-check.js +3 -3
- package/sidecar/dist/src/capture/service-config.d.ts +19 -0
- package/sidecar/dist/src/capture/service-config.js +57 -0
- package/sidecar/dist/src/index.d.ts +1 -1
- package/sidecar/dist/src/index.js +4 -115
- package/sidecar/dist/src/origin.d.ts +49 -8
- package/sidecar/dist/src/origin.js +81 -8
- package/sidecar/dist/src/project-loader.d.ts +10 -2
- package/sidecar/dist/src/project-loader.js +22 -21
- package/sidecar/dist/src/type-inferrer.d.ts +72 -0
- package/sidecar/dist/src/type-inferrer.js +248 -8
- package/sidecar/dist/src/type-structural-expander.d.ts +5 -1
- package/sidecar/dist/src/type-structural-expander.js +1 -1
- package/sidecar/dist/src/types.d.ts +39 -178
- package/sidecar/dist/src/validators.d.ts +70 -914
- package/sidecar/dist/src/validators.js +2 -55
- package/sidecar/dist/src/monorepo-builder.d.ts +0 -129
- package/sidecar/dist/src/monorepo-builder.js +0 -584
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
import { Project } from 'ts-morph';
|
|
13
13
|
import * as path from 'node:path';
|
|
14
14
|
import * as fs from 'node:fs';
|
|
15
|
-
import { DenoProject, findDenoConfig, serviceConfigPath } from './capture/index.js';
|
|
15
|
+
import { DenoProject, findDenoConfig, findServiceTsconfig, serviceConfigPath } from './capture/index.js';
|
|
16
16
|
import { moduleFormatResolutionHost } from './module-format.js';
|
|
17
|
+
import { ExternalImports, registerExternalImports } from './origin.js';
|
|
17
18
|
/**
|
|
18
19
|
* Source-file patterns used when the repo declares no tsconfig, relative to
|
|
19
20
|
* the repo root. `node_modules` is excluded explicitly: a glob that matches
|
|
@@ -124,6 +125,7 @@ export class ProjectLoader {
|
|
|
124
125
|
ownerProjects = new Map();
|
|
125
126
|
repoRoot;
|
|
126
127
|
tsconfigPath;
|
|
128
|
+
scanRoot;
|
|
127
129
|
tsconfigSnapshot;
|
|
128
130
|
pinnedDependencies;
|
|
129
131
|
initialized = false;
|
|
@@ -140,6 +142,7 @@ export class ProjectLoader {
|
|
|
140
142
|
? options.tsconfigPath
|
|
141
143
|
: path.resolve(this.repoRoot, options.tsconfigPath);
|
|
142
144
|
}
|
|
145
|
+
this.scanRoot = options.scanRoot;
|
|
143
146
|
// Store snapshot if provided
|
|
144
147
|
this.tsconfigSnapshot = options.tsconfigSnapshot;
|
|
145
148
|
this.pinnedDependencies = options.pinnedDependencies;
|
|
@@ -190,14 +193,18 @@ export class ProjectLoader {
|
|
|
190
193
|
this.buildProject = () => {
|
|
191
194
|
const deno = new DenoProject(denoConfig, this.repoRoot);
|
|
192
195
|
this.denoProject = deno;
|
|
196
|
+
// The graph's external-library verdicts, kept past the program
|
|
197
|
+
// rebuilds that drop them (carrick#1731).
|
|
198
|
+
const imports = new ExternalImports();
|
|
193
199
|
const project = new Project({
|
|
194
200
|
compilerOptions: deno.parsed.options,
|
|
195
201
|
skipAddingFilesFromTsConfig: true,
|
|
196
|
-
resolutionHost: (host, getOptions) => ({
|
|
202
|
+
resolutionHost: imports.recording((host, getOptions) => ({
|
|
197
203
|
resolveModuleNames: (names, from) => names.map(name => deno.resolve(name, from, getOptions(), host)),
|
|
198
204
|
resolveTypeReferenceDirectives: (names, from) => names.map(name => deno.resolveTypeReference(typeof name === 'string' ? name : name.fileName, from, getOptions(), host)),
|
|
199
|
-
}),
|
|
205
|
+
})),
|
|
200
206
|
});
|
|
207
|
+
registerExternalImports(project, imports);
|
|
201
208
|
for (const file of deno.parsed.fileNames)
|
|
202
209
|
project.addSourceFileAtPath(file);
|
|
203
210
|
for (const diagnostic of deno.diagnostics)
|
|
@@ -313,12 +320,17 @@ export class ProjectLoader {
|
|
|
313
320
|
}
|
|
314
321
|
/** A ts-morph project built from one tsconfig and the files it lists. */
|
|
315
322
|
projectFromConfig(configPath) {
|
|
316
|
-
|
|
323
|
+
const imports = new ExternalImports();
|
|
324
|
+
const project = new Project({
|
|
317
325
|
tsConfigFilePath: configPath,
|
|
318
326
|
skipAddingFilesFromTsConfig: false,
|
|
319
|
-
// Each import resolves in its file's own module format (carrick#1619)
|
|
320
|
-
|
|
327
|
+
// Each import resolves in its file's own module format (carrick#1619),
|
|
328
|
+
// and every external-library answer is kept past the program rebuilds
|
|
329
|
+
// that drop it (carrick#1731).
|
|
330
|
+
resolutionHost: imports.recording(moduleFormatResolutionHost),
|
|
321
331
|
});
|
|
332
|
+
registerExternalImports(project, imports);
|
|
333
|
+
return project;
|
|
322
334
|
}
|
|
323
335
|
/**
|
|
324
336
|
* Which project types `file` (carrick#1604): the key of its owning
|
|
@@ -412,30 +424,19 @@ export class ProjectLoader {
|
|
|
412
424
|
return this.pinnedDependencies;
|
|
413
425
|
}
|
|
414
426
|
/**
|
|
415
|
-
*
|
|
427
|
+
* The tsconfig to build from: the named one when it exists, else the one
|
|
428
|
+
* `findServiceTsconfig` finds for the service, which capture also reads.
|
|
416
429
|
*
|
|
417
|
-
* @returns Absolute path to tsconfig
|
|
430
|
+
* @returns Absolute path to the tsconfig, or undefined if not found
|
|
418
431
|
*/
|
|
419
432
|
findTsConfig() {
|
|
420
|
-
// If a specific path was provided, try to use it
|
|
421
433
|
if (this.tsconfigPath) {
|
|
422
434
|
if (fs.existsSync(this.tsconfigPath)) {
|
|
423
435
|
return this.tsconfigPath;
|
|
424
436
|
}
|
|
425
437
|
this.log(`Specified tsconfig not found: ${this.tsconfigPath}`);
|
|
426
438
|
}
|
|
427
|
-
|
|
428
|
-
const candidates = [
|
|
429
|
-
path.join(this.repoRoot, 'tsconfig.json'),
|
|
430
|
-
path.join(this.repoRoot, 'tsconfig.build.json'),
|
|
431
|
-
path.join(this.repoRoot, 'tsconfig.app.json'),
|
|
432
|
-
];
|
|
433
|
-
for (const candidate of candidates) {
|
|
434
|
-
if (fs.existsSync(candidate)) {
|
|
435
|
-
return candidate;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return undefined;
|
|
439
|
+
return findServiceTsconfig(this.repoRoot, this.scanRoot);
|
|
439
440
|
}
|
|
440
441
|
/**
|
|
441
442
|
* Add source files from common project locations when no tsconfig is found.
|
|
@@ -182,6 +182,25 @@ export declare class TypeInferrer {
|
|
|
182
182
|
*/
|
|
183
183
|
private resolveParamTarget;
|
|
184
184
|
private inferResponseBody;
|
|
185
|
+
/**
|
|
186
|
+
* True when a located call's own result is the route's payload, so the
|
|
187
|
+
* transitional drill into its first argument must not run (carrick#1732).
|
|
188
|
+
*
|
|
189
|
+
* `res.json(users)` reached that drill because nothing above it recognised
|
|
190
|
+
* the send: its result reads `void`, `any` or `unknown`, and the payload is
|
|
191
|
+
* the argument. `toPublicView(row)` is the opposite case: a mapper building
|
|
192
|
+
* the object the route sends. Its first argument is the row it was built
|
|
193
|
+
* FROM, which carries columns the route never sends.
|
|
194
|
+
*
|
|
195
|
+
* The call's result decides, not where its callee is declared. It is the
|
|
196
|
+
* payload when it reads as one by the rule a response helper's argument is
|
|
197
|
+
* read with (`nodeCarriesPayloadContract`: object-shaped, not machinery,
|
|
198
|
+
* not `void`/`any`/`unknown`) and the object is not a library's own: a
|
|
199
|
+
* codec's writer from `encode(message)` or a reply builder from a send is
|
|
200
|
+
* the library describing itself, and keeps the drill. A library call that
|
|
201
|
+
* returns the repo's own type (`toInstance(View, plain)`) is the payload.
|
|
202
|
+
*/
|
|
203
|
+
private callResultIsPayload;
|
|
185
204
|
/**
|
|
186
205
|
* The wire representation a request's printed type takes. A route response
|
|
187
206
|
* is serialised as JSON by every sender this layer reads a payload out of,
|
|
@@ -238,6 +257,30 @@ export declare class TypeInferrer {
|
|
|
238
257
|
*/
|
|
239
258
|
private receivingCallOf;
|
|
240
259
|
private inferCallResult;
|
|
260
|
+
/**
|
|
261
|
+
* What the source states the body read at `terminal` to be, when the type
|
|
262
|
+
* `extractExplicitTypeFromAncestor` printed for it is stated AT the read
|
|
263
|
+
* (carrick#1749): the read is the operand of that cast, or the initializer
|
|
264
|
+
* of that annotated declaration, through wrappers that leave a value as it
|
|
265
|
+
* is (parentheses, `await`, `!`, another cast). An annotation further out —
|
|
266
|
+
* the declared type of the function the read sits in — describes something
|
|
267
|
+
* else, and is not reported.
|
|
268
|
+
*/
|
|
269
|
+
private statedBodyAtRead;
|
|
270
|
+
/**
|
|
271
|
+
* A stated type with `any` or `unknown` written anywhere in it (`unknown`,
|
|
272
|
+
* `Record<string, unknown>`, `{ items: any[] }`) leaves a position open: it
|
|
273
|
+
* is a placeholder the source narrows later (`const data: unknown = await
|
|
274
|
+
* res.json()`, then `data as Entry[]`), not its statement of the body.
|
|
275
|
+
*/
|
|
276
|
+
private leavesAPositionOpen;
|
|
277
|
+
/**
|
|
278
|
+
* The named root of a stated body type: `Promise<...>` and `PromiseLike`
|
|
279
|
+
* (the language's await protocol), array levels, parentheses, `readonly`
|
|
280
|
+
* and `| null`/`| undefined` are peeled, and what is left is either a type
|
|
281
|
+
* reference, whose name is the root, or anything else, which has none.
|
|
282
|
+
*/
|
|
283
|
+
private statedRoot;
|
|
241
284
|
private inferVariable;
|
|
242
285
|
private inferExpression;
|
|
243
286
|
private inferRequestBody;
|
|
@@ -361,6 +404,33 @@ export declare class TypeInferrer {
|
|
|
361
404
|
* HTTP response is read exactly this way whatever produced the response, so
|
|
362
405
|
* the shape is structural, not a framework's name.
|
|
363
406
|
*/
|
|
407
|
+
/**
|
|
408
|
+
* The body read inside a chain that starts at `callExpr` (carrick#1749):
|
|
409
|
+
* `request(url).check(ok).mapOk(response => { const data = response as
|
|
410
|
+
* SearchResponse; ... })`.
|
|
411
|
+
*
|
|
412
|
+
* The chain is the run of member calls whose receiver is the call, then
|
|
413
|
+
* that call's result, and so on. A callback passed to one of them whose
|
|
414
|
+
* first parameter the compiler types `unknown` is handed a value nothing
|
|
415
|
+
* has typed yet, which is what a parsed body is; where the source casts
|
|
416
|
+
* that parameter, the cast is what the caller says the body is. The value
|
|
417
|
+
* the chain ends in is what the caller computed from it, and publishing
|
|
418
|
+
* that as the body is the false mismatch this rule exists for.
|
|
419
|
+
*
|
|
420
|
+
* Exactly one such cast across the whole chain answers. None, or more than
|
|
421
|
+
* one (a callback on the failure side can be handed an `unknown` too), and
|
|
422
|
+
* the walk carries on as before. A parameter typed `any` is not read: an
|
|
423
|
+
* unresolved library types every callback that way, success and failure
|
|
424
|
+
* alike, so it says nothing about which one is the body.
|
|
425
|
+
*/
|
|
426
|
+
private bodyReadInCallbackChain;
|
|
427
|
+
/**
|
|
428
|
+
* The casts of a callback's first parameter, when the compiler types that
|
|
429
|
+
* parameter `unknown`: `response as T` and `<T>response`, the operand being
|
|
430
|
+
* the parameter itself. A cast that leaves a position open states nothing
|
|
431
|
+
* about the body and is skipped.
|
|
432
|
+
*/
|
|
433
|
+
private castsOfUnreadParameter;
|
|
364
434
|
private bodyReadOnReceiver;
|
|
365
435
|
private collectDefUseNodes;
|
|
366
436
|
private expressionUsesNames;
|
|
@@ -539,6 +609,8 @@ export declare class TypeInferrer {
|
|
|
539
609
|
*/
|
|
540
610
|
private unwrapJsonStringifyArg;
|
|
541
611
|
private extractExplicitTypeFromAncestor;
|
|
612
|
+
/** The annotation `extractExplicitTypeFromAncestor` prints, as a node. */
|
|
613
|
+
private explicitTypeNodeFromAncestor;
|
|
542
614
|
/**
|
|
543
615
|
* Render an explicit annotation (`as T`, `<T>`, or a typed binding) as
|
|
544
616
|
* fully-structural text.
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { Node, SyntaxKind, ts, } from 'ts-morph';
|
|
22
22
|
import { validateInferRequestItem } from './validators.js';
|
|
23
|
-
import { isExternalOrigin } from './origin.js';
|
|
23
|
+
import { externalImportsOf, isExternalOrigin } from './origin.js';
|
|
24
24
|
import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
|
|
25
25
|
import { expandTypeStructural, } from './type-structural-expander.js';
|
|
26
26
|
/**
|
|
@@ -277,6 +277,7 @@ export class TypeInferrer {
|
|
|
277
277
|
return {
|
|
278
278
|
program: this.project.getProgram().compilerObject,
|
|
279
279
|
repoRoot: this.repoRoot,
|
|
280
|
+
imports: externalImportsOf(this.project),
|
|
280
281
|
};
|
|
281
282
|
}
|
|
282
283
|
/**
|
|
@@ -940,7 +941,9 @@ export class TypeInferrer {
|
|
|
940
941
|
this.log(`Span resolves to a callback-registration call at ${request.file_path}:${request.line_number}; no payload to infer`);
|
|
941
942
|
return null;
|
|
942
943
|
}
|
|
943
|
-
|
|
944
|
+
// A call whose result is a value the repo shapes is not a send: it is
|
|
945
|
+
// the payload (carrick#1732), and drilling would publish its input.
|
|
946
|
+
if (args.length > 0 && !this.callResultIsPayload(node)) {
|
|
944
947
|
payloadNode = args[0];
|
|
945
948
|
}
|
|
946
949
|
}
|
|
@@ -973,6 +976,31 @@ export class TypeInferrer {
|
|
|
973
976
|
const anchor = this.unwrapArrayLevels(this.unwrapPromiseType(payloadType));
|
|
974
977
|
return this.createInferredType(request, typeString, false, this.getNodeLocation(payloadNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth);
|
|
975
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* True when a located call's own result is the route's payload, so the
|
|
981
|
+
* transitional drill into its first argument must not run (carrick#1732).
|
|
982
|
+
*
|
|
983
|
+
* `res.json(users)` reached that drill because nothing above it recognised
|
|
984
|
+
* the send: its result reads `void`, `any` or `unknown`, and the payload is
|
|
985
|
+
* the argument. `toPublicView(row)` is the opposite case: a mapper building
|
|
986
|
+
* the object the route sends. Its first argument is the row it was built
|
|
987
|
+
* FROM, which carries columns the route never sends.
|
|
988
|
+
*
|
|
989
|
+
* The call's result decides, not where its callee is declared. It is the
|
|
990
|
+
* payload when it reads as one by the rule a response helper's argument is
|
|
991
|
+
* read with (`nodeCarriesPayloadContract`: object-shaped, not machinery,
|
|
992
|
+
* not `void`/`any`/`unknown`) and the object is not a library's own: a
|
|
993
|
+
* codec's writer from `encode(message)` or a reply builder from a send is
|
|
994
|
+
* the library describing itself, and keeps the drill. A library call that
|
|
995
|
+
* returns the repo's own type (`toInstance(View, plain)`) is the payload.
|
|
996
|
+
*/
|
|
997
|
+
callResultIsPayload(call) {
|
|
998
|
+
const { element } = this.unwrapArrayLevels(this.unwrapPromiseType(call.getType()));
|
|
999
|
+
if (this.symbolIsLibOrExternalOrigin(element.getSymbol() ?? element.getAliasSymbol())) {
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
return this.nodeCarriesPayloadContract(call, false);
|
|
1003
|
+
}
|
|
976
1004
|
/**
|
|
977
1005
|
* The wire representation a request's printed type takes. A route response
|
|
978
1006
|
* is serialised as JSON by every sender this layer reads a payload out of,
|
|
@@ -1309,7 +1337,132 @@ export class TypeInferrer {
|
|
|
1309
1337
|
}
|
|
1310
1338
|
}
|
|
1311
1339
|
}
|
|
1312
|
-
|
|
1340
|
+
const inferred = this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(terminalNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, anchor ? this.primaryTypeSymbol(anchor.element) : undefined, anchor?.depth);
|
|
1341
|
+
// carrick#1749: say when the text is the source's own statement of the
|
|
1342
|
+
// body, and what that statement is rooted at, so the scanner can tell a
|
|
1343
|
+
// model symbol that names the body from one that names a part of it.
|
|
1344
|
+
const statedBody = explicitType ? this.statedBodyAtRead(terminalNode) : undefined;
|
|
1345
|
+
if (statedBody) {
|
|
1346
|
+
inferred.stated_body = statedBody;
|
|
1347
|
+
}
|
|
1348
|
+
return inferred;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* What the source states the body read at `terminal` to be, when the type
|
|
1352
|
+
* `extractExplicitTypeFromAncestor` printed for it is stated AT the read
|
|
1353
|
+
* (carrick#1749): the read is the operand of that cast, or the initializer
|
|
1354
|
+
* of that annotated declaration, through wrappers that leave a value as it
|
|
1355
|
+
* is (parentheses, `await`, `!`, another cast). An annotation further out —
|
|
1356
|
+
* the declared type of the function the read sits in — describes something
|
|
1357
|
+
* else, and is not reported.
|
|
1358
|
+
*/
|
|
1359
|
+
statedBodyAtRead(terminal) {
|
|
1360
|
+
const typeNode = this.explicitTypeNodeFromAncestor(terminal);
|
|
1361
|
+
const owner = typeNode?.getParent();
|
|
1362
|
+
if (!typeNode || !owner || this.leavesAPositionOpen(typeNode))
|
|
1363
|
+
return undefined;
|
|
1364
|
+
let current = terminal;
|
|
1365
|
+
for (;;) {
|
|
1366
|
+
if (current === owner)
|
|
1367
|
+
break;
|
|
1368
|
+
const parent = current.getParent();
|
|
1369
|
+
if (!parent)
|
|
1370
|
+
return undefined;
|
|
1371
|
+
if (parent === owner &&
|
|
1372
|
+
Node.isVariableDeclaration(parent) &&
|
|
1373
|
+
parent.getInitializer() === current) {
|
|
1374
|
+
break;
|
|
1375
|
+
}
|
|
1376
|
+
if (!Node.isParenthesizedExpression(parent) &&
|
|
1377
|
+
!Node.isAwaitExpression(parent) &&
|
|
1378
|
+
!Node.isNonNullExpression(parent) &&
|
|
1379
|
+
!Node.isAsExpression(parent) &&
|
|
1380
|
+
!Node.isTypeAssertion(parent) &&
|
|
1381
|
+
!Node.isSatisfiesExpression(parent)) {
|
|
1382
|
+
return undefined;
|
|
1383
|
+
}
|
|
1384
|
+
current = parent;
|
|
1385
|
+
}
|
|
1386
|
+
return this.statedRoot(typeNode);
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* A stated type with `any` or `unknown` written anywhere in it (`unknown`,
|
|
1390
|
+
* `Record<string, unknown>`, `{ items: any[] }`) leaves a position open: it
|
|
1391
|
+
* is a placeholder the source narrows later (`const data: unknown = await
|
|
1392
|
+
* res.json()`, then `data as Entry[]`), not its statement of the body.
|
|
1393
|
+
*/
|
|
1394
|
+
leavesAPositionOpen(typeNode) {
|
|
1395
|
+
const open = (node) => node.getKind() === SyntaxKind.AnyKeyword || node.getKind() === SyntaxKind.UnknownKeyword;
|
|
1396
|
+
return open(typeNode) || typeNode.getDescendants().some(open);
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* The named root of a stated body type: `Promise<...>` and `PromiseLike`
|
|
1400
|
+
* (the language's await protocol), array levels, parentheses, `readonly`
|
|
1401
|
+
* and `| null`/`| undefined` are peeled, and what is left is either a type
|
|
1402
|
+
* reference, whose name is the root, or anything else, which has none.
|
|
1403
|
+
*/
|
|
1404
|
+
statedRoot(typeNode) {
|
|
1405
|
+
let node = typeNode;
|
|
1406
|
+
let depth = 0;
|
|
1407
|
+
for (let step = 0; step < 16; step++) {
|
|
1408
|
+
if (Node.isParenthesizedTypeNode(node)) {
|
|
1409
|
+
node = node.getTypeNode();
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
if (Node.isArrayTypeNode(node)) {
|
|
1413
|
+
node = node.getElementTypeNode();
|
|
1414
|
+
depth++;
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
if (Node.isTypeOperatorTypeNode(node) &&
|
|
1418
|
+
node.getOperator() === SyntaxKind.ReadonlyKeyword) {
|
|
1419
|
+
node = node.getTypeNode();
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
if (Node.isUnionTypeNode(node)) {
|
|
1423
|
+
const present = node
|
|
1424
|
+
.getTypeNodes()
|
|
1425
|
+
.filter((member) => !((Node.isLiteralTypeNode(member) &&
|
|
1426
|
+
member.getLiteral().getKind() === SyntaxKind.NullKeyword) ||
|
|
1427
|
+
member.getKind() === SyntaxKind.UndefinedKeyword ||
|
|
1428
|
+
member.getKind() === SyntaxKind.NullKeyword));
|
|
1429
|
+
if (present.length === 1) {
|
|
1430
|
+
node = present[0];
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
break;
|
|
1434
|
+
}
|
|
1435
|
+
if (Node.isTypeReference(node)) {
|
|
1436
|
+
const name = node.getTypeName().getText();
|
|
1437
|
+
const args = node.getTypeArguments();
|
|
1438
|
+
if (args.length === 1 && (name === 'Promise' || name === 'PromiseLike')) {
|
|
1439
|
+
node = args[0];
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
if (args.length === 1 && (name === 'Array' || name === 'ReadonlyArray')) {
|
|
1443
|
+
node = args[0];
|
|
1444
|
+
depth++;
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
break;
|
|
1449
|
+
}
|
|
1450
|
+
const depthField = depth > 0 ? { array_depth: depth } : {};
|
|
1451
|
+
if (!Node.isTypeReference(node)) {
|
|
1452
|
+
return depthField;
|
|
1453
|
+
}
|
|
1454
|
+
const typeName = node.getTypeName();
|
|
1455
|
+
const nameNode = Node.isQualifiedName(typeName) ? typeName.getRight() : typeName;
|
|
1456
|
+
let symbol = nameNode.getSymbol();
|
|
1457
|
+
if (symbol?.isAlias()) {
|
|
1458
|
+
symbol = symbol.getAliasedSymbol() ?? symbol;
|
|
1459
|
+
}
|
|
1460
|
+
const source = symbol?.getDeclarations()[0]?.getSourceFile().getFilePath();
|
|
1461
|
+
return {
|
|
1462
|
+
root: nameNode.getText(),
|
|
1463
|
+
...(source ? { root_source: source } : {}),
|
|
1464
|
+
...depthField,
|
|
1465
|
+
};
|
|
1313
1466
|
}
|
|
1314
1467
|
inferVariable(sourceFile, request, extractionConfig) {
|
|
1315
1468
|
const node = this.resolveTargetNode(sourceFile, request);
|
|
@@ -1632,6 +1785,14 @@ export class TypeInferrer {
|
|
|
1632
1785
|
// Call Result Resolution
|
|
1633
1786
|
// ===========================================================================
|
|
1634
1787
|
resolveCallResultTerminalNode(callExpr, func) {
|
|
1788
|
+
// carrick#1749: the call starts a chain, and a callback in it is handed
|
|
1789
|
+
// the body unread and casts it. That cast is the body read, and it comes
|
|
1790
|
+
// before the return-statement answer below, which for a chain is the
|
|
1791
|
+
// value the caller computed from the body, not the body.
|
|
1792
|
+
const chainRead = this.bodyReadInCallbackChain(callExpr);
|
|
1793
|
+
if (chainRead) {
|
|
1794
|
+
return { terminal: chainRead, projectionOnly: false, projections: [] };
|
|
1795
|
+
}
|
|
1635
1796
|
const returnStmt = callExpr.getFirstAncestorByKind(SyntaxKind.ReturnStatement);
|
|
1636
1797
|
if (returnStmt) {
|
|
1637
1798
|
const returnExpr = returnStmt.getExpression();
|
|
@@ -2094,6 +2255,80 @@ export class TypeInferrer {
|
|
|
2094
2255
|
* HTTP response is read exactly this way whatever produced the response, so
|
|
2095
2256
|
* the shape is structural, not a framework's name.
|
|
2096
2257
|
*/
|
|
2258
|
+
/**
|
|
2259
|
+
* The body read inside a chain that starts at `callExpr` (carrick#1749):
|
|
2260
|
+
* `request(url).check(ok).mapOk(response => { const data = response as
|
|
2261
|
+
* SearchResponse; ... })`.
|
|
2262
|
+
*
|
|
2263
|
+
* The chain is the run of member calls whose receiver is the call, then
|
|
2264
|
+
* that call's result, and so on. A callback passed to one of them whose
|
|
2265
|
+
* first parameter the compiler types `unknown` is handed a value nothing
|
|
2266
|
+
* has typed yet, which is what a parsed body is; where the source casts
|
|
2267
|
+
* that parameter, the cast is what the caller says the body is. The value
|
|
2268
|
+
* the chain ends in is what the caller computed from it, and publishing
|
|
2269
|
+
* that as the body is the false mismatch this rule exists for.
|
|
2270
|
+
*
|
|
2271
|
+
* Exactly one such cast across the whole chain answers. None, or more than
|
|
2272
|
+
* one (a callback on the failure side can be handed an `unknown` too), and
|
|
2273
|
+
* the walk carries on as before. A parameter typed `any` is not read: an
|
|
2274
|
+
* unresolved library types every callback that way, success and failure
|
|
2275
|
+
* alike, so it says nothing about which one is the body.
|
|
2276
|
+
*/
|
|
2277
|
+
bodyReadInCallbackChain(callExpr) {
|
|
2278
|
+
const reads = [];
|
|
2279
|
+
let receiver = callExpr;
|
|
2280
|
+
for (let link = 0; link < 64; link++) {
|
|
2281
|
+
const access = receiver.getParent();
|
|
2282
|
+
if (!access ||
|
|
2283
|
+
!Node.isPropertyAccessExpression(access) ||
|
|
2284
|
+
access.getExpression() !== receiver) {
|
|
2285
|
+
break;
|
|
2286
|
+
}
|
|
2287
|
+
const call = access.getParent();
|
|
2288
|
+
if (!call || !Node.isCallExpression(call) || call.getExpression() !== access) {
|
|
2289
|
+
break;
|
|
2290
|
+
}
|
|
2291
|
+
for (const arg of call.getArguments()) {
|
|
2292
|
+
if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {
|
|
2293
|
+
reads.push(...this.castsOfUnreadParameter(arg));
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
receiver = call;
|
|
2297
|
+
}
|
|
2298
|
+
return reads.length === 1 ? reads[0] : undefined;
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* The casts of a callback's first parameter, when the compiler types that
|
|
2302
|
+
* parameter `unknown`: `response as T` and `<T>response`, the operand being
|
|
2303
|
+
* the parameter itself. A cast that leaves a position open states nothing
|
|
2304
|
+
* about the body and is skipped.
|
|
2305
|
+
*/
|
|
2306
|
+
castsOfUnreadParameter(callback) {
|
|
2307
|
+
const param = callback.getParameters()[0];
|
|
2308
|
+
const name = param?.getNameNode();
|
|
2309
|
+
if (!param || !name || !Node.isIdentifier(name) || !param.getType().isUnknown()) {
|
|
2310
|
+
return [];
|
|
2311
|
+
}
|
|
2312
|
+
const symbol = name.getSymbol();
|
|
2313
|
+
if (!symbol)
|
|
2314
|
+
return [];
|
|
2315
|
+
return callback.getDescendants().filter((node) => {
|
|
2316
|
+
if (!Node.isAsExpression(node) && !Node.isTypeAssertion(node))
|
|
2317
|
+
return false;
|
|
2318
|
+
let operand = node.getExpression();
|
|
2319
|
+
while (Node.isParenthesizedExpression(operand)) {
|
|
2320
|
+
operand = operand.getExpression();
|
|
2321
|
+
}
|
|
2322
|
+
if (!Node.isIdentifier(operand) || operand.getSymbol() !== symbol)
|
|
2323
|
+
return false;
|
|
2324
|
+
const stated = node.getType();
|
|
2325
|
+
const statedNode = node.getTypeNode();
|
|
2326
|
+
return (!!statedNode &&
|
|
2327
|
+
!stated.isAny() &&
|
|
2328
|
+
!stated.isUnknown() &&
|
|
2329
|
+
!this.leavesAPositionOpen(statedNode));
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2097
2332
|
bodyReadOnReceiver(identifier) {
|
|
2098
2333
|
const access = identifier.getParent();
|
|
2099
2334
|
if (!access ||
|
|
@@ -2610,7 +2845,7 @@ export class TypeInferrer {
|
|
|
2610
2845
|
}
|
|
2611
2846
|
const program = this.project.getProgram().compilerObject;
|
|
2612
2847
|
for (const decl of symbol.getDeclarations()) {
|
|
2613
|
-
if (isExternalOrigin(program, decl.getSourceFile().compilerNode, this.repoRoot)) {
|
|
2848
|
+
if (isExternalOrigin(program, decl.getSourceFile().compilerNode, this.repoRoot, externalImportsOf(this.project))) {
|
|
2614
2849
|
return true;
|
|
2615
2850
|
}
|
|
2616
2851
|
}
|
|
@@ -2796,11 +3031,16 @@ export class TypeInferrer {
|
|
|
2796
3031
|
return this.unwrapExpressionNode(args[0]);
|
|
2797
3032
|
}
|
|
2798
3033
|
extractExplicitTypeFromAncestor(node) {
|
|
3034
|
+
const typeNode = this.explicitTypeNodeFromAncestor(node);
|
|
3035
|
+
return typeNode ? this.expandAnnotationTypeNode(typeNode) : null;
|
|
3036
|
+
}
|
|
3037
|
+
/** The annotation `extractExplicitTypeFromAncestor` prints, as a node. */
|
|
3038
|
+
explicitTypeNodeFromAncestor(node) {
|
|
2799
3039
|
const varDecl = node.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
|
|
2800
3040
|
if (varDecl) {
|
|
2801
3041
|
const typeNode = varDecl.getTypeNode();
|
|
2802
3042
|
if (typeNode) {
|
|
2803
|
-
return
|
|
3043
|
+
return typeNode;
|
|
2804
3044
|
}
|
|
2805
3045
|
}
|
|
2806
3046
|
// Consider the node ITSELF as well as its ancestors: the `call_result`
|
|
@@ -2813,7 +3053,7 @@ export class TypeInferrer {
|
|
|
2813
3053
|
if (asExpr) {
|
|
2814
3054
|
const typeNode = asExpr.getTypeNode();
|
|
2815
3055
|
if (typeNode) {
|
|
2816
|
-
return
|
|
3056
|
+
return typeNode;
|
|
2817
3057
|
}
|
|
2818
3058
|
}
|
|
2819
3059
|
const typeAssertion = Node.isTypeAssertion(node)
|
|
@@ -2822,10 +3062,10 @@ export class TypeInferrer {
|
|
|
2822
3062
|
if (typeAssertion) {
|
|
2823
3063
|
const typeNode = typeAssertion.getTypeNode();
|
|
2824
3064
|
if (typeNode) {
|
|
2825
|
-
return
|
|
3065
|
+
return typeNode;
|
|
2826
3066
|
}
|
|
2827
3067
|
}
|
|
2828
|
-
return
|
|
3068
|
+
return undefined;
|
|
2829
3069
|
}
|
|
2830
3070
|
/**
|
|
2831
3071
|
* Render an explicit annotation (`as T`, `<T>`, or a typed binding) as
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* structural form rather than a dangling name.
|
|
30
30
|
*/
|
|
31
31
|
import { type Node, type Type, ts } from 'ts-morph';
|
|
32
|
+
import { type ExternalImports } from './origin.js';
|
|
32
33
|
/**
|
|
33
34
|
* Bound on the structural-expansion recursion. Deep enough for every realistic
|
|
34
35
|
* request/response shape; a backstop against pathological/recursive types the
|
|
@@ -83,11 +84,14 @@ export type WireFormat = 'declared' | 'json';
|
|
|
83
84
|
* its own cache leaves no such segment in the path (carrick#1264). The program
|
|
84
85
|
* carries the resolver's own verdict, so it is what `isExternalOrigin` is
|
|
85
86
|
* asked — the same instrument the inference path uses, so the two layers
|
|
86
|
-
* cannot disagree about which types to inline.
|
|
87
|
+
* cannot disagree about which types to inline. `imports` is that verdict kept
|
|
88
|
+
* past the program rebuilds that drop it (carrick#1731), for a project the
|
|
89
|
+
* loader built.
|
|
87
90
|
*/
|
|
88
91
|
export interface ExpandOrigin {
|
|
89
92
|
readonly program: ts.Program;
|
|
90
93
|
readonly repoRoot: string;
|
|
94
|
+
readonly imports?: ExternalImports;
|
|
91
95
|
}
|
|
92
96
|
/** Everything `expandTypeStructural` takes besides the type and its origin. */
|
|
93
97
|
export interface ExpandOptions {
|
|
@@ -366,7 +366,7 @@ function isLibraryType(type, origin) {
|
|
|
366
366
|
const decls = symbol.getDeclarations();
|
|
367
367
|
if (decls.length === 0)
|
|
368
368
|
return false;
|
|
369
|
-
return decls.some((decl) => isExternalOrigin(origin.program, decl.getSourceFile().compilerNode, origin.repoRoot));
|
|
369
|
+
return decls.some((decl) => isExternalOrigin(origin.program, decl.getSourceFile().compilerNode, origin.repoRoot, origin.imports));
|
|
370
370
|
}
|
|
371
371
|
/**
|
|
372
372
|
* Non-expanded text for a type. Passes `undefined` as the enclosing node so
|