ast-mcp-server 0.5.1 → 0.7.0
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/CHANGELOG.md +36 -0
- package/README.md +63 -14
- package/SECURITY.md +31 -0
- package/dist/cli.js +0 -0
- package/dist/index.js +129 -10
- package/dist/server.js +25 -2
- package/dist/services/agent-setup.js +36 -117
- package/dist/services/agent-targets.js +122 -0
- package/dist/services/context-builder.js +262 -0
- package/dist/services/diagnostics.js +10 -5
- package/dist/services/file-fingerprints.js +104 -0
- package/dist/services/file-snapshot.js +103 -0
- package/dist/services/impact.js +334 -0
- package/dist/services/operations.js +343 -254
- package/dist/services/outline.js +94 -21
- package/dist/services/pagination.js +10 -4
- package/dist/services/project-operation-scheduler.js +475 -0
- package/dist/services/project-status.js +134 -52
- package/dist/services/project-watcher.js +230 -0
- package/dist/services/project.js +646 -66
- package/dist/services/public-errors.js +255 -0
- package/dist/services/references.js +54 -0
- package/dist/services/relationships.js +1198 -0
- package/dist/services/request-context.js +42 -0
- package/dist/services/runtime-activity.js +65 -0
- package/dist/services/runtime-logger.js +50 -0
- package/dist/services/runtime-policy.js +47 -0
- package/dist/services/setup-wizard.js +4 -7
- package/dist/services/shutdown.js +93 -0
- package/dist/services/symbol-index-limits.js +5 -0
- package/dist/services/symbol-index-order.js +13 -0
- package/dist/services/symbol-index-policy.js +91 -0
- package/dist/services/symbol-index-sqlite.js +850 -0
- package/dist/services/symbol-index.js +447 -0
- package/dist/services/symbols.js +430 -47
- package/dist/services/test-candidates.js +209 -0
- package/dist/services/workspace.js +18 -7
- package/dist/tools/apply_operation.js +8 -4
- package/dist/tools/explore.js +176 -0
- package/dist/tools/find_references.js +17 -55
- package/dist/tools/get_diagnostics.js +10 -7
- package/dist/tools/get_file.js +107 -0
- package/dist/tools/get_impact.js +134 -0
- package/dist/tools/get_operation_preview.js +7 -4
- package/dist/tools/get_outline.js +8 -6
- package/dist/tools/get_project_status.js +77 -8
- package/dist/tools/get_symbol_source.js +8 -6
- package/dist/tools/list_files.js +11 -6
- package/dist/tools/rename_symbol.js +6 -4
- package/dist/tools/replace_symbol_body.js +6 -4
- package/dist/tools/result.js +27 -17
- package/dist/tools/scaffold_class.js +6 -4
- package/dist/tools/search_symbols.js +15 -63
- package/docs/support.md +80 -0
- package/package.json +18 -6
- package/skills/structural-code-editing/SKILL.md +42 -8
- package/benchmark/README.md +0 -129
package/dist/services/symbols.js
CHANGED
|
@@ -1,4 +1,62 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Node, ts } from "ts-morph";
|
|
3
|
+
import { buildFileOutline, declarationSignature } from "./outline.js";
|
|
4
|
+
import { isCooperativeInterruption, NO_REQUEST_CONTEXT, } from "./request-context.js";
|
|
5
|
+
import { MAX_SYMBOL_INDEX_QUERY_CANDIDATES } from "./symbol-index-limits.js";
|
|
6
|
+
import { compareSymbolIndexText } from "./symbol-index-order.js";
|
|
7
|
+
class IndexedProjectionMismatchError extends Error {
|
|
8
|
+
code = "corrupt_storage";
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "IndexedProjectionMismatchError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function sourceFileSymbols(sourceFile, projectRoot, requestContext = NO_REQUEST_CONTEXT) {
|
|
15
|
+
const file = path.relative(projectRoot, sourceFile.getFilePath());
|
|
16
|
+
const signatures = new Map(buildFileOutline(sourceFile, requestContext).symbols.map((symbol) => [
|
|
17
|
+
`${symbol.symbolPath}@${symbol.startLine}`,
|
|
18
|
+
symbol.signature,
|
|
19
|
+
]));
|
|
20
|
+
return collectSymbols(sourceFile).map((symbol) => ({
|
|
21
|
+
file,
|
|
22
|
+
symbol_path: symbol.symbolPath,
|
|
23
|
+
selector: `${symbol.symbolPath}@${symbol.line}`,
|
|
24
|
+
name: symbol.name,
|
|
25
|
+
kind: symbol.kind,
|
|
26
|
+
line: symbol.line,
|
|
27
|
+
signature: signatures.get(`${symbol.symbolPath}@${symbol.line}`) ?? symbol.node.getText(),
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
export function sourceFileIndexSymbols(sourceFile, projectRoot) {
|
|
31
|
+
const outlineSignatures = new Map(buildFileOutline(sourceFile).symbols.map((symbol) => [
|
|
32
|
+
`${symbol.symbolPath}@${symbol.startLine}`,
|
|
33
|
+
symbol.signature,
|
|
34
|
+
]));
|
|
35
|
+
const locatedSymbols = new Map(collectSymbols(sourceFile).map((symbol) => [`${symbol.symbolPath}@${symbol.line}`, symbol]));
|
|
36
|
+
return sourceFileSymbols(sourceFile, projectRoot).map((symbol) => {
|
|
37
|
+
const located = locatedSymbols.get(symbol.selector);
|
|
38
|
+
if (!located) {
|
|
39
|
+
throw new Error(`Unable to locate indexed symbol ${symbol.selector}.`);
|
|
40
|
+
}
|
|
41
|
+
const start = sourceFile.getLineAndColumnAtPos(located.node.getStart());
|
|
42
|
+
const end = sourceFile.getLineAndColumnAtPos(Math.max(located.node.getStart(), located.node.getEnd() - 1));
|
|
43
|
+
const range = {
|
|
44
|
+
start_line: start.line,
|
|
45
|
+
start_column: start.column,
|
|
46
|
+
end_line: end.line,
|
|
47
|
+
end_column: end.column,
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
name: symbol.name,
|
|
51
|
+
symbol_path: symbol.symbol_path,
|
|
52
|
+
selector: symbol.selector,
|
|
53
|
+
kind: symbol.kind,
|
|
54
|
+
signature: outlineSignatures.get(symbol.selector) ?? `${symbol.kind} ${symbol.name};`,
|
|
55
|
+
line: symbol.line,
|
|
56
|
+
range,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
2
60
|
export function symbolMatchRank(query, symbol) {
|
|
3
61
|
const normalizedQuery = query.toLowerCase();
|
|
4
62
|
const normalizedPath = symbol.symbolPath.toLowerCase();
|
|
@@ -15,6 +73,237 @@ export function symbolMatchRank(query, symbol) {
|
|
|
15
73
|
}
|
|
16
74
|
return 4;
|
|
17
75
|
}
|
|
76
|
+
export function searchProjectSymbols(project, projectRoot, options, requestContext = NO_REQUEST_CONTEXT) {
|
|
77
|
+
return searchProjectSymbolsPage(project, projectRoot, options, 0, MAX_SYMBOL_INDEX_QUERY_CANDIDATES, requestContext).items;
|
|
78
|
+
}
|
|
79
|
+
function candidateOrder(left, right) {
|
|
80
|
+
return (left.rank - right.rank ||
|
|
81
|
+
compareSymbolIndexText(left.file, right.file) ||
|
|
82
|
+
left.symbol.line - right.symbol.line ||
|
|
83
|
+
left.symbolPosition - right.symbolPosition);
|
|
84
|
+
}
|
|
85
|
+
function assertSearchPage(offset, limit) {
|
|
86
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
87
|
+
throw new Error("Symbol search offset must be a non-negative safe integer.");
|
|
88
|
+
}
|
|
89
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_SYMBOL_INDEX_QUERY_CANDIDATES) {
|
|
90
|
+
throw new Error(`Symbol search limit must be an integer from 1 to ${MAX_SYMBOL_INDEX_QUERY_CANDIDATES}.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function searchProjectSymbolsPage(project, projectRoot, options, offset, limit, requestContext = NO_REQUEST_CONTEXT) {
|
|
94
|
+
assertSearchPage(offset, limit);
|
|
95
|
+
const normalizedQuery = options.query.toLowerCase();
|
|
96
|
+
const normalizedFileFilter = options.fileFilter?.toLowerCase();
|
|
97
|
+
const kindSet = options.kinds ? new Set(options.kinds) : undefined;
|
|
98
|
+
const sourceFiles = project
|
|
99
|
+
.getSourceFiles()
|
|
100
|
+
.map((sourceFile) => ({
|
|
101
|
+
sourceFile,
|
|
102
|
+
file: path.relative(projectRoot, sourceFile.getFilePath()).replaceAll("\\", "/"),
|
|
103
|
+
}))
|
|
104
|
+
.sort((left, right) => compareSymbolIndexText(left.file, right.file));
|
|
105
|
+
const rankCounts = [0, 0, 0, 0, 0];
|
|
106
|
+
for (const { sourceFile, file } of sourceFiles) {
|
|
107
|
+
requestContext.checkpoint();
|
|
108
|
+
if (normalizedFileFilter && !file.toLowerCase().includes(normalizedFileFilter))
|
|
109
|
+
continue;
|
|
110
|
+
forEachLocatedSymbol(sourceFile, (symbol) => {
|
|
111
|
+
requestContext.checkpoint();
|
|
112
|
+
const selector = `${symbol.symbolPath}@${symbol.line}`;
|
|
113
|
+
if ((!kindSet || kindSet.has(symbol.kind)) &&
|
|
114
|
+
(symbol.name.toLowerCase().includes(normalizedQuery) ||
|
|
115
|
+
symbol.symbolPath.toLowerCase().includes(normalizedQuery) ||
|
|
116
|
+
selector.toLowerCase().includes(normalizedQuery))) {
|
|
117
|
+
rankCounts[symbolMatchRank(options.query, symbol)] += 1;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const total = rankCounts.reduce((sum, count) => sum + count, 0);
|
|
122
|
+
const rankSkips = [0, 0, 0, 0, 0];
|
|
123
|
+
const rankTakes = [0, 0, 0, 0, 0];
|
|
124
|
+
let rankOffset = 0;
|
|
125
|
+
for (let rank = 0; rank < rankCounts.length; rank += 1) {
|
|
126
|
+
const rankEnd = rankOffset + rankCounts[rank];
|
|
127
|
+
const pageStart = Math.max(offset, rankOffset);
|
|
128
|
+
const pageEnd = Math.min(offset + limit, rankEnd);
|
|
129
|
+
if (pageStart < pageEnd) {
|
|
130
|
+
rankSkips[rank] = pageStart - rankOffset;
|
|
131
|
+
rankTakes[rank] = pageEnd - pageStart;
|
|
132
|
+
}
|
|
133
|
+
rankOffset = rankEnd;
|
|
134
|
+
}
|
|
135
|
+
const seenByRank = [0, 0, 0, 0, 0];
|
|
136
|
+
const selectedByRank = [[], [], [], [], []];
|
|
137
|
+
for (const { sourceFile, file } of sourceFiles) {
|
|
138
|
+
requestContext.checkpoint();
|
|
139
|
+
if (normalizedFileFilter && !file.toLowerCase().includes(normalizedFileFilter))
|
|
140
|
+
continue;
|
|
141
|
+
let symbolPosition = 0;
|
|
142
|
+
forEachLocatedSymbol(sourceFile, (symbol) => {
|
|
143
|
+
requestContext.checkpoint();
|
|
144
|
+
const selector = `${symbol.symbolPath}@${symbol.line}`;
|
|
145
|
+
const currentSymbolPosition = symbolPosition;
|
|
146
|
+
symbolPosition += 1;
|
|
147
|
+
if ((kindSet && !kindSet.has(symbol.kind)) ||
|
|
148
|
+
(!symbol.name.toLowerCase().includes(normalizedQuery) &&
|
|
149
|
+
!symbol.symbolPath.toLowerCase().includes(normalizedQuery) &&
|
|
150
|
+
!selector.toLowerCase().includes(normalizedQuery))) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const rank = symbolMatchRank(options.query, symbol);
|
|
154
|
+
const position = seenByRank[rank];
|
|
155
|
+
seenByRank[rank] += 1;
|
|
156
|
+
if (position < rankSkips[rank] || selectedByRank[rank].length >= rankTakes[rank]) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
selectedByRank[rank].push({
|
|
160
|
+
file,
|
|
161
|
+
selector,
|
|
162
|
+
symbol,
|
|
163
|
+
rank,
|
|
164
|
+
symbolPosition: currentSymbolPosition,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
requestContext.checkpoint();
|
|
169
|
+
const pageCandidates = selectedByRank.flat().sort(candidateOrder);
|
|
170
|
+
const items = pageCandidates.map((match) => {
|
|
171
|
+
requestContext.checkpoint();
|
|
172
|
+
return {
|
|
173
|
+
file: match.file,
|
|
174
|
+
symbol_path: match.symbol.symbolPath,
|
|
175
|
+
selector: match.selector,
|
|
176
|
+
name: match.symbol.name,
|
|
177
|
+
kind: match.symbol.kind,
|
|
178
|
+
line: match.symbol.line,
|
|
179
|
+
signature: declarationSignature(match.symbol.node, requestContext),
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
const nextOffset = offset + items.length;
|
|
183
|
+
const hasMore = nextOffset < total;
|
|
184
|
+
return {
|
|
185
|
+
items,
|
|
186
|
+
offset,
|
|
187
|
+
limit,
|
|
188
|
+
total,
|
|
189
|
+
has_more: hasMore,
|
|
190
|
+
next_offset: hasMore ? nextOffset : null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export async function searchProjectSymbolsWithIndex(project, projectRoot, projectIdentity, symbolIndex, symbolIndexReady, options, onIndexFailure, requestContext = NO_REQUEST_CONTEXT) {
|
|
194
|
+
const page = await searchProjectSymbolsPageWithIndex(project, projectRoot, projectIdentity, symbolIndex, symbolIndexReady, options, 0, MAX_SYMBOL_INDEX_QUERY_CANDIDATES, onIndexFailure, requestContext);
|
|
195
|
+
return page?.items;
|
|
196
|
+
}
|
|
197
|
+
function sameSymbolRecord(left, right) {
|
|
198
|
+
return (left.file === right.file &&
|
|
199
|
+
left.symbol_path === right.symbol_path &&
|
|
200
|
+
left.selector === right.selector &&
|
|
201
|
+
left.name === right.name &&
|
|
202
|
+
left.kind === right.kind &&
|
|
203
|
+
left.line === right.line &&
|
|
204
|
+
left.signature === right.signature);
|
|
205
|
+
}
|
|
206
|
+
export async function searchProjectSymbolsPageWithIndex(project, projectRoot, projectIdentity, symbolIndex, symbolIndexReady, options, offset, limit, onIndexFailure, requestContext = NO_REQUEST_CONTEXT) {
|
|
207
|
+
requestContext.checkpoint();
|
|
208
|
+
if (!symbolIndexReady)
|
|
209
|
+
return undefined;
|
|
210
|
+
const canonical = searchProjectSymbolsPage(project, projectRoot, options, offset, limit, requestContext);
|
|
211
|
+
try {
|
|
212
|
+
requestContext.checkpoint();
|
|
213
|
+
const indexQuery = {
|
|
214
|
+
project: projectIdentity,
|
|
215
|
+
query: options.query,
|
|
216
|
+
filters: {
|
|
217
|
+
...(options.kinds ? { kinds: options.kinds } : {}),
|
|
218
|
+
...(options.fileFilter ? { file_path: options.fileFilter } : {}),
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
const indexedTotal = await symbolIndex.countSymbols(indexQuery);
|
|
222
|
+
requestContext.checkpoint();
|
|
223
|
+
let complete = indexedTotal === canonical.total;
|
|
224
|
+
if (complete) {
|
|
225
|
+
const matches = await symbolIndex.querySymbols({ ...indexQuery, offset, limit });
|
|
226
|
+
requestContext.checkpoint();
|
|
227
|
+
const indexedPage = matches.map((match) => {
|
|
228
|
+
requestContext.checkpoint();
|
|
229
|
+
return validateIndexedSymbol(project, projectRoot, match, requestContext);
|
|
230
|
+
});
|
|
231
|
+
complete =
|
|
232
|
+
indexedPage.length === canonical.items.length &&
|
|
233
|
+
indexedPage.every((record, index) => sameSymbolRecord(record, canonical.items[index]));
|
|
234
|
+
}
|
|
235
|
+
if (!complete) {
|
|
236
|
+
requestContext.checkpoint();
|
|
237
|
+
try {
|
|
238
|
+
await onIndexFailure?.("corrupt_storage");
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (isCooperativeInterruption(error))
|
|
242
|
+
throw error;
|
|
243
|
+
// Failure reporting cannot suppress the compiler-authoritative result.
|
|
244
|
+
}
|
|
245
|
+
requestContext.checkpoint();
|
|
246
|
+
return canonical;
|
|
247
|
+
}
|
|
248
|
+
return canonical;
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
requestContext.checkpoint();
|
|
252
|
+
if (isCooperativeInterruption(error))
|
|
253
|
+
throw error;
|
|
254
|
+
if (error.code !== "scan_limit_exceeded") {
|
|
255
|
+
try {
|
|
256
|
+
await onIndexFailure?.(typeof error?.code === "string"
|
|
257
|
+
? error.code
|
|
258
|
+
: "sqlite_read_failed");
|
|
259
|
+
}
|
|
260
|
+
catch (reportError) {
|
|
261
|
+
if (isCooperativeInterruption(reportError))
|
|
262
|
+
throw reportError;
|
|
263
|
+
// Failure reporting cannot suppress the compiler-authoritative result.
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
requestContext.checkpoint();
|
|
267
|
+
return canonical;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function validateIndexedSymbol(project, projectRoot, match, requestContext) {
|
|
271
|
+
const sourceFile = project.getSourceFile(path.resolve(projectRoot, match.file_path));
|
|
272
|
+
if (!sourceFile) {
|
|
273
|
+
throw new IndexedProjectionMismatchError(`Indexed source file "${match.file_path}" was not found in the compiler project.`);
|
|
274
|
+
}
|
|
275
|
+
const expectedFile = path.relative(projectRoot, sourceFile.getFilePath()).replaceAll("\\", "/");
|
|
276
|
+
if (expectedFile !== match.file_path.replaceAll("\\", "/")) {
|
|
277
|
+
throw new IndexedProjectionMismatchError(`Indexed source file "${match.file_path}" resolved ambiguously.`);
|
|
278
|
+
}
|
|
279
|
+
const located = findLocatedSymbol(sourceFile, match.selector);
|
|
280
|
+
if (!located) {
|
|
281
|
+
throw new IndexedProjectionMismatchError(`Indexed symbol "${match.selector}" does not exist in the compiler project.`);
|
|
282
|
+
}
|
|
283
|
+
const node = located.node;
|
|
284
|
+
if (node.getStartLineNumber() !== match.line || node.getKindName() !== match.kind) {
|
|
285
|
+
throw new IndexedProjectionMismatchError(`Indexed symbol "${match.selector}" does not match the compiler declaration.`);
|
|
286
|
+
}
|
|
287
|
+
const canonicalName = namedNode(node);
|
|
288
|
+
const canonicalSymbolPath = located.symbolPath;
|
|
289
|
+
const canonicalSignature = declarationSignature(node, requestContext);
|
|
290
|
+
if (!canonicalName ||
|
|
291
|
+
canonicalSymbolPath !== match.symbol_path ||
|
|
292
|
+
canonicalName !== match.name ||
|
|
293
|
+
canonicalSignature !== match.signature ||
|
|
294
|
+
node.getStartLineNumber() !== match.range.start_line) {
|
|
295
|
+
throw new IndexedProjectionMismatchError(`Indexed symbol "${match.selector}" does not match canonical compiler metadata.`);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
file: expectedFile,
|
|
299
|
+
symbol_path: canonicalSymbolPath,
|
|
300
|
+
selector: match.selector,
|
|
301
|
+
name: canonicalName,
|
|
302
|
+
kind: node.getKindName(),
|
|
303
|
+
line: node.getStartLineNumber(),
|
|
304
|
+
signature: canonicalSignature,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
18
307
|
function namedNode(node) {
|
|
19
308
|
if (Node.isConstructorDeclaration(node))
|
|
20
309
|
return "constructor";
|
|
@@ -35,82 +324,176 @@ function namedNode(node) {
|
|
|
35
324
|
}
|
|
36
325
|
return undefined;
|
|
37
326
|
}
|
|
38
|
-
function
|
|
327
|
+
function locatedSymbol(node, symbolPath) {
|
|
39
328
|
const name = namedNode(node);
|
|
40
329
|
if (!name)
|
|
41
|
-
return;
|
|
42
|
-
|
|
330
|
+
return undefined;
|
|
331
|
+
return {
|
|
43
332
|
node,
|
|
44
333
|
symbolPath,
|
|
45
334
|
name,
|
|
46
335
|
kind: node.getKindName(),
|
|
47
336
|
line: node.getStartLineNumber(),
|
|
48
|
-
}
|
|
337
|
+
};
|
|
49
338
|
}
|
|
50
|
-
function
|
|
339
|
+
function visitClassLike(node, prefix, visit) {
|
|
51
340
|
if (!Node.isClassDeclaration(node) && !Node.isInterfaceDeclaration(node))
|
|
52
|
-
return;
|
|
53
|
-
|
|
341
|
+
return true;
|
|
342
|
+
let complete = true;
|
|
343
|
+
node.forEachChild((member) => {
|
|
54
344
|
const name = namedNode(member);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
345
|
+
const located = name ? locatedSymbol(member, `${prefix}.${name}`) : undefined;
|
|
346
|
+
if (located && visit(located) === false) {
|
|
347
|
+
complete = false;
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
return undefined;
|
|
351
|
+
});
|
|
352
|
+
return complete;
|
|
58
353
|
}
|
|
59
|
-
function
|
|
354
|
+
function visitModuleStatements(module, prefix, visit) {
|
|
60
355
|
let body = module.getBody();
|
|
61
356
|
while (body && Node.isModuleDeclaration(body))
|
|
62
357
|
body = body.getBody();
|
|
63
|
-
return body
|
|
358
|
+
return !body || !Node.isModuleBlock(body) || visitChildStatements(body, prefix, visit);
|
|
64
359
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
360
|
+
function visitStatement(statement, prefix, visit) {
|
|
361
|
+
if (Node.isVariableStatement(statement)) {
|
|
362
|
+
let complete = true;
|
|
363
|
+
statement.getDeclarationList().forEachChild((declaration) => {
|
|
364
|
+
if (Node.isVariableDeclaration(declaration)) {
|
|
69
365
|
const path = prefix ? `${prefix}.${declaration.getName()}` : declaration.getName();
|
|
70
|
-
|
|
366
|
+
const located = locatedSymbol(declaration, path);
|
|
367
|
+
if (located && visit(located) === false) {
|
|
368
|
+
complete = false;
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
71
371
|
}
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!name)
|
|
76
|
-
continue;
|
|
77
|
-
const symbolPath = prefix ? `${prefix}.${name}` : name;
|
|
78
|
-
addLocated(statement, symbolPath, output);
|
|
79
|
-
collectClassLike(statement, symbolPath, output);
|
|
80
|
-
if (Node.isModuleDeclaration(statement)) {
|
|
81
|
-
collectStatements(moduleStatements(statement), symbolPath, output);
|
|
82
|
-
}
|
|
372
|
+
return undefined;
|
|
373
|
+
});
|
|
374
|
+
return complete;
|
|
83
375
|
}
|
|
376
|
+
const name = namedNode(statement);
|
|
377
|
+
if (!name)
|
|
378
|
+
return true;
|
|
379
|
+
const symbolPath = prefix ? `${prefix}.${name}` : name;
|
|
380
|
+
const located = locatedSymbol(statement, symbolPath);
|
|
381
|
+
if (located && visit(located) === false)
|
|
382
|
+
return false;
|
|
383
|
+
if (!visitClassLike(statement, symbolPath, visit))
|
|
384
|
+
return false;
|
|
385
|
+
if (Node.isModuleDeclaration(statement)) {
|
|
386
|
+
return visitModuleStatements(statement, symbolPath, visit);
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
function visitChildStatements(container, prefix, visit) {
|
|
391
|
+
let complete = true;
|
|
392
|
+
container.forEachChild((statement) => {
|
|
393
|
+
if (!visitStatement(statement, prefix, visit)) {
|
|
394
|
+
complete = false;
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
return undefined;
|
|
398
|
+
});
|
|
399
|
+
return complete;
|
|
400
|
+
}
|
|
401
|
+
export function forEachLocatedSymbol(sourceFile, visit) {
|
|
402
|
+
visitChildStatements(sourceFile, undefined, visit);
|
|
84
403
|
}
|
|
85
404
|
export function collectSymbols(sourceFile) {
|
|
86
405
|
const symbols = [];
|
|
87
|
-
|
|
406
|
+
forEachLocatedSymbol(sourceFile, (symbol) => {
|
|
407
|
+
symbols.push(symbol);
|
|
408
|
+
});
|
|
88
409
|
return symbols;
|
|
89
410
|
}
|
|
411
|
+
export function containingSymbol(sourceFile, node) {
|
|
412
|
+
return collectSymbols(sourceFile)
|
|
413
|
+
.filter((symbol) => symbol.node.getStart() <= node.getStart() && symbol.node.getEnd() >= node.getEnd())
|
|
414
|
+
.sort((left, right) => {
|
|
415
|
+
const leftSize = left.node.getEnd() - left.node.getStart();
|
|
416
|
+
const rightSize = right.node.getEnd() - right.node.getStart();
|
|
417
|
+
return (leftSize - rightSize ||
|
|
418
|
+
left.line - right.line ||
|
|
419
|
+
left.symbolPath.localeCompare(right.symbolPath));
|
|
420
|
+
})[0];
|
|
421
|
+
}
|
|
90
422
|
function parseSelector(symbolPath) {
|
|
91
423
|
const match = /^(.*)@(\d+)$/.exec(symbolPath);
|
|
92
424
|
return match ? { path: match[1], line: Number(match[2]) } : { path: symbolPath };
|
|
93
425
|
}
|
|
94
|
-
export function
|
|
95
|
-
const selector = parseSelector(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const implementations = matches.filter((symbol) => {
|
|
99
|
-
const node = symbol.node;
|
|
100
|
-
return ((Node.isFunctionDeclaration(node) || Node.isMethodDeclaration(node)) &&
|
|
101
|
-
node.getBody() !== undefined);
|
|
102
|
-
});
|
|
103
|
-
if (implementations.length === 1)
|
|
104
|
-
matches = implementations;
|
|
426
|
+
export function findLocatedSymbol(sourceFile, requestedSelector, onVisit = () => undefined) {
|
|
427
|
+
const selector = parseSelector(requestedSelector);
|
|
428
|
+
if (selector.line === undefined) {
|
|
429
|
+
throw new Error("Located-symbol resolution requires an exact selector with a line number.");
|
|
105
430
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
431
|
+
let match;
|
|
432
|
+
forEachLocatedSymbol(sourceFile, (symbol) => {
|
|
433
|
+
onVisit(symbol);
|
|
434
|
+
if (symbol.symbolPath !== selector.path || symbol.line !== selector.line)
|
|
435
|
+
return;
|
|
436
|
+
match = symbol;
|
|
437
|
+
return false;
|
|
438
|
+
});
|
|
439
|
+
return match;
|
|
440
|
+
}
|
|
441
|
+
export function findLocatedDeclaration(sourceFile, requestedPath, onVisit = () => undefined) {
|
|
442
|
+
const selector = parseSelector(requestedPath);
|
|
443
|
+
let match;
|
|
444
|
+
forEachLocatedSymbol(sourceFile, (symbol) => {
|
|
445
|
+
onVisit(symbol);
|
|
446
|
+
if (symbol.symbolPath !== selector.path)
|
|
447
|
+
return;
|
|
448
|
+
if (selector.line !== undefined && symbol.line !== selector.line)
|
|
449
|
+
return;
|
|
450
|
+
match = symbol;
|
|
451
|
+
return false;
|
|
452
|
+
});
|
|
453
|
+
if (!match) {
|
|
109
454
|
throw new Error(`Symbol "${requestedPath}" was not found in ${sourceFile.getFilePath()}. Use ast_get_outline or ast_search_symbols to obtain an exact symbol path.`);
|
|
110
455
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
456
|
+
if (selector.line !== undefined)
|
|
457
|
+
return match;
|
|
458
|
+
const compilerDeclarations = match.node.getSymbol()?.compilerSymbol.declarations;
|
|
459
|
+
if (!compilerDeclarations)
|
|
460
|
+
return match;
|
|
461
|
+
let declarationsInFile = 0;
|
|
462
|
+
let implementationNode;
|
|
463
|
+
let implementationCount = 0;
|
|
464
|
+
const selectors = [];
|
|
465
|
+
for (const declaration of compilerDeclarations) {
|
|
466
|
+
if (declaration.getSourceFile() !== sourceFile.compilerNode)
|
|
467
|
+
continue;
|
|
468
|
+
declarationsInFile += 1;
|
|
469
|
+
if (selectors.length < 20) {
|
|
470
|
+
const line = sourceFile.compilerNode.getLineAndCharacterOfPosition(declaration.getStart()).line + 1;
|
|
471
|
+
selectors.push(`${match.symbolPath}@${line}`);
|
|
472
|
+
}
|
|
473
|
+
if ((ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration)) &&
|
|
474
|
+
declaration.body) {
|
|
475
|
+
implementationNode = declaration;
|
|
476
|
+
implementationCount += 1;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (declarationsInFile <= 1)
|
|
480
|
+
return match;
|
|
481
|
+
if (implementationCount === 1) {
|
|
482
|
+
let implementation;
|
|
483
|
+
forEachLocatedSymbol(sourceFile, (symbol) => {
|
|
484
|
+
onVisit(symbol);
|
|
485
|
+
if (symbol.node.compilerNode !== implementationNode)
|
|
486
|
+
return;
|
|
487
|
+
implementation = symbol;
|
|
488
|
+
return false;
|
|
489
|
+
});
|
|
490
|
+
if (implementation)
|
|
491
|
+
return implementation;
|
|
492
|
+
}
|
|
493
|
+
throw new Error(`Symbol "${requestedPath}" is ambiguous. Select one by line: ${selectors.join(", ")}${declarationsInFile > selectors.length ? ", ..." : ""}.`);
|
|
494
|
+
}
|
|
495
|
+
export function findDeclaration(sourceFile, requestedPath) {
|
|
496
|
+
return findLocatedDeclaration(sourceFile, requestedPath).node;
|
|
114
497
|
}
|
|
115
498
|
export function executableDeclaration(node) {
|
|
116
499
|
if (Node.isFunctionDeclaration(node) ||
|