ast-mcp-server 0.6.0 → 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.
Files changed (56) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +47 -15
  3. package/SECURITY.md +31 -0
  4. package/dist/cli.js +0 -0
  5. package/dist/index.js +129 -10
  6. package/dist/server.js +21 -2
  7. package/dist/services/agent-setup.js +36 -117
  8. package/dist/services/agent-targets.js +122 -0
  9. package/dist/services/context-builder.js +52 -24
  10. package/dist/services/diagnostics.js +10 -5
  11. package/dist/services/file-fingerprints.js +104 -0
  12. package/dist/services/file-snapshot.js +8 -1
  13. package/dist/services/impact.js +334 -0
  14. package/dist/services/operations.js +343 -254
  15. package/dist/services/outline.js +94 -21
  16. package/dist/services/pagination.js +10 -4
  17. package/dist/services/project-operation-scheduler.js +475 -0
  18. package/dist/services/project-status.js +134 -52
  19. package/dist/services/project-watcher.js +230 -0
  20. package/dist/services/project.js +646 -66
  21. package/dist/services/public-errors.js +255 -0
  22. package/dist/services/references.js +7 -1
  23. package/dist/services/relationships.js +1198 -0
  24. package/dist/services/request-context.js +42 -0
  25. package/dist/services/runtime-activity.js +65 -0
  26. package/dist/services/runtime-logger.js +50 -0
  27. package/dist/services/runtime-policy.js +47 -0
  28. package/dist/services/setup-wizard.js +4 -7
  29. package/dist/services/shutdown.js +93 -0
  30. package/dist/services/symbol-index-limits.js +5 -0
  31. package/dist/services/symbol-index-order.js +13 -0
  32. package/dist/services/symbol-index-policy.js +91 -0
  33. package/dist/services/symbol-index-sqlite.js +850 -0
  34. package/dist/services/symbol-index.js +447 -0
  35. package/dist/services/symbols.js +403 -79
  36. package/dist/services/test-candidates.js +209 -0
  37. package/dist/services/workspace.js +18 -7
  38. package/dist/tools/apply_operation.js +8 -4
  39. package/dist/tools/explore.js +8 -6
  40. package/dist/tools/find_references.js +12 -13
  41. package/dist/tools/get_diagnostics.js +10 -7
  42. package/dist/tools/get_file.js +23 -11
  43. package/dist/tools/get_impact.js +134 -0
  44. package/dist/tools/get_operation_preview.js +7 -4
  45. package/dist/tools/get_outline.js +8 -6
  46. package/dist/tools/get_project_status.js +77 -8
  47. package/dist/tools/get_symbol_source.js +8 -6
  48. package/dist/tools/list_files.js +11 -6
  49. package/dist/tools/rename_symbol.js +6 -4
  50. package/dist/tools/replace_symbol_body.js +6 -4
  51. package/dist/tools/result.js +27 -17
  52. package/dist/tools/scaffold_class.js +6 -4
  53. package/dist/tools/search_symbols.js +15 -14
  54. package/docs/support.md +80 -0
  55. package/package.json +16 -5
  56. package/skills/structural-code-editing/SKILL.md +37 -10
@@ -1,9 +1,19 @@
1
1
  import path from "node:path";
2
- import { Node, } from "ts-morph";
3
- import { buildFileOutline } from "./outline.js";
4
- export function sourceFileSymbols(sourceFile, projectRoot) {
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 matches = [];
41
- for (const sourceFile of project.getSourceFiles()) {
42
- const file = path.relative(projectRoot, sourceFile.getFilePath());
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
- const matchingSymbols = sourceFileSymbols(sourceFile, projectRoot).filter((symbol) => {
46
- return ((!kindSet || kindSet.has(symbol.kind)) &&
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.symbol_path.toLowerCase().includes(normalizedQuery) ||
49
- symbol.selector.toLowerCase().includes(normalizedQuery)));
115
+ symbol.symbolPath.toLowerCase().includes(normalizedQuery) ||
116
+ selector.toLowerCase().includes(normalizedQuery))) {
117
+ rankCounts[symbolMatchRank(options.query, symbol)] += 1;
118
+ }
50
119
  });
51
- if (matchingSymbols.length === 0)
52
- continue;
53
- for (const symbol of matchingSymbols) {
54
- matches.push(symbol);
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
- matches.sort((left, right) => {
58
- const rank = symbolMatchRank(options.query, {
59
- symbolPath: left.symbol_path,
60
- name: left.name,
61
- line: left.line,
62
- }) -
63
- symbolMatchRank(options.query, {
64
- symbolPath: right.symbol_path,
65
- name: right.name,
66
- line: right.line,
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
- if (rank !== 0)
69
- return rank;
70
- return (left.file.localeCompare(right.file) ||
71
- left.line - right.line ||
72
- left.symbol_path.localeCompare(right.symbol_path) ||
73
- left.kind.localeCompare(right.kind));
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
- return matches;
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 addLocated(node, symbolPath, output) {
327
+ function locatedSymbol(node, symbolPath) {
98
328
  const name = namedNode(node);
99
329
  if (!name)
100
- return;
101
- output.push({
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 collectClassLike(node, prefix, output) {
339
+ function visitClassLike(node, prefix, visit) {
110
340
  if (!Node.isClassDeclaration(node) && !Node.isInterfaceDeclaration(node))
111
- return;
112
- for (const member of node.getMembers()) {
341
+ return true;
342
+ let complete = true;
343
+ node.forEachChild((member) => {
113
344
  const name = namedNode(member);
114
- if (name)
115
- addLocated(member, `${prefix}.${name}`, output);
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 moduleStatements(module) {
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 && Node.isModuleBlock(body) ? body.getStatements() : [];
358
+ return !body || !Node.isModuleBlock(body) || visitChildStatements(body, prefix, visit);
123
359
  }
124
- function collectStatements(statements, prefix, output) {
125
- for (const statement of statements) {
126
- if (Node.isVariableStatement(statement)) {
127
- for (const declaration of statement.getDeclarations()) {
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
- addLocated(declaration, path, output);
366
+ const located = locatedSymbol(declaration, path);
367
+ if (located && visit(located) === false) {
368
+ complete = false;
369
+ return true;
370
+ }
130
371
  }
131
- continue;
132
- }
133
- const name = namedNode(statement);
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
- collectStatements(sourceFile.getStatements(), undefined, symbols);
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 findDeclaration(sourceFile, requestedPath) {
154
- const selector = parseSelector(requestedPath);
155
- let matches = collectSymbols(sourceFile).filter((symbol) => symbol.symbolPath === selector.path && (!selector.line || symbol.line === selector.line));
156
- if (matches.length > 1) {
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
- if (matches.length === 1)
166
- return matches[0].node;
167
- if (matches.length === 0) {
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
- throw new Error(`Symbol "${requestedPath}" is ambiguous. Select one by line: ${matches
171
- .map((symbol) => `${symbol.symbolPath}@${symbol.line}`)
172
- .join(", ")}.`);
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) ||