carrick 0.3.124 → 0.3.125
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/capture/check-classify.js +11 -5
- package/sidecar/dist/src/capture/check.js +8 -1
- package/sidecar/dist/src/capture/deep-walk.d.ts +31 -0
- package/sidecar/dist/src/capture/deep-walk.js +43 -1
- package/sidecar/dist/src/capture/index.d.ts +1 -1
- package/sidecar/dist/src/capture/index.js +1 -1
- package/sidecar/dist/src/capture/service-config.d.ts +11 -0
- package/sidecar/dist/src/capture/service-config.js +40 -0
- package/sidecar/dist/src/project-loader.d.ts +16 -0
- package/sidecar/dist/src/project-loader.js +51 -4
- package/sidecar/dist/src/type-inferrer.d.ts +56 -15
- package/sidecar/dist/src/type-inferrer.js +307 -67
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carrick",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.125",
|
|
4
4
|
"carrickCache": {
|
|
5
5
|
"cache_version": 22,
|
|
6
6
|
"intent_cache_version": 2
|
|
@@ -113,11 +113,11 @@
|
|
|
113
113
|
"zod": "^3.23.0"
|
|
114
114
|
},
|
|
115
115
|
"optionalDependencies": {
|
|
116
|
-
"@carrick-tools/cli-darwin-arm64": "0.3.
|
|
117
|
-
"@carrick-tools/cli-darwin-x64": "0.3.
|
|
118
|
-
"@carrick-tools/cli-linux-arm64": "0.3.
|
|
119
|
-
"@carrick-tools/cli-linux-x64": "0.3.
|
|
120
|
-
"@carrick-tools/cli-win32-x64": "0.3.
|
|
116
|
+
"@carrick-tools/cli-darwin-arm64": "0.3.125",
|
|
117
|
+
"@carrick-tools/cli-darwin-x64": "0.3.125",
|
|
118
|
+
"@carrick-tools/cli-linux-arm64": "0.3.125",
|
|
119
|
+
"@carrick-tools/cli-linux-x64": "0.3.125",
|
|
120
|
+
"@carrick-tools/cli-win32-x64": "0.3.125"
|
|
121
121
|
},
|
|
122
122
|
"devDependencies": {
|
|
123
123
|
"@types/node": "^24.13.3"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carrick",
|
|
3
3
|
"description": "Claude Code plugin for Carrick, which indexes TypeScript codebases across service and repository boundaries. After each edit it adds the file's routes, calls, and cross-service type mismatches to the session, and it registers Carrick's language server. It pairs with the Carrick MCP server, which lets agents search functions by intent rather than name.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.125",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Carrick",
|
|
7
7
|
"email": "hello@carrick.tools"
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { decisiveAssignmentLine, unwidenedAliasOf, } from './check-probe.js';
|
|
31
31
|
import { scrubDiagnostic, scrubPaths } from './check-scrub.js';
|
|
32
|
+
import { onlyDeclaredOpenAny } from './deep-walk.js';
|
|
32
33
|
import { describeFieldReport, fieldReportNotes, } from './check-fields.js';
|
|
33
34
|
const PRIMARY_RE = /^(?<file>(?:[a-zA-Z]:)?[^(]*?)\((?<line>\d+),(?<col>\d+)\): error TS(?<code>\d+): (?<msg>.*)$/;
|
|
34
35
|
/** Parse `tsc --pretty false` output into structured diagnostics. */
|
|
@@ -350,9 +351,10 @@ function onUnwidenedLines(plan, diagnostic) {
|
|
|
350
351
|
* - its own declarations typecheck (the alias is not poisoned);
|
|
351
352
|
* - the member-level walk ran over it and found no `any`/`unknown`, which
|
|
352
353
|
* would assign to whatever the other side declares;
|
|
353
|
-
* - the same walk found none on the two published sides either
|
|
354
|
-
* that is not a fact stays the
|
|
355
|
-
* clearing it against a member that
|
|
354
|
+
* - the same walk found none on the two published sides either, an open
|
|
355
|
+
* member included (carrick#2591). A mismatch that is not a fact stays the
|
|
356
|
+
* mismatch it was: the reading would be clearing it against a member that
|
|
357
|
+
* accepts anything.
|
|
356
358
|
*/
|
|
357
359
|
function unwidenedReadingFits(input) {
|
|
358
360
|
const { plan, probeDiags, poisonReason } = input;
|
|
@@ -364,7 +366,7 @@ function unwidenedReadingFits(input) {
|
|
|
364
366
|
if (poisonReason(sent.service_name, unwidenedAliasOf(sent.alias)))
|
|
365
367
|
return false;
|
|
366
368
|
const walked = input.deepFindings?.sentUnwidened;
|
|
367
|
-
return walked !== undefined && walked.length === 0 && factness(input).resolved;
|
|
369
|
+
return walked !== undefined && walked.length === 0 && factness(input, true).resolved;
|
|
368
370
|
}
|
|
369
371
|
/**
|
|
370
372
|
* The field-level sentence appended to a mismatch diagnostic
|
|
@@ -410,7 +412,7 @@ function namedFields(input, scrubCtx) {
|
|
|
410
412
|
* empty. A walk that could not run leaves the verdict not-a-fact: absence of
|
|
411
413
|
* findings is not evidence.
|
|
412
414
|
*/
|
|
413
|
-
function factness(input) {
|
|
415
|
+
function factness(input, strict = false) {
|
|
414
416
|
const findings = input.deepFindings;
|
|
415
417
|
if (!findings) {
|
|
416
418
|
return {
|
|
@@ -419,6 +421,10 @@ function factness(input) {
|
|
|
419
421
|
};
|
|
420
422
|
}
|
|
421
423
|
for (const side of ['sent', 'expected']) {
|
|
424
|
+
// Open members only (carrick#2591): the closed members were judged and an
|
|
425
|
+
// open one is never a mismatch, so the verdict is a fact about the type.
|
|
426
|
+
if (!strict && onlyDeclaredOpenAny(findings[side]))
|
|
427
|
+
continue;
|
|
422
428
|
const first = findings[side][0];
|
|
423
429
|
if (!first)
|
|
424
430
|
continue;
|
|
@@ -25,6 +25,7 @@ import { scrubPaths } from './check-scrub.js';
|
|
|
25
25
|
import { assembleWorkspace, writeProbes, } from './check-workspace.js';
|
|
26
26
|
import { buildPoisonIndexes } from './check-poison.js';
|
|
27
27
|
import { openProbeProgram, probeDeepFindings } from './check-deep.js';
|
|
28
|
+
import { onlyDeclaredOpenAny } from './deep-walk.js';
|
|
28
29
|
import { pairFieldReports } from './check-fields.js';
|
|
29
30
|
import { pairDispatchUnions } from './check-union.js';
|
|
30
31
|
function runProcess(command, args, cwd, env) {
|
|
@@ -465,7 +466,13 @@ function deepDecayOf(plan, aliasRecords) {
|
|
|
465
466
|
const record = aliasRecords.get(endpoint.service_name)?.get(endpoint.alias);
|
|
466
467
|
// The capture emits every deep finding it made; the FIRST is the one the
|
|
467
468
|
// pre-gate reads, exactly as when the walk stopped at the first hit.
|
|
468
|
-
const
|
|
469
|
+
const findings = record?.any_provenance ?? [];
|
|
470
|
+
// An `any` the source declares under a named member is an open member
|
|
471
|
+
// (carrick#2591): the pair is probed, and the compiler judges the closed
|
|
472
|
+
// members. Anything else on the record keeps the pair out of the probe.
|
|
473
|
+
if (onlyDeclaredOpenAny(findings))
|
|
474
|
+
continue;
|
|
475
|
+
const finding = findings[0];
|
|
469
476
|
if (finding) {
|
|
470
477
|
return { side, kind: finding.kind, path: finding.path };
|
|
471
478
|
}
|
|
@@ -20,6 +20,14 @@ import type { TypeProvenance } from './api.js';
|
|
|
20
20
|
* within its depth/node budget, treated as unverifiable (fail closed) rather
|
|
21
21
|
* than silently clean. */
|
|
22
22
|
export type DeepTopType = Pick<TypeProvenance, 'kind' | 'path'>;
|
|
23
|
+
/** Cap on findings reported per alias. The FIRST one is what the check phase
|
|
24
|
+
* pre-gates on, so verdicts never depend on this number; the rest are there to
|
|
25
|
+
* tell a reader which fields are `any` (carrick#376), and a type with more
|
|
26
|
+
* than this many is already better described by "this type is not typed".
|
|
27
|
+
* The scanner mirrors it (`CAPTURE_FINDINGS_CAP` in
|
|
28
|
+
* `src/engine/type_compat_v2.rs`): a record this long may have stopped
|
|
29
|
+
* listing, so it cannot vouch for every position (carrick#1752). */
|
|
30
|
+
export declare const MAX_DEEP_FINDINGS = 32;
|
|
23
31
|
/**
|
|
24
32
|
* Depth-bounded structural walk for a disqualifying top type at ANY depth:
|
|
25
33
|
* a member, array element, index signature, type argument, or callable RETURN
|
|
@@ -76,6 +84,29 @@ export declare function findUncarriedPositions(root: ts.Type, program: ts.Progra
|
|
|
76
84
|
/** TypeScript's unresolved-reference placeholder: `TypeFlags.Any` with the
|
|
77
85
|
* internal `intrinsicName === 'error'` (stable since TS 1.x; see `anchors.ts`). */
|
|
78
86
|
export declare function isErrorPlaceholder(t: ts.Type): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* True when a deep-walk path passes through a named member (the same rule as
|
|
89
|
+
* `path_names_a_member` in `src/engine/type_compat_v2.rs`). The walk writes a
|
|
90
|
+
* member as its name, an element or type argument as `<n>`, an index signature
|
|
91
|
+
* as `[index]` and a callable return as `()`; a path made of the last three
|
|
92
|
+
* alone sits at the root of the type or inside a container, never in a field.
|
|
93
|
+
*/
|
|
94
|
+
export declare function pathNamesAMember(path: string): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* True when every finding is an `any` the SOURCE declares under a named
|
|
97
|
+
* member (carrick#2591): an OPEN member. The policy: an open member is never
|
|
98
|
+
* a mismatch, and the closed members are judged as for a fully typed pair.
|
|
99
|
+
* `any` accepts and supplies every shape in the assignment the probe makes, so
|
|
100
|
+
* the compiler already judges only the closed members.
|
|
101
|
+
*
|
|
102
|
+
* Nothing else qualifies. A callable's return (`getData()`) is no data
|
|
103
|
+
* member. An `unknown` member rejects a narrower counterparty
|
|
104
|
+
* under assignment, so it would read as a mismatch the policy forbids; a
|
|
105
|
+
* `budget_exhausted` sentinel, a root top type and an `any` an unresolved
|
|
106
|
+
* import left say nothing about the author's intent. A list as long as the
|
|
107
|
+
* walk's cap may have stopped listing, so it cannot vouch for every position.
|
|
108
|
+
*/
|
|
109
|
+
export declare function onlyDeclaredOpenAny(findings: readonly TypeProvenance[]): boolean;
|
|
79
110
|
/** What an anchor's SOURCE program could not resolve (carrick#1164). */
|
|
80
111
|
export interface UnresolvedAtAnchor {
|
|
81
112
|
/** Member paths holding the unresolved-reference placeholder. */
|
|
@@ -22,7 +22,7 @@ import { memberPath } from './member-name.js';
|
|
|
22
22
|
* The scanner mirrors it (`CAPTURE_FINDINGS_CAP` in
|
|
23
23
|
* `src/engine/type_compat_v2.rs`): a record this long may have stopped
|
|
24
24
|
* listing, so it cannot vouch for every position (carrick#1752). */
|
|
25
|
-
const MAX_DEEP_FINDINGS = 32;
|
|
25
|
+
export const MAX_DEEP_FINDINGS = 32;
|
|
26
26
|
/**
|
|
27
27
|
* Depth-bounded structural walk for a disqualifying top type at ANY depth:
|
|
28
28
|
* a member, array element, index signature, type argument, or callable RETURN
|
|
@@ -269,6 +269,48 @@ function walkTopTypes(root, program, checker, location, flagOf) {
|
|
|
269
269
|
rest.sort((a, b) => a.path.localeCompare(b.path));
|
|
270
270
|
return [head, ...rest];
|
|
271
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* True when a deep-walk path passes through a named member (the same rule as
|
|
274
|
+
* `path_names_a_member` in `src/engine/type_compat_v2.rs`). The walk writes a
|
|
275
|
+
* member as its name, an element or type argument as `<n>`, an index signature
|
|
276
|
+
* as `[index]` and a callable return as `()`; a path made of the last three
|
|
277
|
+
* alone sits at the root of the type or inside a container, never in a field.
|
|
278
|
+
*/
|
|
279
|
+
export function pathNamesAMember(path) {
|
|
280
|
+
const rest = path.replaceAll('[index]', '').replaceAll('()', '');
|
|
281
|
+
let inArgument = false;
|
|
282
|
+
for (const c of rest) {
|
|
283
|
+
if (c === '<')
|
|
284
|
+
inArgument = true;
|
|
285
|
+
else if (c === '>')
|
|
286
|
+
inArgument = false;
|
|
287
|
+
else if (c !== '.' && !inArgument)
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* True when every finding is an `any` the SOURCE declares under a named
|
|
294
|
+
* member (carrick#2591): an OPEN member. The policy: an open member is never
|
|
295
|
+
* a mismatch, and the closed members are judged as for a fully typed pair.
|
|
296
|
+
* `any` accepts and supplies every shape in the assignment the probe makes, so
|
|
297
|
+
* the compiler already judges only the closed members.
|
|
298
|
+
*
|
|
299
|
+
* Nothing else qualifies. A callable's return (`getData()`) is no data
|
|
300
|
+
* member. An `unknown` member rejects a narrower counterparty
|
|
301
|
+
* under assignment, so it would read as a mismatch the policy forbids; a
|
|
302
|
+
* `budget_exhausted` sentinel, a root top type and an `any` an unresolved
|
|
303
|
+
* import left say nothing about the author's intent. A list as long as the
|
|
304
|
+
* walk's cap may have stopped listing, so it cannot vouch for every position.
|
|
305
|
+
*/
|
|
306
|
+
export function onlyDeclaredOpenAny(findings) {
|
|
307
|
+
return (findings.length > 0 &&
|
|
308
|
+
findings.length < MAX_DEEP_FINDINGS &&
|
|
309
|
+
findings.every((f) => f.kind === 'any' &&
|
|
310
|
+
f.reason === 'declared' &&
|
|
311
|
+
pathNamesAMember(f.path) &&
|
|
312
|
+
!f.path.includes('()')));
|
|
313
|
+
}
|
|
272
314
|
/** The disqualifier the self-check and the check phase gate on (see
|
|
273
315
|
* `findDisqualifyingTopTypes`). */
|
|
274
316
|
function disqualifyingFlag(t) {
|
|
@@ -34,7 +34,7 @@ export { sidecarCompilerOptions } from './compiler-options.js';
|
|
|
34
34
|
export { isInsideCheckout, untrustedReadBound, UNTRUSTED_TREE_ENV, type ReadBound } from './checkout.js';
|
|
35
35
|
export { serviceConfigPath } from './project-references.js';
|
|
36
36
|
export { referenceRedirectOf } from './reference-redirect.js';
|
|
37
|
-
export { findServiceTsconfig } from './service-config.js';
|
|
37
|
+
export { findServiceTsconfig, fileConfigPath } from './service-config.js';
|
|
38
38
|
export { WriteGuard } from './guarded-fs.js';
|
|
39
39
|
export { installedPackageDirectory } from './installed-package-directory.js';
|
|
40
40
|
export { isDeclarationFileName, packageNameOf } from './specifiers.js';
|
|
@@ -52,7 +52,7 @@ export { sidecarCompilerOptions } from './compiler-options.js';
|
|
|
52
52
|
export { isInsideCheckout, untrustedReadBound, UNTRUSTED_TREE_ENV } from './checkout.js';
|
|
53
53
|
export { serviceConfigPath } from './project-references.js';
|
|
54
54
|
export { referenceRedirectOf } from './reference-redirect.js';
|
|
55
|
-
export { findServiceTsconfig } from './service-config.js';
|
|
55
|
+
export { findServiceTsconfig, fileConfigPath } from './service-config.js';
|
|
56
56
|
// carrick#2405: the per-position rows write through the guard, and name an
|
|
57
57
|
// installed declaring file the way the capture's resolution walk does.
|
|
58
58
|
export { WriteGuard } from './guarded-fs.js';
|
|
@@ -19,3 +19,14 @@
|
|
|
19
19
|
export declare function findServiceTsconfig(serviceRoot: string, scanRoot?: string): string | undefined;
|
|
20
20
|
/** The path with every symlink resolved; the path as given when it does not exist. */
|
|
21
21
|
export declare function realPath(p: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* The config an editor types `file` under, for a service with none at its
|
|
24
|
+
* root (carrick#1604): the nearest `tsconfig.json`, then `jsconfig.json`, in
|
|
25
|
+
* each directory from the file's up to the service root, inclusive. The
|
|
26
|
+
* climb stops after a directory named `node_modules`, as tsserver's does. A file
|
|
27
|
+
* outside the service root, or with no config on its way up, has none.
|
|
28
|
+
*
|
|
29
|
+
* `known` caches the answer per directory; pass one map for one service so
|
|
30
|
+
* its files share the climb.
|
|
31
|
+
*/
|
|
32
|
+
export declare function fileConfigPath(file: string, serviceRoot: string, known?: Map<string, string | undefined>): string | undefined;
|
|
@@ -55,3 +55,43 @@ export function realPath(p) {
|
|
|
55
55
|
return path.resolve(p);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/** The names an editor looks for in a file's directory, in order. */
|
|
59
|
+
const FILE_CONFIG_NAMES = ['tsconfig.json', 'jsconfig.json'];
|
|
60
|
+
/**
|
|
61
|
+
* The config an editor types `file` under, for a service with none at its
|
|
62
|
+
* root (carrick#1604): the nearest `tsconfig.json`, then `jsconfig.json`, in
|
|
63
|
+
* each directory from the file's up to the service root, inclusive. The
|
|
64
|
+
* climb stops after a directory named `node_modules`, as tsserver's does. A file
|
|
65
|
+
* outside the service root, or with no config on its way up, has none.
|
|
66
|
+
*
|
|
67
|
+
* `known` caches the answer per directory; pass one map for one service so
|
|
68
|
+
* its files share the climb.
|
|
69
|
+
*/
|
|
70
|
+
export function fileConfigPath(file, serviceRoot, known = new Map()) {
|
|
71
|
+
const root = path.resolve(serviceRoot);
|
|
72
|
+
const climbed = [];
|
|
73
|
+
let found;
|
|
74
|
+
let dir = path.dirname(path.resolve(file));
|
|
75
|
+
for (;;) {
|
|
76
|
+
const from = path.relative(root, dir);
|
|
77
|
+
if (from.startsWith('..') || path.isAbsolute(from))
|
|
78
|
+
break;
|
|
79
|
+
if (known.has(dir)) {
|
|
80
|
+
found = known.get(dir);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
climbed.push(dir);
|
|
84
|
+
const name = FILE_CONFIG_NAMES.find((n) => fs.existsSync(path.join(dir, n)));
|
|
85
|
+
if (name !== undefined) {
|
|
86
|
+
found = path.join(dir, name);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
// tsserver looks in a `node_modules` directory itself, then stops.
|
|
90
|
+
if (dir === root || path.basename(dir) === 'node_modules')
|
|
91
|
+
break;
|
|
92
|
+
dir = path.dirname(dir);
|
|
93
|
+
}
|
|
94
|
+
for (const d of climbed)
|
|
95
|
+
known.set(d, found);
|
|
96
|
+
return found;
|
|
97
|
+
}
|
|
@@ -80,6 +80,16 @@ export declare class ProjectLoader {
|
|
|
80
80
|
private denoProject;
|
|
81
81
|
/** The tsconfig the service names, when the project is built from one. */
|
|
82
82
|
private namedTsconfigPath;
|
|
83
|
+
/**
|
|
84
|
+
* Set when the service has no config at or above its root: each file is then
|
|
85
|
+
* typed under the nearest `tsconfig.json`/`jsconfig.json` on its way up to
|
|
86
|
+
* the service root (carrick#1604), and the default project holds the rest.
|
|
87
|
+
*/
|
|
88
|
+
private inferredService;
|
|
89
|
+
/** The per-directory answers of `fileConfigPath` for this service. */
|
|
90
|
+
private readonly fileConfigs;
|
|
91
|
+
/** The reference owner rule of each config a file was found under. */
|
|
92
|
+
private readonly ownerRules;
|
|
83
93
|
/** Each file's owning config (carrick#1604), resolved on first use. */
|
|
84
94
|
private configPathFor;
|
|
85
95
|
/** Programs built for owning projects other than the named tsconfig. */
|
|
@@ -128,6 +138,12 @@ export declare class ProjectLoader {
|
|
|
128
138
|
* as before.
|
|
129
139
|
*/
|
|
130
140
|
projectKeyFor(file?: string): string;
|
|
141
|
+
/**
|
|
142
|
+
* `projectKeyFor` for a service with no config at its root: the owner of the
|
|
143
|
+
* nearest config on the file's way up to the service root, or `''` (the
|
|
144
|
+
* default project, under the inferred options) for a file with none.
|
|
145
|
+
*/
|
|
146
|
+
private inferredKeyFor;
|
|
131
147
|
/**
|
|
132
148
|
* The project behind a key from `projectKeyFor`, built on first use and
|
|
133
149
|
* kept: a service whose files all belong to one project builds one.
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { Project, getCompilerOptionsFromTsConfig } from 'ts-morph';
|
|
13
13
|
import * as path from 'node:path';
|
|
14
|
+
import ts from 'typescript';
|
|
14
15
|
import * as fs from 'node:fs';
|
|
15
|
-
import { DenoProject, findDenoConfig, findServiceTsconfig, referenceRedirectOf, serviceConfigPath, sidecarCompilerOptions, untrustedReadBound, } from './capture/index.js';
|
|
16
|
+
import { DenoProject, findDenoConfig, fileConfigPath, findServiceTsconfig, referenceRedirectOf, serviceConfigPath, sidecarCompilerOptions, untrustedReadBound, } from './capture/index.js';
|
|
16
17
|
import { moduleFormatResolutionHost } from './module-format.js';
|
|
17
18
|
import { ExternalImports, registerExternalImports } from './origin.js';
|
|
18
19
|
/**
|
|
@@ -92,6 +93,11 @@ const TARGET_MAP = {
|
|
|
92
93
|
'ES2023': 10,
|
|
93
94
|
'ESNext': 99,
|
|
94
95
|
};
|
|
96
|
+
/** Whether the config's own `compilerOptions` state `maxNodeModuleJsDepth`. */
|
|
97
|
+
function statesJsDepth(configPath) {
|
|
98
|
+
const raw = ts.readConfigFile(configPath, ts.sys.readFile).config;
|
|
99
|
+
return raw?.compilerOptions?.maxNodeModuleJsDepth !== undefined;
|
|
100
|
+
}
|
|
95
101
|
/**
|
|
96
102
|
* ProjectLoader - Manages ts-morph Project initialization and access
|
|
97
103
|
*
|
|
@@ -126,6 +132,16 @@ export class ProjectLoader {
|
|
|
126
132
|
denoProject;
|
|
127
133
|
/** The tsconfig the service names, when the project is built from one. */
|
|
128
134
|
namedTsconfigPath;
|
|
135
|
+
/**
|
|
136
|
+
* Set when the service has no config at or above its root: each file is then
|
|
137
|
+
* typed under the nearest `tsconfig.json`/`jsconfig.json` on its way up to
|
|
138
|
+
* the service root (carrick#1604), and the default project holds the rest.
|
|
139
|
+
*/
|
|
140
|
+
inferredService = false;
|
|
141
|
+
/** The per-directory answers of `fileConfigPath` for this service. */
|
|
142
|
+
fileConfigs = new Map();
|
|
143
|
+
/** The reference owner rule of each config a file was found under. */
|
|
144
|
+
ownerRules = new Map();
|
|
129
145
|
/** Each file's owning config (carrick#1604), resolved on first use. */
|
|
130
146
|
configPathFor;
|
|
131
147
|
/** Programs built for owning projects other than the named tsconfig. */
|
|
@@ -231,6 +247,7 @@ export class ProjectLoader {
|
|
|
231
247
|
}
|
|
232
248
|
else {
|
|
233
249
|
this.log('No tsconfig.json found, using default compiler options');
|
|
250
|
+
this.inferredService = true;
|
|
234
251
|
this.buildProject = (kind) => {
|
|
235
252
|
const project = new Project({
|
|
236
253
|
...this.fileSystemOption(),
|
|
@@ -239,7 +256,7 @@ export class ProjectLoader {
|
|
|
239
256
|
});
|
|
240
257
|
// Add source files from common locations
|
|
241
258
|
if (kind === 'listed')
|
|
242
|
-
this.addDefaultSourceFiles(project);
|
|
259
|
+
this.addDefaultSourceFiles(project, (file) => this.projectKeyFor(file) === '');
|
|
243
260
|
return project;
|
|
244
261
|
};
|
|
245
262
|
}
|
|
@@ -347,11 +364,18 @@ export class ProjectLoader {
|
|
|
347
364
|
*/
|
|
348
365
|
projectFromConfig(configPath, kind = 'listed') {
|
|
349
366
|
const imports = new ExternalImports();
|
|
367
|
+
const options = getCompilerOptionsFromTsConfig(configPath).options;
|
|
368
|
+
// TypeScript reads a `jsconfig.json` with an implicit depth of 2, which
|
|
369
|
+
// loads the JavaScript sources of installed packages; every other program
|
|
370
|
+
// here reads 0. A depth the config states is kept (carrick#1604).
|
|
371
|
+
if (this.inferredService && path.basename(configPath) === 'jsconfig.json' && !statesJsDepth(configPath)) {
|
|
372
|
+
options.maxNodeModuleJsDepth = 0;
|
|
373
|
+
}
|
|
350
374
|
const project = new Project({
|
|
351
375
|
...this.fileSystemOption(),
|
|
352
376
|
tsConfigFilePath: configPath,
|
|
353
377
|
// Over the tsconfig's own options: ts-morph lays these on top.
|
|
354
|
-
compilerOptions: sidecarCompilerOptions(optionsFor(kind,
|
|
378
|
+
compilerOptions: sidecarCompilerOptions(optionsFor(kind, options)),
|
|
355
379
|
skipAddingFilesFromTsConfig: kind === 'scripts',
|
|
356
380
|
// Each import resolves in its file's own module format (carrick#1619),
|
|
357
381
|
// a referenced project's unbuilt output reads as its source, under its
|
|
@@ -373,12 +397,33 @@ export class ProjectLoader {
|
|
|
373
397
|
* as before.
|
|
374
398
|
*/
|
|
375
399
|
projectKeyFor(file) {
|
|
400
|
+
if (this.inferredService)
|
|
401
|
+
return this.inferredKeyFor(file);
|
|
376
402
|
if (!this.namedTsconfigPath || file === undefined)
|
|
377
403
|
return '';
|
|
378
404
|
this.configPathFor ??= serviceConfigPath(this.namedTsconfigPath, (d) => this.logError(d));
|
|
379
405
|
const config = this.configPathFor(path.resolve(this.repoRoot, file));
|
|
380
406
|
return config === this.namedTsconfigPath ? '' : config;
|
|
381
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* `projectKeyFor` for a service with no config at its root: the owner of the
|
|
410
|
+
* nearest config on the file's way up to the service root, or `''` (the
|
|
411
|
+
* default project, under the inferred options) for a file with none.
|
|
412
|
+
*/
|
|
413
|
+
inferredKeyFor(file) {
|
|
414
|
+
if (file === undefined)
|
|
415
|
+
return '';
|
|
416
|
+
const absolute = path.resolve(this.repoRoot, file);
|
|
417
|
+
const config = fileConfigPath(absolute, this.repoRoot, this.fileConfigs);
|
|
418
|
+
if (config === undefined)
|
|
419
|
+
return '';
|
|
420
|
+
let owner = this.ownerRules.get(config);
|
|
421
|
+
if (!owner) {
|
|
422
|
+
owner = serviceConfigPath(config, (d) => this.logError(d));
|
|
423
|
+
this.ownerRules.set(config, owner);
|
|
424
|
+
}
|
|
425
|
+
return owner(absolute);
|
|
426
|
+
}
|
|
382
427
|
/**
|
|
383
428
|
* The project behind a key from `projectKeyFor`, built on first use and
|
|
384
429
|
* kept: a service whose files all belong to one project builds one.
|
|
@@ -512,7 +557,7 @@ export class ProjectLoader {
|
|
|
512
557
|
* dependencies installed it cost 40 s and added nothing the compiler needs
|
|
513
558
|
* (carrick#749).
|
|
514
559
|
*/
|
|
515
|
-
addDefaultSourceFiles(project) {
|
|
560
|
+
addDefaultSourceFiles(project, keep = () => true) {
|
|
516
561
|
const globs = DEFAULT_SOURCE_PATTERNS.map((pattern) => path.join(this.repoRoot, pattern));
|
|
517
562
|
// Negated last so it applies to every pattern above it.
|
|
518
563
|
globs.push(`!${path.join(this.repoRoot, '**/node_modules/**')}`);
|
|
@@ -525,6 +570,8 @@ export class ProjectLoader {
|
|
|
525
570
|
return;
|
|
526
571
|
}
|
|
527
572
|
for (const filePath of filePaths) {
|
|
573
|
+
if (!keep(filePath))
|
|
574
|
+
continue;
|
|
528
575
|
try {
|
|
529
576
|
project.addSourceFileAtPath(filePath);
|
|
530
577
|
}
|
|
@@ -1800,8 +1800,10 @@ export declare class TypeInferrer {
|
|
|
1800
1800
|
* - a cast on it: `(await request.json()) as NewWidget`;
|
|
1801
1801
|
* - the annotation of the binding it initialises:
|
|
1802
1802
|
* `const input: NewWidget = await request.json()`;
|
|
1803
|
-
* - a
|
|
1804
|
-
* (`
|
|
1803
|
+
* - a type the handler states for the read further on
|
|
1804
|
+
* (`statedInFunction`): a schema it is handed to, the annotation of a
|
|
1805
|
+
* name it is assigned to, a pass-through function, a cast of the whole
|
|
1806
|
+
* value.
|
|
1805
1807
|
*
|
|
1806
1808
|
* The first two are `typeNodeStatedAtRead`, the reading a consumer's
|
|
1807
1809
|
* response read already gets. An annotation further out is not one of them:
|
|
@@ -2030,19 +2032,58 @@ export declare class TypeInferrer {
|
|
|
2030
2032
|
*/
|
|
2031
2033
|
private middlewareBodySchemaValues;
|
|
2032
2034
|
/**
|
|
2033
|
-
* The
|
|
2034
|
-
* (carrick#
|
|
2035
|
-
*
|
|
2036
|
-
*
|
|
2037
|
-
*
|
|
2038
|
-
*
|
|
2039
|
-
*
|
|
2040
|
-
* (`
|
|
2041
|
-
*
|
|
2042
|
-
*
|
|
2043
|
-
*
|
|
2044
|
-
|
|
2045
|
-
|
|
2035
|
+
* The type the function a value is made in states for it, where the
|
|
2036
|
+
* compiler types the value itself `any` or `unknown` (carrick#2594), or
|
|
2037
|
+
* null.
|
|
2038
|
+
*
|
|
2039
|
+
* `value` is the located expression: a body read, or a call whose result is
|
|
2040
|
+
* untyped. The value is followed through the names that hold it: a
|
|
2041
|
+
* declaration it initialises, and a name it is assigned to later
|
|
2042
|
+
* (`let body: unknown; body = await read()`). At each place the value or
|
|
2043
|
+
* one of those names is used whole, the source can state a type:
|
|
2044
|
+
*
|
|
2045
|
+
* 0. the annotation of the name that holds it (`let body: Upload`), or the
|
|
2046
|
+
* declared return type of the function that returns it unchanged (only
|
|
2047
|
+
* with `reads.returned`);
|
|
2048
|
+
* 1. a schema it is handed to (`Schema.safeParse(body ?? {})`,
|
|
2049
|
+
* `parse(Schema, body)`): the schema's input is what a sender may send
|
|
2050
|
+
* (carrick#1166). A candidate counts only when it declares schema types,
|
|
2051
|
+
* so no method or library name is matched;
|
|
2052
|
+
* 2. a function of the project it is handed to that returns that argument
|
|
2053
|
+
* unchanged (through casts) or an empty default: its declared return
|
|
2054
|
+
* type. A function that builds a new value from its argument is a
|
|
2055
|
+
* converter, and its return type describes what it built, never the
|
|
2056
|
+
* value it was handed, so it states nothing here;
|
|
2057
|
+
* 3. a cast of the whole value (`(body as { phone: string }).phone`);
|
|
2058
|
+
* 4. the parameter of a call it is handed to (only with
|
|
2059
|
+
* `reads.parameters`), where that parameter's type is a body shape once
|
|
2060
|
+
* any callable member is dropped (a function never crosses the wire).
|
|
2061
|
+
*
|
|
2062
|
+
* The first rank with an answer decides. A placeholder (`unknown`,
|
|
2063
|
+
* `Record<string, unknown>`, any type node with `any` or `unknown` written
|
|
2064
|
+
* in it) states nothing, and two different statements in one rank decide
|
|
2065
|
+
* nothing: the answer is null.
|
|
2066
|
+
*/
|
|
2067
|
+
private statedInFunction;
|
|
2068
|
+
/**
|
|
2069
|
+
* What a call that takes the value at argument `index` states for it:
|
|
2070
|
+
* a schema among its receiver and other arguments (rank 1), a callee of the
|
|
2071
|
+
* project that returns that argument unchanged (rank 2), or, with
|
|
2072
|
+
* `reads.parameters`, the parameter's own type (rank 4).
|
|
2073
|
+
*/
|
|
2074
|
+
private stateAtReceivingCall;
|
|
2075
|
+
/**
|
|
2076
|
+
* The project function `call` invokes, when every value it returns is its
|
|
2077
|
+
* argument at `index` as it was handed in (through parentheses, casts,
|
|
2078
|
+
* `satisfies` and `!`) or an empty default (`{}`, `[]`, `null`,
|
|
2079
|
+
* `undefined`), and at least one return is that argument. Such a function
|
|
2080
|
+
* narrows its argument, so its declared return type describes the value it
|
|
2081
|
+
* was handed. Any other function builds something new from it, and is not
|
|
2082
|
+
* this.
|
|
2083
|
+
*/
|
|
2084
|
+
private passThroughFunction;
|
|
2085
|
+
/** `{}`, `[]`, `null` or `undefined`: a default that states no member. */
|
|
2086
|
+
private isEmptyDefault;
|
|
2046
2087
|
/**
|
|
2047
2088
|
* The part a validated read names when that part is not a body, or
|
|
2048
2089
|
* `undefined` (carrick#1166).
|
|
@@ -1996,6 +1996,27 @@ export class TypeInferrer {
|
|
|
1996
1996
|
});
|
|
1997
1997
|
}
|
|
1998
1998
|
typeString = this.unwrapPromise(typeString, returnType);
|
|
1999
|
+
// carrick#2594: the call's value is untyped (`Promise<any>`,
|
|
2000
|
+
// `Promise<unknown>`), and the function it is made in states its type:
|
|
2001
|
+
// it returns the value from a function with a declared return type, holds
|
|
2002
|
+
// it in an annotated name, validates it with a schema, narrows it with a
|
|
2003
|
+
// function that returns it unchanged, casts it whole, or hands it to a
|
|
2004
|
+
// typed parameter. A function that converts the value into something new
|
|
2005
|
+
// states nothing about it, and neither does a use on a failure path.
|
|
2006
|
+
const untypedAnswer = typeString.trim() === 'any' || typeString.trim() === 'unknown';
|
|
2007
|
+
if (untypedAnswer && func && !explicitType && !unwrapResult.wasUnwrapped && !carrierPayload) {
|
|
2008
|
+
const stated = this.statedInFunction(callExpr, func, {
|
|
2009
|
+
returned: true,
|
|
2010
|
+
parameters: true,
|
|
2011
|
+
skipFailurePaths: true,
|
|
2012
|
+
});
|
|
2013
|
+
if (stated) {
|
|
2014
|
+
this.log(`Call result at ${where} is untyped, and its function states its type ` +
|
|
2015
|
+
`(${stated.how}); publishing that type`);
|
|
2016
|
+
typeString = stated.text;
|
|
2017
|
+
isExplicit = true;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
1999
2020
|
// #336: consumer analogue of the #306 producer anchor. Anchor on the
|
|
2000
2021
|
// CALL's own payload (the rule-extracted Type, else the awaited call
|
|
2001
2022
|
// type), peeling array levels to the element symbol + depth. Without the
|
|
@@ -2659,18 +2680,22 @@ export class TypeInferrer {
|
|
|
2659
2680
|
isExplicit = true;
|
|
2660
2681
|
}
|
|
2661
2682
|
// carrick#1166: the read itself is untyped (`await c.req.json()` is `any`
|
|
2662
|
-
// or `unknown`), and the handler
|
|
2663
|
-
// `Schema.safeParse(body)`,
|
|
2664
|
-
//
|
|
2665
|
-
//
|
|
2683
|
+
// or `unknown`), and the handler states its type further on: it validates
|
|
2684
|
+
// it with a schema (`Schema.safeParse(body ?? {})`, whose INPUT is the
|
|
2685
|
+
// request contract, carrick#1101), assigns it to an annotated name
|
|
2686
|
+
// (`let body: T; body = await c.req.json()`), narrows it with a function
|
|
2687
|
+
// that returns it unchanged, or casts the whole value (carrick#2594).
|
|
2666
2688
|
const readType = this.unwrapPromiseType(payloadType);
|
|
2667
2689
|
if (!explicitType &&
|
|
2668
2690
|
!unwrapResult.wasUnwrapped &&
|
|
2669
2691
|
(readType.isAny() || readType.isUnknown())) {
|
|
2670
|
-
const
|
|
2692
|
+
const func = this.findContainingFunctionForNode(node);
|
|
2693
|
+
const parsedWith = func
|
|
2694
|
+
? this.statedInFunction(node, func, { returned: false, parameters: false, skipFailurePaths: false })
|
|
2695
|
+
: null;
|
|
2671
2696
|
if (parsedWith) {
|
|
2672
2697
|
this.log(`Request locator at ${request.file_path}:${request.line_number} is an untyped read ` +
|
|
2673
|
-
|
|
2698
|
+
`its handler states a type for (${parsedWith.how}); publishing that type`);
|
|
2674
2699
|
return this.declaredRequestInferredType(request, parsedWith, this.getNodeLocation(node));
|
|
2675
2700
|
}
|
|
2676
2701
|
}
|
|
@@ -6606,8 +6631,10 @@ export class TypeInferrer {
|
|
|
6606
6631
|
* - a cast on it: `(await request.json()) as NewWidget`;
|
|
6607
6632
|
* - the annotation of the binding it initialises:
|
|
6608
6633
|
* `const input: NewWidget = await request.json()`;
|
|
6609
|
-
* - a
|
|
6610
|
-
* (`
|
|
6634
|
+
* - a type the handler states for the read further on
|
|
6635
|
+
* (`statedInFunction`): a schema it is handed to, the annotation of a
|
|
6636
|
+
* name it is assigned to, a pass-through function, a cast of the whole
|
|
6637
|
+
* value.
|
|
6611
6638
|
*
|
|
6612
6639
|
* The first two are `typeNodeStatedAtRead`, the reading a consumer's
|
|
6613
6640
|
* response read already gets. An annotation further out is not one of them:
|
|
@@ -6623,10 +6650,13 @@ export class TypeInferrer {
|
|
|
6623
6650
|
'reads off the platform request, as the source types that read');
|
|
6624
6651
|
return this.createInferredType(request, statedText, true, this.getNodeLocation(read));
|
|
6625
6652
|
}
|
|
6626
|
-
const
|
|
6653
|
+
const func = this.findContainingFunctionForNode(read);
|
|
6654
|
+
const validated = func
|
|
6655
|
+
? this.statedInFunction(read, func, { returned: false, parameters: false, skipFailurePaths: false })
|
|
6656
|
+
: null;
|
|
6627
6657
|
if (validated) {
|
|
6628
6658
|
this.log(`Request at ${request.file_path}:${request.line_number} is the body its handler ` +
|
|
6629
|
-
|
|
6659
|
+
`reads off the platform request and states a type for (${validated.how}); publishing that type`);
|
|
6630
6660
|
return this.declaredRequestInferredType(request, validated, this.getNodeLocation(read));
|
|
6631
6661
|
}
|
|
6632
6662
|
return null;
|
|
@@ -7285,70 +7315,280 @@ export class TypeInferrer {
|
|
|
7285
7315
|
return values;
|
|
7286
7316
|
}
|
|
7287
7317
|
/**
|
|
7288
|
-
* The
|
|
7289
|
-
* (carrick#
|
|
7290
|
-
*
|
|
7291
|
-
*
|
|
7292
|
-
*
|
|
7293
|
-
*
|
|
7294
|
-
*
|
|
7295
|
-
* (`
|
|
7296
|
-
*
|
|
7297
|
-
*
|
|
7298
|
-
*
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7318
|
+
* The type the function a value is made in states for it, where the
|
|
7319
|
+
* compiler types the value itself `any` or `unknown` (carrick#2594), or
|
|
7320
|
+
* null.
|
|
7321
|
+
*
|
|
7322
|
+
* `value` is the located expression: a body read, or a call whose result is
|
|
7323
|
+
* untyped. The value is followed through the names that hold it: a
|
|
7324
|
+
* declaration it initialises, and a name it is assigned to later
|
|
7325
|
+
* (`let body: unknown; body = await read()`). At each place the value or
|
|
7326
|
+
* one of those names is used whole, the source can state a type:
|
|
7327
|
+
*
|
|
7328
|
+
* 0. the annotation of the name that holds it (`let body: Upload`), or the
|
|
7329
|
+
* declared return type of the function that returns it unchanged (only
|
|
7330
|
+
* with `reads.returned`);
|
|
7331
|
+
* 1. a schema it is handed to (`Schema.safeParse(body ?? {})`,
|
|
7332
|
+
* `parse(Schema, body)`): the schema's input is what a sender may send
|
|
7333
|
+
* (carrick#1166). A candidate counts only when it declares schema types,
|
|
7334
|
+
* so no method or library name is matched;
|
|
7335
|
+
* 2. a function of the project it is handed to that returns that argument
|
|
7336
|
+
* unchanged (through casts) or an empty default: its declared return
|
|
7337
|
+
* type. A function that builds a new value from its argument is a
|
|
7338
|
+
* converter, and its return type describes what it built, never the
|
|
7339
|
+
* value it was handed, so it states nothing here;
|
|
7340
|
+
* 3. a cast of the whole value (`(body as { phone: string }).phone`);
|
|
7341
|
+
* 4. the parameter of a call it is handed to (only with
|
|
7342
|
+
* `reads.parameters`), where that parameter's type is a body shape once
|
|
7343
|
+
* any callable member is dropped (a function never crosses the wire).
|
|
7344
|
+
*
|
|
7345
|
+
* The first rank with an answer decides. A placeholder (`unknown`,
|
|
7346
|
+
* `Record<string, unknown>`, any type node with `any` or `unknown` written
|
|
7347
|
+
* in it) states nothing, and two different statements in one rank decide
|
|
7348
|
+
* nothing: the answer is null.
|
|
7349
|
+
*/
|
|
7350
|
+
statedInFunction(value, func, reads) {
|
|
7351
|
+
const holders = new Set();
|
|
7352
|
+
const positions = [value];
|
|
7353
|
+
const holderUse = (node) => {
|
|
7354
|
+
if (!Node.isIdentifier(node))
|
|
7355
|
+
return false;
|
|
7356
|
+
const symbol = node.getSymbol()?.compilerSymbol;
|
|
7357
|
+
return !!symbol && holders.has(symbol);
|
|
7309
7358
|
};
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7359
|
+
const hold = (name) => {
|
|
7360
|
+
if (!Node.isIdentifier(name))
|
|
7361
|
+
return;
|
|
7362
|
+
const symbol = name.getSymbol()?.compilerSymbol;
|
|
7363
|
+
if (!symbol || holders.has(symbol))
|
|
7364
|
+
return;
|
|
7365
|
+
holders.add(symbol);
|
|
7366
|
+
for (const id of func.getDescendantsOfKind(SyntaxKind.Identifier)) {
|
|
7367
|
+
if (id === name || id.getSymbol()?.compilerSymbol !== symbol)
|
|
7368
|
+
continue;
|
|
7369
|
+
const parent = id.getParent();
|
|
7370
|
+
if (Node.isVariableDeclaration(parent) && parent.getNameNode() === id)
|
|
7371
|
+
continue;
|
|
7372
|
+
if (Node.isBinaryExpression(parent) &&
|
|
7373
|
+
parent.getLeft() === id &&
|
|
7374
|
+
parent.getOperatorToken().getKind() === SyntaxKind.EqualsToken) {
|
|
7375
|
+
continue;
|
|
7376
|
+
}
|
|
7377
|
+
positions.push(id);
|
|
7378
|
+
}
|
|
7379
|
+
};
|
|
7380
|
+
hold(value);
|
|
7381
|
+
const found = [];
|
|
7382
|
+
const stateNode = (rank, typeNode, at, how) => {
|
|
7383
|
+
if (!typeNode || this.leavesAPositionOpen(typeNode))
|
|
7384
|
+
return;
|
|
7385
|
+
const text = this.structuralTextFromTypeNode(typeNode);
|
|
7386
|
+
if (text)
|
|
7387
|
+
found.push({ rank, text, at, how });
|
|
7388
|
+
};
|
|
7389
|
+
for (let i = 0; i < positions.length; i++) {
|
|
7390
|
+
const position = positions[i];
|
|
7391
|
+
if (reads.skipFailurePaths &&
|
|
7392
|
+
position !== value &&
|
|
7393
|
+
reachedOnlyOnFailure(position, func, holderUse)) {
|
|
7330
7394
|
continue;
|
|
7331
|
-
const candidates = [];
|
|
7332
|
-
const callee = call.getExpression();
|
|
7333
|
-
if (Node.isPropertyAccessExpression(callee)) {
|
|
7334
|
-
candidates.push(callee.getExpression());
|
|
7335
7395
|
}
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
for (
|
|
7341
|
-
const
|
|
7342
|
-
if (!
|
|
7343
|
-
|
|
7396
|
+
// Climb the wrappers that leave the value what it was: parentheses,
|
|
7397
|
+
// `await`, `!`, `satisfies`, a placeholder cast, and an empty default
|
|
7398
|
+
// (`body ?? {}`).
|
|
7399
|
+
let outer = position;
|
|
7400
|
+
for (;;) {
|
|
7401
|
+
const parent = outer.getParent();
|
|
7402
|
+
if (!parent)
|
|
7403
|
+
break;
|
|
7404
|
+
if ((Node.isParenthesizedExpression(parent) ||
|
|
7405
|
+
Node.isAwaitExpression(parent) ||
|
|
7406
|
+
Node.isNonNullExpression(parent) ||
|
|
7407
|
+
Node.isSatisfiesExpression(parent)) &&
|
|
7408
|
+
parent.getExpression() === outer) {
|
|
7409
|
+
outer = parent;
|
|
7410
|
+
continue;
|
|
7411
|
+
}
|
|
7412
|
+
if ((Node.isAsExpression(parent) || Node.isTypeAssertion(parent)) &&
|
|
7413
|
+
parent.getExpression() === outer) {
|
|
7414
|
+
const typeNode = parent.getTypeNode();
|
|
7415
|
+
if (typeNode && !this.leavesAPositionOpen(typeNode)) {
|
|
7416
|
+
stateNode(3, typeNode, parent, 'a cast of the whole value');
|
|
7417
|
+
break;
|
|
7418
|
+
}
|
|
7419
|
+
outer = parent;
|
|
7344
7420
|
continue;
|
|
7345
7421
|
}
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7422
|
+
if (Node.isBinaryExpression(parent) &&
|
|
7423
|
+
parent.getLeft() === outer &&
|
|
7424
|
+
[SyntaxKind.QuestionQuestionToken, SyntaxKind.BarBarToken].includes(parent.getOperatorToken().getKind()) &&
|
|
7425
|
+
this.isEmptyDefault(parent.getRight())) {
|
|
7426
|
+
outer = parent;
|
|
7427
|
+
continue;
|
|
7428
|
+
}
|
|
7429
|
+
break;
|
|
7430
|
+
}
|
|
7431
|
+
const parent = outer.getParent();
|
|
7432
|
+
if (!parent)
|
|
7433
|
+
continue;
|
|
7434
|
+
if (Node.isVariableDeclaration(parent) && parent.getInitializer() === outer) {
|
|
7435
|
+
stateNode(0, parent.getTypeNode(), parent, 'the annotation of the name that holds it');
|
|
7436
|
+
hold(parent.getNameNode());
|
|
7437
|
+
continue;
|
|
7438
|
+
}
|
|
7439
|
+
if (Node.isBinaryExpression(parent) &&
|
|
7440
|
+
parent.getRight() === outer &&
|
|
7441
|
+
parent.getOperatorToken().getKind() === SyntaxKind.EqualsToken &&
|
|
7442
|
+
Node.isIdentifier(parent.getLeft())) {
|
|
7443
|
+
const declaration = parent.getLeft().getSymbol()?.getValueDeclaration();
|
|
7444
|
+
if (declaration && Node.isVariableDeclaration(declaration)) {
|
|
7445
|
+
stateNode(0, declaration.getTypeNode(), declaration, 'the annotation of the name it is assigned to');
|
|
7446
|
+
}
|
|
7447
|
+
hold(parent.getLeft());
|
|
7448
|
+
continue;
|
|
7449
|
+
}
|
|
7450
|
+
if (reads.returned) {
|
|
7451
|
+
const returnedHere = (Node.isReturnStatement(parent) &&
|
|
7452
|
+
parent.getExpression() === outer &&
|
|
7453
|
+
this.findContainingFunctionForNode(parent) === func) ||
|
|
7454
|
+
(Node.isArrowFunction(parent) && parent === func && parent.getBody() === outer);
|
|
7455
|
+
if (returnedHere) {
|
|
7456
|
+
stateNode(0, func.getReturnTypeNode(), func, 'the declared return type of the function that returns it');
|
|
7457
|
+
continue;
|
|
7458
|
+
}
|
|
7459
|
+
}
|
|
7460
|
+
if (Node.isCallExpression(parent) && parent.getArguments().includes(outer)) {
|
|
7461
|
+
this.stateAtReceivingCall(parent, parent.getArguments().indexOf(outer), reads, found, stateNode);
|
|
7349
7462
|
}
|
|
7350
7463
|
}
|
|
7351
|
-
|
|
7464
|
+
if (found.length === 0)
|
|
7465
|
+
return null;
|
|
7466
|
+
const rank = Math.min(...found.map((entry) => entry.rank));
|
|
7467
|
+
const decided = found.filter((entry) => entry.rank === rank);
|
|
7468
|
+
if (new Set(decided.map((entry) => entry.text)).size !== 1) {
|
|
7469
|
+
this.log(`A value at ${this.getNodeLocation(value).file_path}:${this.getNodeLocation(value).start_line} ` +
|
|
7470
|
+
`is stated as ${decided.length} different types in its function; stating none`);
|
|
7471
|
+
return null;
|
|
7472
|
+
}
|
|
7473
|
+
const [first] = decided;
|
|
7474
|
+
return { text: first.text, provenance: first.provenance, at: first.at, how: first.how };
|
|
7475
|
+
}
|
|
7476
|
+
/**
|
|
7477
|
+
* What a call that takes the value at argument `index` states for it:
|
|
7478
|
+
* a schema among its receiver and other arguments (rank 1), a callee of the
|
|
7479
|
+
* project that returns that argument unchanged (rank 2), or, with
|
|
7480
|
+
* `reads.parameters`, the parameter's own type (rank 4).
|
|
7481
|
+
*/
|
|
7482
|
+
stateAtReceivingCall(call, index, reads, found, stateNode) {
|
|
7483
|
+
const schemaCandidates = [];
|
|
7484
|
+
const callee = call.getExpression();
|
|
7485
|
+
if (Node.isPropertyAccessExpression(callee))
|
|
7486
|
+
schemaCandidates.push(callee.getExpression());
|
|
7487
|
+
call.getArguments().forEach((argument, at) => {
|
|
7488
|
+
if (at !== index)
|
|
7489
|
+
schemaCandidates.push(this.unwrapExpressionNode(argument));
|
|
7490
|
+
});
|
|
7491
|
+
for (const candidate of schemaCandidates) {
|
|
7492
|
+
const candidateType = candidate.getType();
|
|
7493
|
+
if (!this.schemaOutputType(candidateType, candidate) &&
|
|
7494
|
+
!this.schemaInputType(candidateType, candidate)) {
|
|
7495
|
+
continue;
|
|
7496
|
+
}
|
|
7497
|
+
const contract = this.schemaContract(candidate, 'input');
|
|
7498
|
+
if (contract) {
|
|
7499
|
+
found.push({ rank: 1, text: contract.text, provenance: contract.provenance, at: call, how: 'a schema it is handed to' });
|
|
7500
|
+
return;
|
|
7501
|
+
}
|
|
7502
|
+
}
|
|
7503
|
+
const passThrough = this.passThroughFunction(call, index);
|
|
7504
|
+
if (passThrough) {
|
|
7505
|
+
stateNode(2, passThrough.getReturnTypeNode(), call, 'a function that returns it unchanged');
|
|
7506
|
+
return;
|
|
7507
|
+
}
|
|
7508
|
+
if (!reads.parameters)
|
|
7509
|
+
return;
|
|
7510
|
+
const signature = this.project.getTypeChecker().getResolvedSignature(call);
|
|
7511
|
+
const parameter = signature?.getParameters()[index];
|
|
7512
|
+
const declaration = parameter?.getValueDeclaration();
|
|
7513
|
+
if (!parameter || (declaration && Node.isParameterDeclaration(declaration) && declaration.isRestParameter())) {
|
|
7514
|
+
return;
|
|
7515
|
+
}
|
|
7516
|
+
let parameterType = parameter.getTypeAtLocation(call);
|
|
7517
|
+
if (parameterType.isUnion()) {
|
|
7518
|
+
const bodies = parameterType.getUnionTypes().filter((part) => !this.isCallableType(part));
|
|
7519
|
+
if (bodies.length !== 1)
|
|
7520
|
+
return;
|
|
7521
|
+
parameterType = bodies[0];
|
|
7522
|
+
}
|
|
7523
|
+
if (!this.isObjectShape(parameterType))
|
|
7524
|
+
return;
|
|
7525
|
+
const text = this.structuralTextFromType(parameterType, call);
|
|
7526
|
+
if (!text || /\b(any|unknown)\b/.test(text))
|
|
7527
|
+
return;
|
|
7528
|
+
found.push({ rank: 4, text, at: call, how: 'the parameter of a call it is handed to' });
|
|
7529
|
+
}
|
|
7530
|
+
/**
|
|
7531
|
+
* The project function `call` invokes, when every value it returns is its
|
|
7532
|
+
* argument at `index` as it was handed in (through parentheses, casts,
|
|
7533
|
+
* `satisfies` and `!`) or an empty default (`{}`, `[]`, `null`,
|
|
7534
|
+
* `undefined`), and at least one return is that argument. Such a function
|
|
7535
|
+
* narrows its argument, so its declared return type describes the value it
|
|
7536
|
+
* was handed. Any other function builds something new from it, and is not
|
|
7537
|
+
* this.
|
|
7538
|
+
*/
|
|
7539
|
+
passThroughFunction(call, index) {
|
|
7540
|
+
let symbol = call.getExpression().getSymbol();
|
|
7541
|
+
if (symbol?.isAlias())
|
|
7542
|
+
symbol = symbol.getAliasedSymbol() ?? symbol;
|
|
7543
|
+
for (const declaration of symbol?.getDeclarations() ?? []) {
|
|
7544
|
+
const fn = Node.isFunctionDeclaration(declaration) || Node.isMethodDeclaration(declaration)
|
|
7545
|
+
? declaration
|
|
7546
|
+
: Node.isVariableDeclaration(declaration)
|
|
7547
|
+
? this.unwrapExpressionNode(declaration.getInitializer())
|
|
7548
|
+
: undefined;
|
|
7549
|
+
if (!fn ||
|
|
7550
|
+
!(Node.isFunctionDeclaration(fn) ||
|
|
7551
|
+
Node.isMethodDeclaration(fn) ||
|
|
7552
|
+
Node.isArrowFunction(fn) ||
|
|
7553
|
+
Node.isFunctionExpression(fn)) ||
|
|
7554
|
+
!fn.getBody() ||
|
|
7555
|
+
fn.getSourceFile().isDeclarationFile() ||
|
|
7556
|
+
fn.getSourceFile().isInNodeModules()) {
|
|
7557
|
+
continue;
|
|
7558
|
+
}
|
|
7559
|
+
const parameter = fn.getParameters()[index];
|
|
7560
|
+
const parameterSymbol = parameter?.getNameNode().getSymbol()?.compilerSymbol;
|
|
7561
|
+
if (!parameter || parameter.isRestParameter() || !parameterSymbol)
|
|
7562
|
+
continue;
|
|
7563
|
+
const returned = this.returnedExpressions(fn);
|
|
7564
|
+
let returnsArgument = false;
|
|
7565
|
+
const everyReturnPasses = returned.every((expression) => {
|
|
7566
|
+
let node = expression;
|
|
7567
|
+
while (Node.isParenthesizedExpression(node) ||
|
|
7568
|
+
Node.isAsExpression(node) ||
|
|
7569
|
+
Node.isTypeAssertion(node) ||
|
|
7570
|
+
Node.isSatisfiesExpression(node) ||
|
|
7571
|
+
Node.isNonNullExpression(node)) {
|
|
7572
|
+
node = node.getExpression();
|
|
7573
|
+
}
|
|
7574
|
+
if (Node.isIdentifier(node) && node.getSymbol()?.compilerSymbol === parameterSymbol) {
|
|
7575
|
+
returnsArgument = true;
|
|
7576
|
+
return true;
|
|
7577
|
+
}
|
|
7578
|
+
return this.isEmptyDefault(node);
|
|
7579
|
+
});
|
|
7580
|
+
if (everyReturnPasses && returnsArgument)
|
|
7581
|
+
return fn;
|
|
7582
|
+
}
|
|
7583
|
+
return undefined;
|
|
7584
|
+
}
|
|
7585
|
+
/** `{}`, `[]`, `null` or `undefined`: a default that states no member. */
|
|
7586
|
+
isEmptyDefault(node) {
|
|
7587
|
+
const bare = this.unwrapExpressionNode(node);
|
|
7588
|
+
return ((Node.isObjectLiteralExpression(bare) && bare.getProperties().length === 0) ||
|
|
7589
|
+
(Node.isArrayLiteralExpression(bare) && bare.getElements().length === 0) ||
|
|
7590
|
+
Node.isNullLiteral(bare) ||
|
|
7591
|
+
(Node.isIdentifier(bare) && bare.getText() === 'undefined'));
|
|
7352
7592
|
}
|
|
7353
7593
|
/**
|
|
7354
7594
|
* The part a validated read names when that part is not a body, or
|