carrick 0.3.125 → 0.3.127
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/deno-project.d.ts +7 -0
- package/sidecar/dist/src/capture/deno-project.js +7 -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 +48 -1
- package/sidecar/dist/src/library-claims.d.ts +17 -2
- package/sidecar/dist/src/library-claims.js +297 -52
- 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 +15 -0
- package/sidecar/dist/src/project-loader.js +16 -0
- package/sidecar/dist/src/retype.js +88 -8
- package/sidecar/dist/src/type-inferrer.d.ts +149 -8
- package/sidecar/dist/src/type-inferrer.js +569 -64
- package/sidecar/dist/src/types.d.ts +54 -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.127",
|
|
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.127",
|
|
117
|
+
"@carrick-tools/cli-darwin-x64": "0.3.127",
|
|
118
|
+
"@carrick-tools/cli-linux-arm64": "0.3.127",
|
|
119
|
+
"@carrick-tools/cli-linux-x64": "0.3.127",
|
|
120
|
+
"@carrick-tools/cli-win32-x64": "0.3.127"
|
|
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.127",
|
|
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
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -30,6 +30,7 @@ import { PROGRESS_INTERVAL_MS, atMostEvery } from './progress.js';
|
|
|
30
30
|
import { mergeInferTimings } from './infer-timing.js';
|
|
31
31
|
import { addProgramFiles, listProgramFiles } from './program-files.js';
|
|
32
32
|
import { writePositionRows } from './position-rows.js';
|
|
33
|
+
import { findImplementations } from './implementations.js';
|
|
33
34
|
// ===========================================================================
|
|
34
35
|
// Module-level state
|
|
35
36
|
// ===========================================================================
|
|
@@ -55,6 +56,18 @@ function initializedLoader() {
|
|
|
55
56
|
}
|
|
56
57
|
return projectLoader;
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* carrick#2100: the unwidened reading's budget in milliseconds, when
|
|
61
|
+
* `CARRICK_UNWIDENED_BUDGET_MS` sets one. A switch for tests that drive the
|
|
62
|
+
* scanner's binary: it forces the budget to run out on any machine. Unset or
|
|
63
|
+
* not a whole number of milliseconds, the inferrer's default stands.
|
|
64
|
+
*/
|
|
65
|
+
function unwidenedBudgetFromEnv() {
|
|
66
|
+
const value = process.env.CARRICK_UNWIDENED_BUDGET_MS;
|
|
67
|
+
if (value === undefined || !/^\d+$/.test(value.trim()))
|
|
68
|
+
return undefined;
|
|
69
|
+
return Number(value.trim());
|
|
70
|
+
}
|
|
58
71
|
/**
|
|
59
72
|
* Get the project-backed components, building the project if this is the
|
|
60
73
|
* first request that needs it.
|
|
@@ -77,6 +90,7 @@ function projectComponents(key = '') {
|
|
|
77
90
|
project,
|
|
78
91
|
repoRoot,
|
|
79
92
|
packageOf: (filePath) => loader.packageOf(filePath),
|
|
93
|
+
unwidenedBudgetMs: unwidenedBudgetFromEnv(),
|
|
80
94
|
});
|
|
81
95
|
built = {
|
|
82
96
|
typeBundler: new TypeBundler({ project, repoRoot }),
|
|
@@ -90,7 +104,10 @@ function projectComponents(key = '') {
|
|
|
90
104
|
// (`npm/carrick/package.json`) resolves `typescript ^5.8.0` unpinned,
|
|
91
105
|
// so an install can pair ts-morph's copy with a newer 5.x.
|
|
92
106
|
retyper: new Retyper(project, typeInferrer, jsonWireDeclarations, findDisqualifyingTopTypes),
|
|
93
|
-
claimsVerifier: new LibraryClaimsVerifier(project
|
|
107
|
+
claimsVerifier: new LibraryClaimsVerifier(project, {
|
|
108
|
+
packageDirectoryOf: (file) => loader.packageDirectoryOf(file),
|
|
109
|
+
resolveModule: (name, from, options) => loader.resolveThroughGraph(name, from, options),
|
|
110
|
+
}),
|
|
94
111
|
};
|
|
95
112
|
components.set(key, built);
|
|
96
113
|
}
|
|
@@ -556,6 +573,34 @@ function handleAddProgramFiles(request) {
|
|
|
556
573
|
return { request_id: request.request_id, status: 'error', errors: [error] };
|
|
557
574
|
}
|
|
558
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Handle the 'implementations' action - the implementations of the bodyless
|
|
578
|
+
* declaration each call goes through (carrick#2432). Asked of the analysis
|
|
579
|
+
* process: it reads the programs the service's other actions read, and
|
|
580
|
+
* releases none of them.
|
|
581
|
+
*/
|
|
582
|
+
function handleImplementations(request) {
|
|
583
|
+
try {
|
|
584
|
+
const loader = initializedLoader();
|
|
585
|
+
let done = 0;
|
|
586
|
+
const report = atMostEvery(PROGRESS_INTERVAL_MS, () => writeProgress(request.request_id, 'implementations', `${done} of ${request.queries.length}`));
|
|
587
|
+
const answers = findImplementations({
|
|
588
|
+
loader,
|
|
589
|
+
scanRoot: request.scan_root,
|
|
590
|
+
queries: request.queries,
|
|
591
|
+
onAnswer: () => {
|
|
592
|
+
done += 1;
|
|
593
|
+
report();
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
return { request_id: request.request_id, status: 'success', result: { answers } };
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
600
|
+
logError(`Implementations failed: ${error}`);
|
|
601
|
+
return { request_id: request.request_id, status: 'error', errors: [error] };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
559
604
|
/**
|
|
560
605
|
* Handle the 'position_rows' action - write the compiler's rows for each
|
|
561
606
|
* call, member access, comparison and identifier of the given files
|
|
@@ -648,6 +693,8 @@ function handleRequest(request) {
|
|
|
648
693
|
return handleAddProgramFiles(request);
|
|
649
694
|
case 'position_rows':
|
|
650
695
|
return handlePositionRows(request);
|
|
696
|
+
case 'implementations':
|
|
697
|
+
return handleImplementations(request);
|
|
651
698
|
case 'health':
|
|
652
699
|
return handleHealth(request);
|
|
653
700
|
case 'shutdown':
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
* package's: a `paths` alias or a `declare module` block the service writes
|
|
36
36
|
* for itself is not evidence about the library.
|
|
37
37
|
*/
|
|
38
|
-
import { type Project } from 'ts-morph';
|
|
38
|
+
import { ts, type Project } from 'ts-morph';
|
|
39
|
+
import { type PackageDirectoryOf } from './position-rows.js';
|
|
39
40
|
import type { LibraryCheck, LibrarySurface, SemanticsCheck, SemanticsModule, SemanticsResult } from './types.js';
|
|
40
41
|
export interface VerifyResult {
|
|
41
42
|
semantics: SemanticsResult[];
|
|
@@ -54,9 +55,23 @@ export interface VerifyResult {
|
|
|
54
55
|
* options at argument 1; `request_body` adds the body key there.
|
|
55
56
|
*/
|
|
56
57
|
export declare function httpCheck(check: SemanticsCheck): LibraryCheck;
|
|
58
|
+
/**
|
|
59
|
+
* What a project's module graph says, where it resolved through one rather
|
|
60
|
+
* than through `node_modules` (carrick#2599). Such a resolver serves a package
|
|
61
|
+
* from a cache whose path has no `node_modules` segment, so neither the path
|
|
62
|
+
* nor a Node resolution of the package name finds the files the program
|
|
63
|
+
* loaded.
|
|
64
|
+
*/
|
|
65
|
+
export interface ModuleGraph {
|
|
66
|
+
/** The installed package directory a file lies in, and its name. */
|
|
67
|
+
packageDirectoryOf: PackageDirectoryOf;
|
|
68
|
+
/** How the program resolves `name` from `from`; `undefined` where there is no graph, and Node resolution answers. */
|
|
69
|
+
resolveModule(name: string, from: string, options: ts.CompilerOptions): ts.ResolvedModuleFull | undefined;
|
|
70
|
+
}
|
|
57
71
|
export declare class LibraryClaimsVerifier {
|
|
58
72
|
private readonly project;
|
|
59
|
-
|
|
73
|
+
private readonly graph;
|
|
74
|
+
constructor(project: Project, graph?: ModuleGraph);
|
|
60
75
|
/**
|
|
61
76
|
* Judge every check, in request order, spending at most `budgetMs`. Checks
|
|
62
77
|
* the budget does not reach come back `unchecked` with reason `budget`.
|