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.
Files changed (57) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +63 -14
  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 +25 -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 +262 -0
  10. package/dist/services/diagnostics.js +10 -5
  11. package/dist/services/file-fingerprints.js +104 -0
  12. package/dist/services/file-snapshot.js +103 -0
  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 +54 -0
  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 +430 -47
  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 +176 -0
  40. package/dist/tools/find_references.js +17 -55
  41. package/dist/tools/get_diagnostics.js +10 -7
  42. package/dist/tools/get_file.js +107 -0
  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 -63
  54. package/docs/support.md +80 -0
  55. package/package.json +18 -6
  56. package/skills/structural-code-editing/SKILL.md +42 -8
  57. package/benchmark/README.md +0 -129
@@ -1,4 +1,62 @@
1
- import { Node, } from "ts-morph";
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 addLocated(node, symbolPath, output) {
327
+ function locatedSymbol(node, symbolPath) {
39
328
  const name = namedNode(node);
40
329
  if (!name)
41
- return;
42
- output.push({
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 collectClassLike(node, prefix, output) {
339
+ function visitClassLike(node, prefix, visit) {
51
340
  if (!Node.isClassDeclaration(node) && !Node.isInterfaceDeclaration(node))
52
- return;
53
- for (const member of node.getMembers()) {
341
+ return true;
342
+ let complete = true;
343
+ node.forEachChild((member) => {
54
344
  const name = namedNode(member);
55
- if (name)
56
- addLocated(member, `${prefix}.${name}`, output);
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 moduleStatements(module) {
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 && Node.isModuleBlock(body) ? body.getStatements() : [];
358
+ return !body || !Node.isModuleBlock(body) || visitChildStatements(body, prefix, visit);
64
359
  }
65
- function collectStatements(statements, prefix, output) {
66
- for (const statement of statements) {
67
- if (Node.isVariableStatement(statement)) {
68
- 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)) {
69
365
  const path = prefix ? `${prefix}.${declaration.getName()}` : declaration.getName();
70
- addLocated(declaration, path, output);
366
+ const located = locatedSymbol(declaration, path);
367
+ if (located && visit(located) === false) {
368
+ complete = false;
369
+ return true;
370
+ }
71
371
  }
72
- continue;
73
- }
74
- const name = namedNode(statement);
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
- collectStatements(sourceFile.getStatements(), undefined, symbols);
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 findDeclaration(sourceFile, requestedPath) {
95
- const selector = parseSelector(requestedPath);
96
- let matches = collectSymbols(sourceFile).filter((symbol) => symbol.symbolPath === selector.path && (!selector.line || symbol.line === selector.line));
97
- if (matches.length > 1) {
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
- if (matches.length === 1)
107
- return matches[0].node;
108
- 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) {
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
- throw new Error(`Symbol "${requestedPath}" is ambiguous. Select one by line: ${matches
112
- .map((symbol) => `${symbol.symbolPath}@${symbol.line}`)
113
- .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;
114
497
  }
115
498
  export function executableDeclaration(node) {
116
499
  if (Node.isFunctionDeclaration(node) ||