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.
Files changed (29) hide show
  1. package/package.json +6 -6
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/sidecar/dist/src/capture/api.d.ts +5 -1
  4. package/sidecar/dist/src/capture/check-classify.js +11 -5
  5. package/sidecar/dist/src/capture/check.js +8 -1
  6. package/sidecar/dist/src/capture/deep-walk.d.ts +31 -0
  7. package/sidecar/dist/src/capture/deep-walk.js +43 -1
  8. package/sidecar/dist/src/capture/deno-project.d.ts +7 -0
  9. package/sidecar/dist/src/capture/deno-project.js +7 -0
  10. package/sidecar/dist/src/capture/index.d.ts +1 -1
  11. package/sidecar/dist/src/capture/index.js +1 -1
  12. package/sidecar/dist/src/capture/service-config.d.ts +11 -0
  13. package/sidecar/dist/src/capture/service-config.js +40 -0
  14. package/sidecar/dist/src/declaration-attribution.d.ts +11 -0
  15. package/sidecar/dist/src/declaration-attribution.js +16 -0
  16. package/sidecar/dist/src/implementations.d.ts +43 -0
  17. package/sidecar/dist/src/implementations.js +224 -0
  18. package/sidecar/dist/src/index.js +35 -1
  19. package/sidecar/dist/src/library-claims.d.ts +17 -2
  20. package/sidecar/dist/src/library-claims.js +242 -47
  21. package/sidecar/dist/src/position-rows.d.ts +119 -1
  22. package/sidecar/dist/src/position-rows.js +163 -9
  23. package/sidecar/dist/src/project-loader.d.ts +31 -0
  24. package/sidecar/dist/src/project-loader.js +67 -4
  25. package/sidecar/dist/src/type-inferrer.d.ts +150 -16
  26. package/sidecar/dist/src/type-inferrer.js +651 -94
  27. package/sidecar/dist/src/types.d.ts +46 -2
  28. package/sidecar/dist/src/validators.d.ts +73 -0
  29. 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.124",
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.124",
117
- "@carrick-tools/cli-darwin-x64": "0.3.124",
118
- "@carrick-tools/cli-linux-arm64": "0.3.124",
119
- "@carrick-tools/cli-linux-x64": "0.3.124",
120
- "@carrick-tools/cli-win32-x64": "0.3.124"
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.124",
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. A mismatch
354
- * that is not a fact stays the mismatch it was: the reading would be
355
- * clearing it against a member that accepts anything.
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 finding = record?.any_provenance?.[0];
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
+ }