ast-mcp-server 0.6.0 → 0.7.1
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 +38 -0
- package/README.md +47 -15
- package/SECURITY.md +31 -0
- package/dist/cli.js +0 -0
- package/dist/index.js +129 -10
- package/dist/server.js +21 -2
- package/dist/services/agent-setup.js +36 -117
- package/dist/services/agent-targets.js +122 -0
- package/dist/services/context-builder.js +52 -24
- package/dist/services/diagnostics.js +10 -5
- package/dist/services/file-fingerprints.js +104 -0
- package/dist/services/file-snapshot.js +8 -1
- 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 +7 -1
- 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 +403 -79
- 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 +8 -6
- package/dist/tools/find_references.js +12 -13
- package/dist/tools/get_diagnostics.js +10 -7
- package/dist/tools/get_file.js +23 -11
- 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 -14
- package/docs/support.md +80 -0
- package/package.json +16 -5
- package/skills/structural-code-editing/SKILL.md +37 -10
package/dist/services/symbols.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { Node, } from "ts-morph";
|
|
3
|
-
import { buildFileOutline } from "./outline.js";
|
|
4
|
-
|
|
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) {
|
|
5
15
|
const file = path.relative(projectRoot, sourceFile.getFilePath());
|
|
6
|
-
const signatures = new Map(buildFileOutline(sourceFile).symbols.map((symbol) => [
|
|
16
|
+
const signatures = new Map(buildFileOutline(sourceFile, requestContext).symbols.map((symbol) => [
|
|
7
17
|
`${symbol.symbolPath}@${symbol.startLine}`,
|
|
8
18
|
symbol.signature,
|
|
9
19
|
]));
|
|
@@ -17,6 +27,36 @@ export function sourceFileSymbols(sourceFile, projectRoot) {
|
|
|
17
27
|
signature: signatures.get(`${symbol.symbolPath}@${symbol.line}`) ?? symbol.node.getText(),
|
|
18
28
|
}));
|
|
19
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
|
+
}
|
|
20
60
|
export function symbolMatchRank(query, symbol) {
|
|
21
61
|
const normalizedQuery = query.toLowerCase();
|
|
22
62
|
const normalizedPath = symbol.symbolPath.toLowerCase();
|
|
@@ -33,46 +73,236 @@ export function symbolMatchRank(query, symbol) {
|
|
|
33
73
|
}
|
|
34
74
|
return 4;
|
|
35
75
|
}
|
|
36
|
-
export function searchProjectSymbols(project, projectRoot, options) {
|
|
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);
|
|
37
95
|
const normalizedQuery = options.query.toLowerCase();
|
|
38
96
|
const normalizedFileFilter = options.fileFilter?.toLowerCase();
|
|
39
97
|
const kindSet = options.kinds ? new Set(options.kinds) : undefined;
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
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();
|
|
43
108
|
if (normalizedFileFilter && !file.toLowerCase().includes(normalizedFileFilter))
|
|
44
109
|
continue;
|
|
45
|
-
|
|
46
|
-
|
|
110
|
+
forEachLocatedSymbol(sourceFile, (symbol) => {
|
|
111
|
+
requestContext.checkpoint();
|
|
112
|
+
const selector = `${symbol.symbolPath}@${symbol.line}`;
|
|
113
|
+
if ((!kindSet || kindSet.has(symbol.kind)) &&
|
|
47
114
|
(symbol.name.toLowerCase().includes(normalizedQuery) ||
|
|
48
|
-
symbol.
|
|
49
|
-
|
|
115
|
+
symbol.symbolPath.toLowerCase().includes(normalizedQuery) ||
|
|
116
|
+
selector.toLowerCase().includes(normalizedQuery))) {
|
|
117
|
+
rankCounts[symbolMatchRank(options.query, symbol)] += 1;
|
|
118
|
+
}
|
|
50
119
|
});
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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;
|
|
55
132
|
}
|
|
133
|
+
rankOffset = rankEnd;
|
|
56
134
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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,
|
|
67
165
|
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
};
|
|
74
181
|
});
|
|
75
|
-
|
|
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
|
+
};
|
|
76
306
|
}
|
|
77
307
|
function namedNode(node) {
|
|
78
308
|
if (Node.isConstructorDeclaration(node))
|
|
@@ -94,82 +324,176 @@ function namedNode(node) {
|
|
|
94
324
|
}
|
|
95
325
|
return undefined;
|
|
96
326
|
}
|
|
97
|
-
function
|
|
327
|
+
function locatedSymbol(node, symbolPath) {
|
|
98
328
|
const name = namedNode(node);
|
|
99
329
|
if (!name)
|
|
100
|
-
return;
|
|
101
|
-
|
|
330
|
+
return undefined;
|
|
331
|
+
return {
|
|
102
332
|
node,
|
|
103
333
|
symbolPath,
|
|
104
334
|
name,
|
|
105
335
|
kind: node.getKindName(),
|
|
106
336
|
line: node.getStartLineNumber(),
|
|
107
|
-
}
|
|
337
|
+
};
|
|
108
338
|
}
|
|
109
|
-
function
|
|
339
|
+
function visitClassLike(node, prefix, visit) {
|
|
110
340
|
if (!Node.isClassDeclaration(node) && !Node.isInterfaceDeclaration(node))
|
|
111
|
-
return;
|
|
112
|
-
|
|
341
|
+
return true;
|
|
342
|
+
let complete = true;
|
|
343
|
+
node.forEachChild((member) => {
|
|
113
344
|
const name = namedNode(member);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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;
|
|
117
353
|
}
|
|
118
|
-
function
|
|
354
|
+
function visitModuleStatements(module, prefix, visit) {
|
|
119
355
|
let body = module.getBody();
|
|
120
356
|
while (body && Node.isModuleDeclaration(body))
|
|
121
357
|
body = body.getBody();
|
|
122
|
-
return body
|
|
358
|
+
return !body || !Node.isModuleBlock(body) || visitChildStatements(body, prefix, visit);
|
|
123
359
|
}
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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)) {
|
|
128
365
|
const path = prefix ? `${prefix}.${declaration.getName()}` : declaration.getName();
|
|
129
|
-
|
|
366
|
+
const located = locatedSymbol(declaration, path);
|
|
367
|
+
if (located && visit(located) === false) {
|
|
368
|
+
complete = false;
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
130
371
|
}
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (!name)
|
|
135
|
-
continue;
|
|
136
|
-
const symbolPath = prefix ? `${prefix}.${name}` : name;
|
|
137
|
-
addLocated(statement, symbolPath, output);
|
|
138
|
-
collectClassLike(statement, symbolPath, output);
|
|
139
|
-
if (Node.isModuleDeclaration(statement)) {
|
|
140
|
-
collectStatements(moduleStatements(statement), symbolPath, output);
|
|
141
|
-
}
|
|
372
|
+
return undefined;
|
|
373
|
+
});
|
|
374
|
+
return complete;
|
|
142
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);
|
|
143
403
|
}
|
|
144
404
|
export function collectSymbols(sourceFile) {
|
|
145
405
|
const symbols = [];
|
|
146
|
-
|
|
406
|
+
forEachLocatedSymbol(sourceFile, (symbol) => {
|
|
407
|
+
symbols.push(symbol);
|
|
408
|
+
});
|
|
147
409
|
return symbols;
|
|
148
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
|
+
}
|
|
149
422
|
function parseSelector(symbolPath) {
|
|
150
423
|
const match = /^(.*)@(\d+)$/.exec(symbolPath);
|
|
151
424
|
return match ? { path: match[1], line: Number(match[2]) } : { path: symbolPath };
|
|
152
425
|
}
|
|
153
|
-
export function
|
|
154
|
-
const selector = parseSelector(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const implementations = matches.filter((symbol) => {
|
|
158
|
-
const node = symbol.node;
|
|
159
|
-
return ((Node.isFunctionDeclaration(node) || Node.isMethodDeclaration(node)) &&
|
|
160
|
-
node.getBody() !== undefined);
|
|
161
|
-
});
|
|
162
|
-
if (implementations.length === 1)
|
|
163
|
-
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.");
|
|
164
430
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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) {
|
|
168
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.`);
|
|
169
455
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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;
|
|
173
497
|
}
|
|
174
498
|
export function executableDeclaration(node) {
|
|
175
499
|
if (Node.isFunctionDeclaration(node) ||
|