carrick 0.3.124 → 0.3.126
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/api.d.ts +5 -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/deno-project.d.ts +7 -0
- package/sidecar/dist/src/capture/deno-project.js +7 -0
- 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/declaration-attribution.d.ts +11 -0
- package/sidecar/dist/src/declaration-attribution.js +16 -0
- package/sidecar/dist/src/implementations.d.ts +43 -0
- package/sidecar/dist/src/implementations.js +224 -0
- package/sidecar/dist/src/index.js +35 -1
- package/sidecar/dist/src/library-claims.d.ts +17 -2
- package/sidecar/dist/src/library-claims.js +242 -47
- package/sidecar/dist/src/position-rows.d.ts +119 -1
- package/sidecar/dist/src/position-rows.js +163 -9
- package/sidecar/dist/src/project-loader.d.ts +31 -0
- package/sidecar/dist/src/project-loader.js +67 -4
- package/sidecar/dist/src/type-inferrer.d.ts +150 -16
- package/sidecar/dist/src/type-inferrer.js +651 -94
- package/sidecar/dist/src/types.d.ts +46 -2
- package/sidecar/dist/src/validators.d.ts +73 -0
- package/sidecar/dist/src/validators.js +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carrick",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.126",
|
|
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.126",
|
|
117
|
+
"@carrick-tools/cli-darwin-x64": "0.3.126",
|
|
118
|
+
"@carrick-tools/cli-linux-arm64": "0.3.126",
|
|
119
|
+
"@carrick-tools/cli-linux-x64": "0.3.126",
|
|
120
|
+
"@carrick-tools/cli-win32-x64": "0.3.126"
|
|
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.126",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Carrick",
|
|
7
7
|
"email": "hello@carrick.tools"
|
|
@@ -212,10 +212,14 @@ export type SelfCheckOutcome = 'ok' | 'allowlisted_external' | 'decayed_internal
|
|
|
212
212
|
* serialised itself, and at least one return builds that string in a way
|
|
213
213
|
* the inferrer cannot trace to a typed value, so it publishes no part of
|
|
214
214
|
* the contract (carrick#2200).
|
|
215
|
+
* - `body_built_by_callee`: a request locator names an argument of a call
|
|
216
|
+
* into a function that builds the request body from it, and the body that
|
|
217
|
+
* function builds cannot be typed, so the argument is not the body and no
|
|
218
|
+
* contract is published (carrick#2628).
|
|
215
219
|
* - `not_recorded`: the position carries a top type and this layer has no
|
|
216
220
|
* cause for it.
|
|
217
221
|
*/
|
|
218
|
-
export type TypeProvenanceReason = 'declared' | 'unresolved_import' | 'budget_exhausted' | 'no_payload_evidence' | 'machinery_envelope' | 'coerced_input' | 'no_success_payload' | 'no_request_body' | 'projected_value_only' | 'serialised_body_unread' | 'not_recorded';
|
|
222
|
+
export type TypeProvenanceReason = 'declared' | 'unresolved_import' | 'budget_exhausted' | 'no_payload_evidence' | 'machinery_envelope' | 'coerced_input' | 'no_success_payload' | 'no_request_body' | 'projected_value_only' | 'serialised_body_unread' | 'body_built_by_callee' | 'not_recorded';
|
|
219
223
|
/**
|
|
220
224
|
* One `any`/`unknown` finding inside a captured or inferred type, with its
|
|
221
225
|
* position and its cause. Sorted by `path` wherever a list is emitted, so the
|
|
@@ -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) {
|
|
@@ -39,6 +39,13 @@ export declare class DenoProject {
|
|
|
39
39
|
* no identity. A file the workspace itself owns belongs to no package and
|
|
40
40
|
* answers nothing, exactly as a workspace-declared type does under Node. */
|
|
41
41
|
packageOf(file: string): string | undefined;
|
|
42
|
+
/** The npm package directory a resolved file lies in, and the package's
|
|
43
|
+
* name: its cache directory, which carries no `node_modules` segment, so
|
|
44
|
+
* only the graph can say where the package starts (carrick#2599). */
|
|
45
|
+
packageDirectoryOf(file: string): {
|
|
46
|
+
directory: string;
|
|
47
|
+
name: string;
|
|
48
|
+
} | undefined;
|
|
42
49
|
private isNpmFile;
|
|
43
50
|
/** Let TypeScript interpret real package exports/types using Deno's exact
|
|
44
51
|
* dependency graph. Only the resolution host sees virtual node_modules;
|
|
@@ -280,6 +280,13 @@ export class DenoProject {
|
|
|
280
280
|
const specifier = this.specifiersByPath.get(resolved);
|
|
281
281
|
return specifier ? jsrPackageName(specifier) : undefined;
|
|
282
282
|
}
|
|
283
|
+
/** The npm package directory a resolved file lies in, and the package's
|
|
284
|
+
* name: its cache directory, which carries no `node_modules` segment, so
|
|
285
|
+
* only the graph can say where the package starts (carrick#2599). */
|
|
286
|
+
packageDirectoryOf(file) {
|
|
287
|
+
const owner = this.npmOwner(path.resolve(file));
|
|
288
|
+
return owner?.localPath ? { directory: owner.localPath, name: owner.name } : undefined;
|
|
289
|
+
}
|
|
283
290
|
isNpmFile(file) {
|
|
284
291
|
return !!this.npmOwner(file) || file.split(path.sep).includes('node_modules');
|
|
285
292
|
}
|
|
@@ -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
|
+
}
|
|
@@ -20,6 +20,17 @@ import type { ts } from 'ts-morph';
|
|
|
20
20
|
* with one declaration needs none.
|
|
21
21
|
*/
|
|
22
22
|
export declare function attributedDeclaration<T extends ts.Node>(programOf: () => ts.Program, declarations: readonly T[]): T | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* The declaration a member is attributed to when it is read on `holder`: its
|
|
25
|
+
* own ({@link attributedDeclaration}), or, for a member with none (a mapped
|
|
26
|
+
* type's), the declaration of the holder's type: its symbol's, else its alias
|
|
27
|
+
* symbol's (carrick#2448). The per-position rows and a library verdict's
|
|
28
|
+
* `declarations` (carrick#2654) both name a member this way, so a verdict
|
|
29
|
+
* joins the row of the call it judges.
|
|
30
|
+
*/
|
|
31
|
+
export declare function memberDeclaration(programOf: () => ts.Program, member: ts.Symbol, holder: ts.Type): ts.Declaration | undefined;
|
|
32
|
+
/** The declaration of a type's symbol, else of its alias symbol: a mapped type node, or the alias naming it. */
|
|
33
|
+
export declare function typeDeclaration(programOf: () => ts.Program, type: ts.Type): ts.Declaration | undefined;
|
|
23
34
|
/**
|
|
24
35
|
* The npm package a declaring file belongs to, by the module graph where
|
|
25
36
|
* there is one and by its path otherwise. `undefined` when neither names one.
|
|
@@ -31,6 +31,22 @@ export function attributedDeclaration(programOf, declarations) {
|
|
|
31
31
|
return pathA < pathB ? -1 : pathA > pathB ? 1 : a.getStart() - b.getStart();
|
|
32
32
|
})[0];
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The declaration a member is attributed to when it is read on `holder`: its
|
|
36
|
+
* own ({@link attributedDeclaration}), or, for a member with none (a mapped
|
|
37
|
+
* type's), the declaration of the holder's type: its symbol's, else its alias
|
|
38
|
+
* symbol's (carrick#2448). The per-position rows and a library verdict's
|
|
39
|
+
* `declarations` (carrick#2654) both name a member this way, so a verdict
|
|
40
|
+
* joins the row of the call it judges.
|
|
41
|
+
*/
|
|
42
|
+
export function memberDeclaration(programOf, member, holder) {
|
|
43
|
+
return attributedDeclaration(programOf, member.declarations ?? []) ?? typeDeclaration(programOf, holder);
|
|
44
|
+
}
|
|
45
|
+
/** The declaration of a type's symbol, else of its alias symbol: a mapped type node, or the alias naming it. */
|
|
46
|
+
export function typeDeclaration(programOf, type) {
|
|
47
|
+
const owner = type.getSymbol() ?? type.aliasSymbol;
|
|
48
|
+
return attributedDeclaration(programOf, owner?.declarations ?? []);
|
|
49
|
+
}
|
|
34
50
|
/**
|
|
35
51
|
* The npm package a declaring file belongs to, by the module graph where
|
|
36
52
|
* there is one and by its path otherwise. `undefined` when neither names one.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The implementations of a bodyless declaration, asked where discovery stops
|
|
3
|
+
* at one (carrick#2432 slice B): a call through an interface's method, a
|
|
4
|
+
* function-typed property or an abstract member, whose receiver the scanner
|
|
5
|
+
* cannot follow to a value.
|
|
6
|
+
*
|
|
7
|
+
* Each question is the span of a `call` (or `member`) position row, in the
|
|
8
|
+
* rows' numbering (UTF-16 code units from the start of the file, as the
|
|
9
|
+
* scanner's `sidecar_position` gives). The call's symbol and declaration are
|
|
10
|
+
* read exactly as the row read them ({@link PositionWalker.symbolOf}), and
|
|
11
|
+
* go-to-implementation is asked once per declaration, at its name, over the
|
|
12
|
+
* language service's program of the project that owns the file. That is the
|
|
13
|
+
* analysis program: nothing here releases it.
|
|
14
|
+
*
|
|
15
|
+
* Each answer:
|
|
16
|
+
* - holds none of the declaration's own signatures: asked at an overloaded
|
|
17
|
+
* interface or abstract method, go-to-implementation names only the bodies
|
|
18
|
+
* that implement it (TypeScript 6.0, pinned in implementations.test.ts).
|
|
19
|
+
* The walk's filter for them (`implementationRows`, removed in #2561) is
|
|
20
|
+
* not carried over: no bodyless declaration reaches it;
|
|
21
|
+
* - names each implementation as a row names a declaration
|
|
22
|
+
* ({@link PositionWalker.located}), at the implementation's declaration's
|
|
23
|
+
* start, so it joins to a definition as a `call` row at it would;
|
|
24
|
+
* - is sorted by file, then position;
|
|
25
|
+
* - holds at most {@link IMPLEMENTATIONS_CAP}; past it, none, and `too_many`.
|
|
26
|
+
*
|
|
27
|
+
* The position rows carry no implementations (version 4, carrick#2561): this
|
|
28
|
+
* action is the only place they are asked.
|
|
29
|
+
*/
|
|
30
|
+
import type { ProjectLoader } from './project-loader.js';
|
|
31
|
+
import type { ImplementationsAnswer, ImplementationsQuery } from './types.js';
|
|
32
|
+
/** The most implementations one answer lists. */
|
|
33
|
+
export declare const IMPLEMENTATIONS_CAP = 16;
|
|
34
|
+
export interface ImplementationsOptions {
|
|
35
|
+
loader: ProjectLoader;
|
|
36
|
+
/** Every name in an answer is relative to it, as in the position rows. */
|
|
37
|
+
scanRoot: string;
|
|
38
|
+
queries: ImplementationsQuery[];
|
|
39
|
+
/** Called after each question is answered. */
|
|
40
|
+
onAnswer?: () => void;
|
|
41
|
+
}
|
|
42
|
+
/** One answer per question, in request order. */
|
|
43
|
+
export declare function findImplementations(options: ImplementationsOptions): ImplementationsAnswer[];
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The implementations of a bodyless declaration, asked where discovery stops
|
|
3
|
+
* at one (carrick#2432 slice B): a call through an interface's method, a
|
|
4
|
+
* function-typed property or an abstract member, whose receiver the scanner
|
|
5
|
+
* cannot follow to a value.
|
|
6
|
+
*
|
|
7
|
+
* Each question is the span of a `call` (or `member`) position row, in the
|
|
8
|
+
* rows' numbering (UTF-16 code units from the start of the file, as the
|
|
9
|
+
* scanner's `sidecar_position` gives). The call's symbol and declaration are
|
|
10
|
+
* read exactly as the row read them ({@link PositionWalker.symbolOf}), and
|
|
11
|
+
* go-to-implementation is asked once per declaration, at its name, over the
|
|
12
|
+
* language service's program of the project that owns the file. That is the
|
|
13
|
+
* analysis program: nothing here releases it.
|
|
14
|
+
*
|
|
15
|
+
* Each answer:
|
|
16
|
+
* - holds none of the declaration's own signatures: asked at an overloaded
|
|
17
|
+
* interface or abstract method, go-to-implementation names only the bodies
|
|
18
|
+
* that implement it (TypeScript 6.0, pinned in implementations.test.ts).
|
|
19
|
+
* The walk's filter for them (`implementationRows`, removed in #2561) is
|
|
20
|
+
* not carried over: no bodyless declaration reaches it;
|
|
21
|
+
* - names each implementation as a row names a declaration
|
|
22
|
+
* ({@link PositionWalker.located}), at the implementation's declaration's
|
|
23
|
+
* start, so it joins to a definition as a `call` row at it would;
|
|
24
|
+
* - is sorted by file, then position;
|
|
25
|
+
* - holds at most {@link IMPLEMENTATIONS_CAP}; past it, none, and `too_many`.
|
|
26
|
+
*
|
|
27
|
+
* The position rows carry no implementations (version 4, carrick#2561): this
|
|
28
|
+
* action is the only place they are asked.
|
|
29
|
+
*/
|
|
30
|
+
import * as path from 'node:path';
|
|
31
|
+
import { ts } from 'ts-morph';
|
|
32
|
+
import { attributedDeclaration } from './declaration-attribution.js';
|
|
33
|
+
import { PathNames, PositionWalker, compareNames, kindOf } from './position-rows.js';
|
|
34
|
+
/** The most implementations one answer lists. */
|
|
35
|
+
export const IMPLEMENTATIONS_CAP = 16;
|
|
36
|
+
/** One answer per question, in request order. */
|
|
37
|
+
export function findImplementations(options) {
|
|
38
|
+
const { loader, scanRoot, queries } = options;
|
|
39
|
+
const paths = new PathNames(scanRoot);
|
|
40
|
+
const answers = new Array(queries.length);
|
|
41
|
+
const groups = new Map();
|
|
42
|
+
queries.forEach((query, index) => {
|
|
43
|
+
const key = loader.projectKeyFor(path.resolve(query.file));
|
|
44
|
+
groups.set(key, [...(groups.get(key) ?? []), index]);
|
|
45
|
+
});
|
|
46
|
+
for (const [key, indexes] of groups) {
|
|
47
|
+
const service = loader.getProjectFor(key).getLanguageService().compilerObject;
|
|
48
|
+
const program = service.getProgram();
|
|
49
|
+
if (!program)
|
|
50
|
+
throw new Error('the language service built no program');
|
|
51
|
+
const finder = new ImplementationFinder(program, service, new PositionWalker(program, paths, loader));
|
|
52
|
+
for (const index of indexes) {
|
|
53
|
+
answers[index] = finder.answer(queries[index]);
|
|
54
|
+
options.onAnswer?.();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return answers;
|
|
58
|
+
}
|
|
59
|
+
/** An answer with nothing found, and why. */
|
|
60
|
+
function none(reason, declaration = null) {
|
|
61
|
+
return { declaration, implementations: [], too_many: false, reason };
|
|
62
|
+
}
|
|
63
|
+
class ImplementationFinder {
|
|
64
|
+
program;
|
|
65
|
+
service;
|
|
66
|
+
walker;
|
|
67
|
+
checker;
|
|
68
|
+
/** Answers by declaration: a declaration is searched once, however many calls reach it. */
|
|
69
|
+
searched = new Map();
|
|
70
|
+
constructor(program, service, walker) {
|
|
71
|
+
this.program = program;
|
|
72
|
+
this.service = service;
|
|
73
|
+
this.walker = walker;
|
|
74
|
+
this.checker = program.getTypeChecker();
|
|
75
|
+
}
|
|
76
|
+
answer(query) {
|
|
77
|
+
const sourceFile = this.program.getSourceFile(path.resolve(query.file));
|
|
78
|
+
if (!sourceFile)
|
|
79
|
+
return none('not_in_program');
|
|
80
|
+
const found = positionAt(sourceFile, query.start, query.end);
|
|
81
|
+
if (!found)
|
|
82
|
+
return none('no_call');
|
|
83
|
+
let declaration;
|
|
84
|
+
let named = null;
|
|
85
|
+
try {
|
|
86
|
+
const symbol = this.walker.symbolOf(found.kind, found.node);
|
|
87
|
+
if (!symbol)
|
|
88
|
+
return none('no_symbol');
|
|
89
|
+
declaration = attributedDeclaration(() => this.program, symbol.declarations ?? []);
|
|
90
|
+
if (!declaration)
|
|
91
|
+
return none('no_declaration');
|
|
92
|
+
named = this.named(declaration, ts.symbolName(symbol));
|
|
93
|
+
if (!isBodyless(declaration, this.checker))
|
|
94
|
+
return none('has_body', named);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return none('checker_threw', named);
|
|
98
|
+
}
|
|
99
|
+
let searched = this.searched.get(declaration);
|
|
100
|
+
if (!searched) {
|
|
101
|
+
try {
|
|
102
|
+
searched = this.implementationsOf(declaration);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return none('checker_threw', named);
|
|
106
|
+
}
|
|
107
|
+
this.searched.set(declaration, searched);
|
|
108
|
+
}
|
|
109
|
+
return { declaration: named, ...searched };
|
|
110
|
+
}
|
|
111
|
+
/** A declaration as a row names it: its symbol, file and line. */
|
|
112
|
+
named(declaration, symbol) {
|
|
113
|
+
const file = declaration.getSourceFile();
|
|
114
|
+
const { declFile, declLine, declPackage } = this.walker.located(file, declaration.getStart(file));
|
|
115
|
+
return { symbol, decl_file: declFile, decl_line: declLine, decl_package: declPackage };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* What go-to-implementation names for `declaration`, asked at its name,
|
|
119
|
+
* in file and position order.
|
|
120
|
+
*/
|
|
121
|
+
implementationsOf(declaration) {
|
|
122
|
+
const name = ts.getNameOfDeclaration(declaration) ?? declaration;
|
|
123
|
+
const declaringFile = name.getSourceFile();
|
|
124
|
+
const found = new Map();
|
|
125
|
+
for (const location of this.service.getImplementationAtPosition(declaringFile.fileName, name.getStart(declaringFile)) ?? []) {
|
|
126
|
+
const file = this.program.getSourceFile(location.fileName);
|
|
127
|
+
if (!file)
|
|
128
|
+
continue;
|
|
129
|
+
const start = location.textSpan.start;
|
|
130
|
+
found.set(`${location.fileName}\u0000${start}`, { file, start });
|
|
131
|
+
}
|
|
132
|
+
if (found.size > IMPLEMENTATIONS_CAP)
|
|
133
|
+
return { implementations: [], too_many: true };
|
|
134
|
+
const implementations = [...found.values()].map(({ file, start }) => {
|
|
135
|
+
const at = nodeStartingAt(file, start);
|
|
136
|
+
const symbol = at ? this.checker.getSymbolAtLocation(at) : undefined;
|
|
137
|
+
// The declaration the answer points into, so its line is the one a
|
|
138
|
+
// `call` row at the implementation names: the declaration's start,
|
|
139
|
+
// decorators and modifiers included, not its name's.
|
|
140
|
+
const holder = symbol?.declarations?.find((d) => d.getSourceFile() === file && d.getStart(file) <= start && start < d.end) ??
|
|
141
|
+
(at ? enclosingDeclaration(at) : undefined);
|
|
142
|
+
const placed = holder ? holder.getStart(file) : start;
|
|
143
|
+
const { declFile, declLine, declPackage } = this.walker.located(file, placed);
|
|
144
|
+
return {
|
|
145
|
+
file: declFile,
|
|
146
|
+
start: placed,
|
|
147
|
+
named: { symbol: symbol ? ts.symbolName(symbol) : null, decl_file: declFile, decl_line: declLine, decl_package: declPackage },
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
implementations.sort((a, b) => compareNames(a.file, b.file) || a.start - b.start);
|
|
151
|
+
return { implementations: implementations.map(({ named }) => named), too_many: false };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The `call` or `member` position whose span is exactly `start..end`: the
|
|
156
|
+
* node a row of that kind was written at. A call is preferred where both
|
|
157
|
+
* kinds share the span.
|
|
158
|
+
*/
|
|
159
|
+
function positionAt(file, start, end) {
|
|
160
|
+
let member;
|
|
161
|
+
let call;
|
|
162
|
+
const visit = (node) => {
|
|
163
|
+
const nodeStart = node.getStart(file);
|
|
164
|
+
if (end <= nodeStart || node.getEnd() < start)
|
|
165
|
+
return;
|
|
166
|
+
if (nodeStart === start && node.getEnd() === end) {
|
|
167
|
+
const kind = kindOf(node);
|
|
168
|
+
if (kind === 'call')
|
|
169
|
+
call ??= node;
|
|
170
|
+
if (kind === 'member')
|
|
171
|
+
member ??= node;
|
|
172
|
+
}
|
|
173
|
+
ts.forEachChild(node, visit);
|
|
174
|
+
};
|
|
175
|
+
ts.forEachChild(file, visit);
|
|
176
|
+
if (call)
|
|
177
|
+
return { kind: 'call', node: call };
|
|
178
|
+
if (member)
|
|
179
|
+
return { kind: 'member', node: member };
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Whether a declaration has no body for a call to run: an interface's or type
|
|
184
|
+
* literal's method, a function-typed property signature, an abstract method or
|
|
185
|
+
* property, or a function-typed class property or parameter with no
|
|
186
|
+
* initializer. Its implementations are what go-to-implementation names.
|
|
187
|
+
*/
|
|
188
|
+
function isBodyless(declaration, checker) {
|
|
189
|
+
const functionTyped = () => checker.getTypeAtLocation(declaration).getNonNullableType().getCallSignatures().length > 0;
|
|
190
|
+
if (ts.isMethodSignature(declaration))
|
|
191
|
+
return true;
|
|
192
|
+
if (ts.isPropertySignature(declaration))
|
|
193
|
+
return functionTyped();
|
|
194
|
+
if (ts.isMethodDeclaration(declaration) || ts.isPropertyDeclaration(declaration)) {
|
|
195
|
+
if (ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Abstract)
|
|
196
|
+
return true;
|
|
197
|
+
return ts.isPropertyDeclaration(declaration) && !declaration.initializer && functionTyped();
|
|
198
|
+
}
|
|
199
|
+
if (ts.isParameter(declaration))
|
|
200
|
+
return !declaration.initializer && functionTyped();
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
/** The innermost node of `file` that starts at `position` (an implementation's name). */
|
|
204
|
+
function nodeStartingAt(file, position) {
|
|
205
|
+
let found;
|
|
206
|
+
const visit = (node) => {
|
|
207
|
+
if (position < node.getStart(file) || position >= node.getEnd())
|
|
208
|
+
return;
|
|
209
|
+
if (node.getStart(file) === position)
|
|
210
|
+
found = node;
|
|
211
|
+
ts.forEachChild(node, visit);
|
|
212
|
+
};
|
|
213
|
+
ts.forEachChild(file, visit);
|
|
214
|
+
return found;
|
|
215
|
+
}
|
|
216
|
+
/** The nearest declaration holding `node`: what an answer with no symbol points into. */
|
|
217
|
+
function enclosingDeclaration(node) {
|
|
218
|
+
for (let at = node; at && !ts.isSourceFile(at); at = at.parent) {
|
|
219
|
+
if (ts.isFunctionLike(at) || ts.isClassLike(at) || ts.isPropertyAssignment(at) || ts.isVariableDeclaration(at)) {
|
|
220
|
+
return at;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|