clast-ai 0.1.2 → 0.2.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 (290) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +11 -9
  3. package/README.md +447 -401
  4. package/bench/ab/onair-concept.json +22 -0
  5. package/bench/ab/onair-deadcode.json +7 -0
  6. package/bench/ab/onair-hard.json +32 -0
  7. package/bench/ab/onair-impact.json +17 -0
  8. package/bench/ab/onair-refactors.json +18 -0
  9. package/bench/ab/onair-survey.json +17 -0
  10. package/bench/ab/onair-tasks.json +20 -0
  11. package/bench/ab/onair.json +52 -0
  12. package/bench/ab/vscode-concept.json +22 -0
  13. package/bench/ab/vscode-hard.json +32 -0
  14. package/bench/ab/vscode-impact.json +17 -0
  15. package/bench/ab/vscode-refactors.json +18 -0
  16. package/bench/ab/vscode-tasks.json +20 -0
  17. package/bench/ab/vscode.json +52 -0
  18. package/bench/ab.mjs +213 -0
  19. package/bench/bench.mjs +98 -0
  20. package/bench/resolution.mjs +75 -0
  21. package/dist/cli.d.ts +2 -0
  22. package/dist/cli.js +257 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/config/defaults.d.ts +12 -2
  25. package/dist/config/defaults.js +38 -31
  26. package/dist/config/defaults.js.map +1 -1
  27. package/dist/config/index.d.ts +1 -1
  28. package/dist/config/index.js +26 -14
  29. package/dist/config/index.js.map +1 -1
  30. package/dist/db/index.d.ts +73 -13
  31. package/dist/db/index.js +481 -39
  32. package/dist/db/index.js.map +1 -1
  33. package/dist/engine.d.ts +15 -0
  34. package/dist/engine.js +28 -0
  35. package/dist/engine.js.map +1 -0
  36. package/dist/indexer/imports.d.ts +6 -0
  37. package/dist/indexer/imports.js +254 -0
  38. package/dist/indexer/imports.js.map +1 -0
  39. package/dist/indexer/index.d.ts +63 -0
  40. package/dist/indexer/index.js +360 -0
  41. package/dist/indexer/index.js.map +1 -0
  42. package/dist/indexer/process.d.ts +22 -0
  43. package/dist/indexer/process.js +42 -0
  44. package/dist/indexer/process.js.map +1 -0
  45. package/dist/indexer/scan.d.ts +43 -0
  46. package/dist/indexer/scan.js +223 -0
  47. package/dist/indexer/scan.js.map +1 -0
  48. package/dist/indexer/worker.d.ts +1 -0
  49. package/dist/indexer/worker.js +23 -0
  50. package/dist/indexer/worker.js.map +1 -0
  51. package/dist/llm/comment-generator.d.ts +17 -19
  52. package/dist/llm/comment-generator.js +46 -80
  53. package/dist/llm/comment-generator.js.map +1 -1
  54. package/dist/llm/index.d.ts +0 -1
  55. package/dist/llm/index.js.map +1 -1
  56. package/dist/llm/prompts.d.ts +0 -1
  57. package/dist/llm/prompts.js +16 -16
  58. package/dist/parser/extractor.d.ts +21 -16
  59. package/dist/parser/extractor.js +700 -228
  60. package/dist/parser/extractor.js.map +1 -1
  61. package/dist/parser/index.d.ts +17 -21
  62. package/dist/parser/index.js +115 -171
  63. package/dist/parser/index.js.map +1 -1
  64. package/dist/parser/langs/c.d.ts +3 -0
  65. package/dist/parser/langs/c.js +879 -0
  66. package/dist/parser/langs/c.js.map +1 -0
  67. package/dist/parser/langs/csharp.d.ts +2 -0
  68. package/dist/parser/langs/csharp.js +548 -0
  69. package/dist/parser/langs/csharp.js.map +1 -0
  70. package/dist/parser/langs/dart.d.ts +2 -0
  71. package/dist/parser/langs/dart.js +893 -0
  72. package/dist/parser/langs/dart.js.map +1 -0
  73. package/dist/parser/langs/go.d.ts +2 -0
  74. package/dist/parser/langs/go.js +455 -0
  75. package/dist/parser/langs/go.js.map +1 -0
  76. package/dist/parser/langs/java.d.ts +2 -0
  77. package/dist/parser/langs/java.js +390 -0
  78. package/dist/parser/langs/java.js.map +1 -0
  79. package/dist/parser/langs/php.d.ts +2 -0
  80. package/dist/parser/langs/php.js +486 -0
  81. package/dist/parser/langs/php.js.map +1 -0
  82. package/dist/parser/langs/python.d.ts +2 -0
  83. package/dist/parser/langs/python.js +216 -0
  84. package/dist/parser/langs/python.js.map +1 -0
  85. package/dist/parser/langs/ruby.d.ts +2 -0
  86. package/dist/parser/langs/ruby.js +602 -0
  87. package/dist/parser/langs/ruby.js.map +1 -0
  88. package/dist/parser/langs/rust.d.ts +2 -0
  89. package/dist/parser/langs/rust.js +931 -0
  90. package/dist/parser/langs/rust.js.map +1 -0
  91. package/dist/parser/langs/swift.d.ts +2 -0
  92. package/dist/parser/langs/swift.js +616 -0
  93. package/dist/parser/langs/swift.js.map +1 -0
  94. package/dist/parser/langs/typescript.d.ts +4 -0
  95. package/dist/parser/langs/typescript.js +535 -0
  96. package/dist/parser/langs/typescript.js.map +1 -0
  97. package/dist/parser/registry.d.ts +16 -0
  98. package/dist/parser/registry.js +87 -0
  99. package/dist/parser/registry.js.map +1 -0
  100. package/dist/parser/returns.d.ts +9 -0
  101. package/dist/parser/returns.js +152 -0
  102. package/dist/parser/returns.js.map +1 -0
  103. package/dist/parser/spec.d.ts +165 -0
  104. package/dist/parser/spec.js +188 -0
  105. package/dist/parser/spec.js.map +1 -0
  106. package/dist/query/arity.d.ts +27 -0
  107. package/dist/query/arity.js +187 -0
  108. package/dist/query/arity.js.map +1 -0
  109. package/dist/query/callgraph.d.ts +113 -0
  110. package/dist/query/callgraph.js +332 -0
  111. package/dist/query/callgraph.js.map +1 -0
  112. package/dist/query/format.d.ts +36 -0
  113. package/dist/query/format.js +136 -0
  114. package/dist/query/format.js.map +1 -0
  115. package/dist/query/hierarchy.d.ts +44 -0
  116. package/dist/query/hierarchy.js +441 -0
  117. package/dist/query/hierarchy.js.map +1 -0
  118. package/dist/query/resolve.d.ts +265 -0
  119. package/dist/query/resolve.js +1957 -0
  120. package/dist/query/resolve.js.map +1 -0
  121. package/dist/query/search.d.ts +35 -0
  122. package/dist/query/search.js +379 -0
  123. package/dist/query/search.js.map +1 -0
  124. package/dist/query/words.d.ts +30 -0
  125. package/dist/query/words.js +201 -0
  126. package/dist/query/words.js.map +1 -0
  127. package/dist/server/index.d.ts +1 -3
  128. package/dist/server/index.js +313 -73
  129. package/dist/server/index.js.map +1 -1
  130. package/dist/tools/context.d.ts +35 -0
  131. package/dist/tools/context.js +79 -0
  132. package/dist/tools/context.js.map +1 -0
  133. package/dist/tools/explore.d.ts +51 -0
  134. package/dist/tools/explore.js +779 -0
  135. package/dist/tools/explore.js.map +1 -0
  136. package/dist/tools/impact.d.ts +21 -0
  137. package/dist/tools/impact.js +743 -0
  138. package/dist/tools/impact.js.map +1 -0
  139. package/dist/tools/search.d.ts +59 -0
  140. package/dist/tools/search.js +843 -0
  141. package/dist/tools/search.js.map +1 -0
  142. package/dist/tools/stats.d.ts +26 -0
  143. package/dist/tools/stats.js +604 -0
  144. package/dist/tools/stats.js.map +1 -0
  145. package/dist/types.d.ts +95 -40
  146. package/dist/types.js +9 -1
  147. package/dist/types.js.map +1 -1
  148. package/dist/version.d.ts +2 -0
  149. package/dist/version.js +3 -0
  150. package/dist/version.js.map +1 -0
  151. package/dist/watcher/index.d.ts +18 -16
  152. package/dist/watcher/index.js +94 -77
  153. package/dist/watcher/index.js.map +1 -1
  154. package/package.json +21 -24
  155. package/plugin/server.mjs +50 -0
  156. package/skills/audit/SKILL.md +37 -0
  157. package/skills/deadcode/SKILL.md +33 -0
  158. package/skills/explore/SKILL.md +54 -0
  159. package/skills/impact/SKILL.md +24 -0
  160. package/skills/status/SKILL.md +11 -0
  161. package/src/cli.ts +256 -0
  162. package/src/config/defaults.ts +50 -32
  163. package/src/config/index.ts +23 -14
  164. package/src/db/index.ts +540 -43
  165. package/src/engine.ts +30 -0
  166. package/src/indexer/imports.ts +241 -0
  167. package/src/indexer/index.ts +369 -0
  168. package/src/indexer/process.ts +63 -0
  169. package/src/indexer/scan.ts +226 -0
  170. package/src/indexer/worker.ts +24 -0
  171. package/src/llm/comment-generator.ts +52 -103
  172. package/src/llm/index.ts +80 -80
  173. package/src/llm/prompts.ts +95 -95
  174. package/src/parser/extractor.ts +643 -256
  175. package/src/parser/index.ts +105 -205
  176. package/src/parser/langs/c.ts +860 -0
  177. package/src/parser/langs/csharp.ts +519 -0
  178. package/src/parser/langs/dart.ts +820 -0
  179. package/src/parser/langs/go.ts +437 -0
  180. package/src/parser/langs/java.ts +361 -0
  181. package/src/parser/langs/php.ts +450 -0
  182. package/src/parser/langs/python.ts +206 -0
  183. package/src/parser/langs/ruby.ts +554 -0
  184. package/src/parser/langs/rust.ts +904 -0
  185. package/src/parser/langs/swift.ts +590 -0
  186. package/src/parser/langs/typescript.ts +499 -0
  187. package/src/parser/registry.ts +88 -0
  188. package/src/parser/returns.ts +137 -0
  189. package/src/parser/spec.ts +309 -0
  190. package/src/query/arity.ts +173 -0
  191. package/src/query/callgraph.ts +473 -0
  192. package/src/query/format.ts +136 -0
  193. package/src/query/hierarchy.ts +447 -0
  194. package/src/query/resolve.ts +1972 -0
  195. package/src/query/search.ts +430 -0
  196. package/src/query/words.ts +183 -0
  197. package/src/server/index.ts +390 -79
  198. package/src/tools/context.ts +82 -0
  199. package/src/tools/explore.ts +860 -0
  200. package/src/tools/impact.ts +748 -0
  201. package/src/tools/search.ts +886 -0
  202. package/src/tools/stats.ts +633 -0
  203. package/src/types.ts +113 -45
  204. package/src/version.ts +2 -0
  205. package/src/watcher/index.ts +82 -88
  206. package/tsconfig.json +7 -4
  207. package/.mcp.json +0 -8
  208. package/commands/clast-status.md +0 -9
  209. package/dist/config/defaults.d.ts.map +0 -1
  210. package/dist/config/index.d.ts.map +0 -1
  211. package/dist/db/index.d.ts.map +0 -1
  212. package/dist/db/queries.d.ts +0 -44
  213. package/dist/db/queries.d.ts.map +0 -1
  214. package/dist/db/queries.js +0 -251
  215. package/dist/db/queries.js.map +0 -1
  216. package/dist/db/schema.d.ts +0 -3
  217. package/dist/db/schema.d.ts.map +0 -1
  218. package/dist/db/schema.js +0 -94
  219. package/dist/db/schema.js.map +0 -1
  220. package/dist/db/types.d.ts +0 -44
  221. package/dist/db/types.d.ts.map +0 -1
  222. package/dist/db/types.js +0 -2
  223. package/dist/db/types.js.map +0 -1
  224. package/dist/llm/comment-generator.d.ts.map +0 -1
  225. package/dist/llm/index.d.ts.map +0 -1
  226. package/dist/llm/prompts.d.ts.map +0 -1
  227. package/dist/parser/call-resolver.d.ts +0 -15
  228. package/dist/parser/call-resolver.d.ts.map +0 -1
  229. package/dist/parser/call-resolver.js +0 -16
  230. package/dist/parser/call-resolver.js.map +0 -1
  231. package/dist/parser/extractor.d.ts.map +0 -1
  232. package/dist/parser/index.d.ts.map +0 -1
  233. package/dist/parser/languages.d.ts +0 -11
  234. package/dist/parser/languages.d.ts.map +0 -1
  235. package/dist/parser/languages.js +0 -74
  236. package/dist/parser/languages.js.map +0 -1
  237. package/dist/server/index.d.ts.map +0 -1
  238. package/dist/server/register-tools.d.ts +0 -7
  239. package/dist/server/register-tools.d.ts.map +0 -1
  240. package/dist/server/register-tools.js +0 -17
  241. package/dist/server/register-tools.js.map +0 -1
  242. package/dist/server/tools/clast-call-graph.d.ts +0 -4
  243. package/dist/server/tools/clast-call-graph.d.ts.map +0 -1
  244. package/dist/server/tools/clast-call-graph.js +0 -70
  245. package/dist/server/tools/clast-call-graph.js.map +0 -1
  246. package/dist/server/tools/clast-comment.d.ts +0 -5
  247. package/dist/server/tools/clast-comment.d.ts.map +0 -1
  248. package/dist/server/tools/clast-comment.js +0 -64
  249. package/dist/server/tools/clast-comment.js.map +0 -1
  250. package/dist/server/tools/clast-file-summary.d.ts +0 -4
  251. package/dist/server/tools/clast-file-summary.d.ts.map +0 -1
  252. package/dist/server/tools/clast-file-summary.js +0 -42
  253. package/dist/server/tools/clast-file-summary.js.map +0 -1
  254. package/dist/server/tools/clast-get-context.d.ts +0 -4
  255. package/dist/server/tools/clast-get-context.d.ts.map +0 -1
  256. package/dist/server/tools/clast-get-context.js +0 -80
  257. package/dist/server/tools/clast-get-context.js.map +0 -1
  258. package/dist/server/tools/clast-reindex.d.ts +0 -5
  259. package/dist/server/tools/clast-reindex.d.ts.map +0 -1
  260. package/dist/server/tools/clast-reindex.js +0 -36
  261. package/dist/server/tools/clast-reindex.js.map +0 -1
  262. package/dist/server/tools/clast-search.d.ts +0 -4
  263. package/dist/server/tools/clast-search.d.ts.map +0 -1
  264. package/dist/server/tools/clast-search.js +0 -29
  265. package/dist/server/tools/clast-search.js.map +0 -1
  266. package/dist/server/tools/clast-status.d.ts +0 -5
  267. package/dist/server/tools/clast-status.d.ts.map +0 -1
  268. package/dist/server/tools/clast-status.js +0 -23
  269. package/dist/server/tools/clast-status.js.map +0 -1
  270. package/dist/types.d.ts.map +0 -1
  271. package/dist/watcher/gitignore.d.ts +0 -9
  272. package/dist/watcher/gitignore.d.ts.map +0 -1
  273. package/dist/watcher/gitignore.js +0 -38
  274. package/dist/watcher/gitignore.js.map +0 -1
  275. package/dist/watcher/index.d.ts.map +0 -1
  276. package/skills/clast-explore/SKILL.md +0 -41
  277. package/src/db/queries.ts +0 -332
  278. package/src/db/schema.ts +0 -106
  279. package/src/db/types.ts +0 -47
  280. package/src/parser/call-resolver.ts +0 -17
  281. package/src/parser/languages.ts +0 -86
  282. package/src/server/register-tools.ts +0 -30
  283. package/src/server/tools/clast-call-graph.ts +0 -82
  284. package/src/server/tools/clast-comment.ts +0 -76
  285. package/src/server/tools/clast-file-summary.ts +0 -59
  286. package/src/server/tools/clast-get-context.ts +0 -97
  287. package/src/server/tools/clast-reindex.ts +0 -52
  288. package/src/server/tools/clast-search.ts +0 -38
  289. package/src/server/tools/clast-status.ts +0 -37
  290. package/src/watcher/gitignore.ts +0 -42
@@ -1,303 +1,690 @@
1
- import type TreeSitter from 'web-tree-sitter';
2
- import type { AstNode, AstNodeType } from '../types.js';
3
-
4
- // Node types that represent extractable symbols per language
5
- const FUNCTION_TYPES = new Set([
6
- 'function_declaration',
7
- 'function_definition',
8
- 'method_definition',
9
- 'method_declaration',
10
- 'arrow_function',
11
- 'function_item', // Rust
12
- 'func_declaration', // Go
13
- 'function_expression',
14
- ]);
15
-
16
- const CLASS_TYPES = new Set([
17
- 'class_declaration',
18
- 'class_definition',
19
- 'struct_item', // Rust
20
- 'struct_specifier', // C/C++
21
- 'impl_item', // Rust
22
- 'type_declaration', // Go
23
- ]);
24
-
25
- const INTERFACE_TYPES = new Set([
26
- 'interface_declaration',
27
- 'trait_item', // Rust
28
- ]);
29
-
30
- const IMPORT_TYPES = new Set([
31
- 'import_statement',
32
- 'import_declaration',
33
- 'use_declaration', // Rust
34
- 'preproc_include', // C/C++ (#include)
35
- ]);
36
-
37
- const EXPORT_TYPES = new Set([
38
- 'export_statement',
39
- 'export_declaration',
40
- ]);
41
-
42
- const ENUM_TYPES = new Set([
43
- 'enum_declaration',
44
- 'enum_item', // Rust
45
- 'enum_specifier', // C/C++
46
- ]);
47
-
48
- const CALL_TYPES = new Set([
49
- 'call_expression',
50
- 'new_expression',
51
- 'method_invocation', // Java
52
- 'invocation_expression', // C#
53
- ]);
54
-
55
- function classifyNodeType(treeSitterType: string): AstNodeType | null {
56
- if (FUNCTION_TYPES.has(treeSitterType)) return 'function';
57
- if (CLASS_TYPES.has(treeSitterType)) return 'class';
58
- if (INTERFACE_TYPES.has(treeSitterType)) return 'interface';
59
- if (IMPORT_TYPES.has(treeSitterType)) return 'import';
60
- if (EXPORT_TYPES.has(treeSitterType)) return 'export';
61
- if (ENUM_TYPES.has(treeSitterType)) return 'enum';
62
- return null;
1
+ import type { Language, Node, Tree } from 'web-tree-sitter';
2
+ import { CONTAINER_KINDS, type ExCall, type ExHeritage, type ExImport, type ExSymbol, type Extraction, type SymbolKind } from '../types.js';
3
+ import { genericArity, typeName, type Ctx, type DefOpts, type LangSpec, type Visitor } from './spec.js';
4
+ import { returnTypeOf } from './returns.js';
5
+
6
+ const MAX_SIGNATURE = 240;
7
+ const MAX_DOC = 600;
8
+ const MAX_OCC_LINES = 40;
9
+ const MAX_NAME = 120;
10
+
11
+ /** A language spec bound to a loaded grammar (node types resolved to ids). */
12
+ export interface CompiledSpec {
13
+ spec: LangSpec;
14
+ language: Language;
15
+ visitors: Array<Visitor | undefined>;
16
+ isIdent: Uint8Array;
17
+ isComment: Uint8Array;
18
+ /** Node types skipped between a doc comment and its item (attributes). */
19
+ isDocSkip: Uint8Array;
20
+ /** Visitor / identifier node types that do not exist in the grammar. */
21
+ unknownTypes: string[];
63
22
  }
64
23
 
65
- function extractName(node: TreeSitter.SyntaxNode): string {
66
- const nameNode =
67
- node.childForFieldName('name') ??
68
- node.childForFieldName('declarator')?.childForFieldName('name') ??
69
- node.childForFieldName('declarator');
70
-
71
- if (nameNode) {
72
- return nameNode.text;
24
+ export function compileSpec(spec: LangSpec, language: Language): CompiledSpec {
25
+ const count = language.nodeTypeCount;
26
+ const visitors: Array<Visitor | undefined> = new Array(count);
27
+ const isIdent = new Uint8Array(count);
28
+ const isComment = new Uint8Array(count);
29
+
30
+ const idsByName = new Map<string, number[]>();
31
+ for (let id = 0; id < count; id++) {
32
+ if (!language.nodeTypeIsNamed(id)) continue;
33
+ const name = language.nodeTypeForId(id);
34
+ if (!name) continue;
35
+ let list = idsByName.get(name);
36
+ if (!list) idsByName.set(name, (list = []));
37
+ list.push(id);
73
38
  }
74
39
 
75
- // Arrow functions assigned to variables
76
- if (node.type === 'arrow_function' && node.parent) {
77
- const parentName = node.parent.childForFieldName('name');
78
- if (parentName) return parentName.text;
40
+ const unknownTypes: string[] = [];
41
+ for (const [type, visitor] of Object.entries(spec.visit)) {
42
+ const ids = idsByName.get(type);
43
+ if (!ids) { unknownTypes.push(type); continue; }
44
+ for (const id of ids) visitors[id] = visitor;
79
45
  }
80
-
81
- // Exports
82
- if (node.type === 'export_statement') {
83
- const decl = node.childForFieldName('declaration');
84
- if (decl) return extractName(decl);
85
- return '<export>';
46
+ for (const type of spec.identifiers) {
47
+ const ids = idsByName.get(type);
48
+ if (!ids) { unknownTypes.push(type); continue; }
49
+ for (const id of ids) isIdent[id] = 1;
86
50
  }
87
-
88
- // Imports
89
- if (IMPORT_TYPES.has(node.type)) {
90
- const source = node.childForFieldName('source') ?? node.childForFieldName('path');
91
- if (source) return source.text.replace(/['"]/g, '');
92
- return node.text.slice(0, 80);
51
+ const commentTypes = spec.comments ?? [...idsByName.keys()].filter(n => n.includes('comment'));
52
+ for (const type of commentTypes) {
53
+ for (const id of idsByName.get(type) ?? []) isComment[id] = 1;
93
54
  }
94
55
 
95
- return '<anonymous>';
96
- }
97
-
98
- function extractSignature(node: TreeSitter.SyntaxNode, maxLength: number = 200): string {
99
- const text = node.text;
100
- const braceIdx = text.indexOf('{');
101
- const parenCloseIdx = text.indexOf(')');
102
-
103
- let end = text.length;
104
- if (braceIdx > 0) {
105
- end = braceIdx;
106
- } else if (parenCloseIdx > 0) {
107
- end = parenCloseIdx + 1;
56
+ const isDocSkip = new Uint8Array(count);
57
+ for (const type of spec.docSkip ?? []) {
58
+ const ids = idsByName.get(type);
59
+ if (!ids) { unknownTypes.push(type); continue; }
60
+ for (const id of ids) isDocSkip[id] = 1;
108
61
  }
109
62
 
110
- const sig = text.slice(0, end).trim();
111
- return sig.length > maxLength ? sig.slice(0, maxLength) + '...' : sig;
63
+ return { spec, language, visitors, isIdent, isComment, isDocSkip, unknownTypes };
112
64
  }
113
65
 
114
- function extractPrecedingComment(
115
- node: TreeSitter.SyntaxNode,
116
- source: string
117
- ): string | null {
118
- const lines = source.split('\n');
119
- const nodeLine = node.startPosition.row;
120
-
121
- const commentLines: string[] = [];
122
- let checkLine = nodeLine - 1;
123
-
124
- while (checkLine >= 0) {
125
- const line = lines[checkLine]?.trim() ?? '';
126
-
127
- if (
128
- line.startsWith('//') ||
129
- line.startsWith('#') ||
130
- line.startsWith('*') ||
131
- line.startsWith('/*') ||
132
- line.startsWith('///') ||
133
- line.startsWith('/**')
134
- ) {
135
- commentLines.unshift(line);
136
- checkLine--;
137
- } else if (line === '' && commentLines.length === 0) {
138
- checkLine--;
139
- } else {
140
- break;
141
- }
142
- }
66
+ interface Scope {
67
+ /** Symbol index, or -1 for naming-only scopes. */
68
+ sym: number;
69
+ name: string;
70
+ kind: SymbolKind | null;
71
+ /** Whether calls inside are attributed to this symbol. */
72
+ callScope: boolean;
73
+ }
143
74
 
144
- if (commentLines.length === 0) return null;
75
+ export function cleanComment(raw: string): string {
76
+ let s = raw.trim();
77
+ // Block comments: /** ... */, /* ... */, """ ... """, =begin ... =end
78
+ s = s.replace(/^\/\*+!?/, '').replace(/\*+\/$/, '');
79
+ const lines = s.split(/\r?\n/).map(l =>
80
+ l
81
+ .replace(/^\s*(\/\/[\/!]?|#+!?|--+|\*+(?!\/)|;+)\s?/, '')
82
+ .replace(/\s+$/, '')
83
+ );
84
+ while (lines.length && !lines[0]!.trim()) lines.shift();
85
+ while (lines.length && !lines[lines.length - 1]!.trim()) lines.pop();
86
+ return lines.join('\n').trim();
87
+ }
145
88
 
146
- const cleaned = commentLines
147
- .map(l => l.replace(/^\/\/\/?\s?|^\/\*\*?\s?|^\*\/?\s?|^#\s?/g, '').trim())
148
- .filter(l => l.length > 0)
149
- .join('\n');
89
+ const VALUE_WRAPPERS = new Set([
90
+ 'await_expression', 'try_expression', 'parenthesized_expression', 'non_null_expression', 'as_expression',
91
+ 'expression_list', 'equals_value_clause', 'unary_expression', 'reference_expression', 'type_cast_expression',
92
+ ]);
93
+ const BINDING_NODES = new Set([
94
+ 'variable_declarator', 'assignment_expression', 'assignment', 'short_var_declaration', 'let_declaration',
95
+ 'init_declarator', 'var_spec', 'local_variable_declaration', 'const_spec', 'augmented_assignment',
96
+ 'property_declaration', 'initialized_variable_definition', // Swift, Dart
97
+ ]);
150
98
 
151
- return cleaned || null;
99
+ /**
100
+ * If `call` is the whole right-hand side of a simple variable binding
101
+ * (`x = f()`, `let x = f()?`, `x, err := f()`, `auto x = f()`), the name of
102
+ * that variable; otherwise null. Works across grammars by looking at the
103
+ * usual field names.
104
+ */
105
+ function boundVariable(call: Node): string | null {
106
+ let node: Node = call;
107
+ let p = call.parent;
108
+ for (let i = 0; p && i < 3 && VALUE_WRAPPERS.has(p.type); i++) {
109
+ node = p;
110
+ p = p.parent;
111
+ }
112
+ if (!p || !BINDING_NODES.has(p.type)) return null;
113
+ const target =
114
+ p.childForFieldName('name') ?? p.childForFieldName('left') ?? p.childForFieldName('pattern') ??
115
+ p.childForFieldName('declarator');
116
+ if (!target || target.endIndex > node.startIndex) return null;
117
+ // Go: `x, err := f()` - the first name receives the first result.
118
+ const first = target.type === 'expression_list' ? target.namedChild(0) : target;
119
+ if (!first || first.endIndex - first.startIndex > 60) return null;
120
+ const text = first.text.replace(/^[$@*&]+/, '').replace(/^mut\s+/, '');
121
+ return /^[A-Za-z_]\w*$/.test(text) ? text : null;
152
122
  }
153
123
 
154
- function extractPythonDocstring(node: TreeSitter.SyntaxNode): string | null {
155
- const body = node.childForFieldName('body');
156
- if (!body) return null;
124
+ const ARGUMENT_LISTS = new Set(['argument_list', 'arguments', 'value_arguments']);
157
125
 
158
- const firstChild = body.firstChild;
159
- if (!firstChild) return null;
126
+ /** Languages where a bare name inside a method can be a field of the enclosing class. */
127
+ const IMPLICIT_THIS_SPECS = new Set(['java', 'csharp', 'cpp', 'c', 'swift', 'dart', 'kotlin']);
160
128
 
161
- if (
162
- firstChild.type === 'expression_statement' &&
163
- firstChild.firstChild?.type === 'string'
164
- ) {
165
- const text = firstChild.firstChild.text;
166
- return text.replace(/^["']{3}|["']{3}$/g, '').trim();
129
+ /** Number of arguments of a call node, or -1 when it cannot be counted (spread, no argument list). */
130
+ function argCount(node: Node): number {
131
+ // Flat grammars (Dart) hand in synthetic call spans that are not real nodes.
132
+ if (typeof node.childForFieldName !== 'function' || !Array.isArray(node.namedChildren)) return -1;
133
+ let args = node.childForFieldName('arguments');
134
+ if (!args) {
135
+ for (const c of node.namedChildren) {
136
+ if (c && ARGUMENT_LISTS.has(c.type)) { args = c; break; }
137
+ }
138
+ }
139
+ if (!args || !ARGUMENT_LISTS.has(args.type)) return -1;
140
+ let n = 0;
141
+ for (const c of args.namedChildren) {
142
+ if (!c || c.type.includes('comment')) continue;
143
+ // f(*args), f(...xs), f(xs...): the count is not known statically.
144
+ if (/splat|spread|variadic/.test(c.type)) return -1;
145
+ n++;
167
146
  }
147
+ return Math.min(n, 99);
148
+ }
168
149
 
169
- return null;
150
+ function squash(s: string, max: number): string {
151
+ const t = s.replace(/\s+/g, ' ').trim();
152
+ return t.length > max ? t.slice(0, max - 3) + '...' : t;
170
153
  }
171
154
 
172
- function extractCallName(node: TreeSitter.SyntaxNode): string | null {
173
- const fn = node.childForFieldName('function') ?? node.childForFieldName('method');
174
- if (!fn) {
175
- const firstChild = node.firstChild;
176
- if (firstChild) {
177
- if (firstChild.type === 'member_expression' || firstChild.type === 'field_expression') {
178
- const prop = firstChild.childForFieldName('property') ?? firstChild.childForFieldName('field');
179
- return prop?.text ?? firstChild.text;
180
- }
181
- return firstChild.text;
182
- }
183
- return null;
155
+ /**
156
+ * Walk a parsed tree and extract symbols, calls, imports, heritage and
157
+ * identifier occurrences according to the language spec.
158
+ */
159
+ export function extract(tree: Tree, source: string, compiled: CompiledSpec): Extraction {
160
+ const { visitors, isIdent, isComment, isDocSkip } = compiled;
161
+
162
+ const symbols: ExSymbol[] = [];
163
+ const imports: ExImport[] = [];
164
+ const heritage: ExHeritage[] = [];
165
+ const callMap = new Map<string, ExCall>();
166
+ const occ = new Map<string, number[]>();
167
+ const scopes: Scope[] = [];
168
+ const containerDefs = new Map<string, number>();
169
+ /** Outer calls of a chain waiting for the call that produces their receiver, by start offset. */
170
+ const chainOuters = new Map<number, Array<{ call: ExCall; end: number }>>();
171
+ const viaRef = new Map<ExCall, ExCall>();
172
+ /** Variables initialised from a call: owner symbol index -> variable name -> that call. */
173
+ const callHints = new Map<number, Map<string, ExCall>>();
174
+ let defaultExport: string | null = null;
175
+ /** Declared types: owner symbol index (-1 = file) -> variable/field name -> type name. */
176
+ const hints = new Map<number, Map<string, string>>();
177
+
178
+ function addOcc(name: string, line: number): void {
179
+ const lines = occ.get(name);
180
+ if (!lines) occ.set(name, [line]);
181
+ else if (lines.length < MAX_OCC_LINES && lines[lines.length - 1] !== line) lines.push(line);
184
182
  }
185
183
 
186
- if (fn.type === 'member_expression' || fn.type === 'field_expression') {
187
- const prop = fn.childForFieldName('property') ?? fn.childForFieldName('field');
188
- return prop?.text ?? fn.text;
184
+ function currentCaller(): number {
185
+ for (let i = scopes.length - 1; i >= 0; i--) {
186
+ const s = scopes[i]!;
187
+ if (s.sym >= 0 && s.callScope) return s.sym;
188
+ }
189
+ return -1;
189
190
  }
190
191
 
191
- return fn.text;
192
- }
193
-
194
- export interface ExtractionResult {
195
- nodes: AstNode[];
196
- edges: Array<{ callerNodeIndex: number; calleeName: string; line: number }>;
197
- comments: Array<{ nodeIndex: number; text: string; source: 'original' }>;
198
- }
199
-
200
- export function extractFromTree(
201
- tree: TreeSitter.Tree,
202
- filePath: string,
203
- language: string,
204
- source: string,
205
- maxBodySize: number = 2000
206
- ): ExtractionResult {
207
- const nodes: AstNode[] = [];
208
- const edges: Array<{ callerNodeIndex: number; calleeName: string; line: number }> = [];
209
- const comments: Array<{ nodeIndex: number; text: string; source: 'original' }> = [];
210
-
211
- const nodeIdToIndex = new Map<number, number>();
212
-
213
- function findEnclosingFunctionIndex(node: TreeSitter.SyntaxNode): number | null {
214
- let current = node.parent;
215
- while (current) {
216
- const type = classifyNodeType(current.type);
217
- if (type === 'function') {
218
- return nodeIdToIndex.get(current.id) ?? null;
192
+ function leadingDoc(range: Node): { doc: string; line: number; returns: string | null } {
193
+ const parts: string[] = [];
194
+ let line = 0;
195
+ let nextRow = range.startPosition.row;
196
+ let prev = range.previousSibling;
197
+ if (!prev) {
198
+ // First node of a wrapper (e.g. Ruby body_statement): the comment sits before the wrapper.
199
+ const p = range.parent;
200
+ if (p && p.startIndex === range.startIndex) prev = p.previousSibling;
201
+ }
202
+ while (prev) {
203
+ if (isDocSkip[prev.typeId]) {
204
+ // Attributes / annotations between the doc comment and the item.
205
+ nextRow = prev.startPosition.row;
206
+ prev = prev.previousSibling;
207
+ continue;
219
208
  }
220
- current = current.parent;
209
+ if (!isComment[prev.typeId]) break;
210
+ if (prev.endPosition.row < nextRow - 1) break;
211
+ // A comment trailing a statement on the same line documents that statement.
212
+ // (Preprocessor nodes end at column 0 of the next line: not trailing.)
213
+ const before = prev.previousSibling;
214
+ if (
215
+ before && !isComment[before.typeId] &&
216
+ before.endPosition.row === prev.startPosition.row && before.endPosition.column > 0
217
+ ) break;
218
+ parts.unshift(prev.text);
219
+ nextRow = prev.startPosition.row;
220
+ line = nextRow + 1;
221
+ if (parts.length >= 200) break;
222
+ prev = prev.previousSibling;
221
223
  }
222
- return null;
224
+ if (parts.length === 0) return { doc: '', line: 0, returns: null };
225
+ let doc = parts.map(cleanComment).join('\n').trim();
226
+ // Documented return type (PHPDoc / JSDoc): taken before the text is truncated.
227
+ const returns = /@returns?\s+\{?([^\s}|]+)/.exec(doc)?.[1] ?? null;
228
+ if (doc.length > MAX_DOC) doc = doc.slice(0, MAX_DOC - 3) + '...';
229
+ return { doc, line, returns };
223
230
  }
224
231
 
225
- function walk(node: TreeSitter.SyntaxNode, parentIndex: number | null): void {
226
- const nodeType = classifyNodeType(node.type);
227
- let currentIndex = parentIndex;
232
+ const ctx: Ctx = {
233
+ src: source,
234
+
235
+ def(node: Node, o: DefOpts): number {
236
+ let name = o.name;
237
+ if (!name) name = '<anonymous>';
238
+ if (name.length > MAX_NAME) name = name.slice(0, MAX_NAME);
239
+ const range = o.range ?? node;
240
+
241
+ const prefix: string[] = [];
242
+ let parent = -1;
243
+ let depth = scopes.length;
244
+ if (o.inContainer) {
245
+ // Attach to the nearest enclosing class-like scope instead of the method we are in.
246
+ depth = 0;
247
+ for (let i = scopes.length - 1; i >= 0; i--) {
248
+ const k = scopes[i]!.kind;
249
+ if (k !== null && CONTAINER_KINDS.has(k)) { depth = i + 1; break; }
250
+ }
251
+ }
252
+ for (let i = 0; i < depth; i++) {
253
+ prefix.push(scopes[i]!.name);
254
+ }
255
+ for (let i = depth - 1; i >= 0; i--) {
256
+ if (scopes[i]!.sym >= 0) { parent = scopes[i]!.sym; break; }
257
+ }
258
+ if (o.qualifier) prefix.push(...o.qualifier.split(/::|\./).filter(Boolean));
259
+ const qname = prefix.length ? prefix.join('.') + '.' + name : name;
260
+
261
+ let signature: string;
262
+ if (o.sig !== undefined) {
263
+ signature = squash(o.sig, MAX_SIGNATURE).replace(/\s*\{$/, '');
264
+ } else {
265
+ const body = o.body === undefined ? node.childForFieldName('body') : o.body;
266
+ const start = range.startIndex;
267
+ let end: number;
268
+ if (body && body.startIndex > start) {
269
+ end = body.startIndex;
270
+ } else {
271
+ end = range.endIndex;
272
+ // Without a body: short declarations in full, long ones (object
273
+ // literals, tables) only up to the end of their first line.
274
+ if (end - start > 200) {
275
+ const nl = source.indexOf('\n', start);
276
+ end = nl !== -1 && nl < end ? nl : Math.min(end, start + 200);
277
+ }
278
+ }
279
+ signature = squash(source.slice(start, end), MAX_SIGNATURE).replace(/\s*[{=]$/, '');
280
+ }
281
+
282
+ let doc = '';
283
+ let docLine = 0;
284
+ if (o.doc !== undefined && o.doc !== null) {
285
+ doc = o.doc.length > MAX_DOC ? o.doc.slice(0, MAX_DOC - 3) + '...' : o.doc;
286
+ }
287
+ const lead = leadingDoc(range);
288
+ if (lead.line) {
289
+ docLine = lead.line;
290
+ if (!doc) doc = lead.doc;
291
+ }
228
292
 
229
- if (nodeType && nodeType !== 'import' && nodeType !== 'export') {
230
- const name = extractName(node);
231
- const signature = extractSignature(node);
232
- const bodyText = node.text.length > maxBodySize
233
- ? node.text.slice(0, maxBodySize) + '...'
234
- : node.text;
293
+ if (o.inContainer) {
294
+ // Instance attributes are assigned many times; keep the first.
295
+ const key = o.kind + ' ' + qname;
296
+ const seen = containerDefs.get(key);
297
+ if (seen !== undefined) return seen;
298
+ containerDefs.set(key, symbols.length);
299
+ }
235
300
 
236
- const astNode: AstNode = {
237
- filePath,
238
- name,
239
- type: nodeType === 'function' && parentIndex !== null ? 'method' : nodeType,
240
- signature,
241
- startLine: node.startPosition.row + 1,
242
- endLine: node.endPosition.row + 1,
243
- startCol: node.startPosition.column,
244
- endCol: node.endPosition.column,
245
- parentId: null,
246
- language,
247
- bodyText,
248
- };
249
-
250
- currentIndex = nodes.length;
251
- nodes.push(astNode);
252
- nodeIdToIndex.set(node.id, currentIndex);
253
-
254
- const comment = language === 'python'
255
- ? (extractPythonDocstring(node) ?? extractPrecedingComment(node, source))
256
- : extractPrecedingComment(node, source);
257
-
258
- if (comment) {
259
- comments.push({ nodeIndex: currentIndex, text: comment, source: 'original' });
301
+ let ret: string | null = null;
302
+ if (o.kind === 'function' || o.kind === 'method' || o.kind === 'declaration') {
303
+ // Enclosing class for Self / static / this return types: the last qualifier segment.
304
+ const selfType = prefix.length ? prefix[prefix.length - 1]!.split('.').pop()! : null;
305
+ ret = returnTypeOf(signature, name, compiled.spec.id, selfType, compiled.spec.lowercaseTypes === true);
306
+ if (!ret) {
307
+ // No declared type: fall back to the documented one (`@return $this`, `@returns {Foo}`).
308
+ const documented = lead.returns ?? /@returns?\s+\{?([^\s}|]+)/.exec(doc)?.[1] ?? null;
309
+ if (documented) {
310
+ ret = /^\\?(\$this|static|self|this)$/.test(documented)
311
+ ? selfType
312
+ : typeName(documented, compiled.spec.lowercaseTypes === true);
313
+ }
314
+ }
260
315
  }
261
- } else if (nodeType === 'import' || nodeType === 'export') {
262
- const name = extractName(node);
263
- nodes.push({
264
- filePath,
316
+
317
+ const index = symbols.length;
318
+ symbols.push({
265
319
  name,
266
- type: nodeType,
267
- signature: node.text.slice(0, 200),
268
- startLine: node.startPosition.row + 1,
269
- endLine: node.endPosition.row + 1,
270
- startCol: node.startPosition.column,
271
- endCol: node.endPosition.column,
272
- parentId: null,
273
- language,
274
- bodyText: '',
320
+ qname,
321
+ kind: o.kind,
322
+ ret,
323
+ signature,
324
+ doc,
325
+ startLine: range.startPosition.row + 1,
326
+ // Nodes that swallow the trailing newline (preprocessor) end at column 0 of the next row.
327
+ endLine:
328
+ range.endPosition.column === 0 && range.endPosition.row > range.startPosition.row
329
+ ? range.endPosition.row
330
+ : range.endPosition.row + 1,
331
+ startCol: range.startPosition.column,
332
+ docLine,
333
+ parent,
334
+ exported: o.exported ?? false,
275
335
  });
336
+ if (o.scope !== false) {
337
+ // The qualifier is part of the scope so nested definitions inherit it.
338
+ const scopeName = o.qualifier ? o.qualifier.split(/::|\./).filter(Boolean).concat(name).join('.') : name;
339
+ scopes.push({ sym: index, name: scopeName, kind: o.kind, callScope: true });
340
+ }
341
+ return index;
342
+ },
343
+
344
+ scope(name: string): void {
345
+ if (!name) return;
346
+ scopes.push({ sym: -1, name, kind: null, callScope: false });
347
+ },
348
+
349
+ exportDefault(name): void {
350
+ // '*' = "whatever the required module exports" (`module.exports = require('./lib/x')`).
351
+ if (name === '*') defaultExport ??= '*';
352
+ else if (name && name.length <= MAX_NAME && /^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(name)) defaultExport = name;
353
+ },
354
+
355
+ ref(name, node): void {
356
+ if (!name || name.length < 2 || name.length > MAX_NAME) return;
357
+ addOcc(name, node.startPosition.row + 1);
358
+ },
359
+
360
+ typeHint(name, type, onContainer = false): void {
361
+ if (!name || name.length > MAX_NAME) return;
362
+ const t = typeName(type, compiled.spec.lowercaseTypes === true, compiled.spec.packageTypes === true);
363
+ if (!t) return;
364
+ // Owner: nearest enclosing function/method, else nearest container, else the file.
365
+ let owner = -1;
366
+ for (let i = scopes.length - 1; i >= 0; i--) {
367
+ const s = scopes[i]!;
368
+ if (s.sym < 0 || s.kind === null) continue;
369
+ const isContainer = CONTAINER_KINDS.has(s.kind);
370
+ if (isContainer || (!onContainer && (s.kind === 'function' || s.kind === 'method'))) { owner = s.sym; break; }
371
+ }
372
+ let map = hints.get(owner);
373
+ if (!map) hints.set(owner, (map = new Map()));
374
+ if (!map.has(name)) map.set(name, t);
375
+ },
376
+
377
+ typeOf(name): string | null {
378
+ if (!name) return null;
379
+ for (let i = scopes.length - 1; i >= 0; i--) {
380
+ const s = scopes[i]!;
381
+ // Class-level hints are fields (reached through this / self), not variables in scope.
382
+ if (s.sym < 0 || (s.kind !== 'function' && s.kind !== 'method')) continue;
383
+ const hit = hints.get(s.sym)?.get(name);
384
+ if (hit) return hit;
385
+ }
386
+ return hints.get(-1)?.get(name) ?? null;
387
+ },
388
+
389
+ call(name, node, receiver = null, receiverType = null): void {
390
+ if (!name || name.length > MAX_NAME) return;
391
+ if (!/^[\p{L}_$~][\p{L}\p{N}_$!?]*$/u.test(name)) return;
392
+ const caller = currentCaller();
393
+ const start = node.startIndex;
394
+ const end = node.endIndex;
395
+ const argc = argCount(node);
396
+ // Calls on expression receivers are kept apart per site until the chain is known (merged below).
397
+ const key = caller + '\0' + name + '\0' + (receiver === '?' ? '?' + start + ':' + end : receiver ?? '') + '\0' + argc;
398
+ let rec = callMap.get(key);
399
+ if (rec) {
400
+ rec.count++;
401
+ } else {
402
+ const rtype = receiverType
403
+ ? typeName(receiverType, compiled.spec.lowercaseTypes === true, compiled.spec.packageTypes === true)
404
+ : null;
405
+ // A chained call spanning several lines starts at the head of the chain; report the
406
+ // line where this call's own argument list opens (`->then(`), which is where a reader looks.
407
+ let row = node.startPosition.row;
408
+ if (node.endPosition.row !== row) {
409
+ const args = node.childForFieldName('arguments');
410
+ if (args) row = args.startPosition.row;
411
+ }
412
+ rec = { caller, name, line: row + 1, receiver, rtype, argc, count: 1, via: -1 };
413
+ callMap.set(key, rec);
414
+ }
415
+ // Call chains: in `a.b().c()` the expression for c starts where the expression for b starts,
416
+ // and the walker reaches c first. Link each outer call to the call that produces its receiver.
417
+ const outers = chainOuters.get(start);
418
+ if (outers) {
419
+ for (const o of outers) {
420
+ if (o.end > end && !viaRef.has(o.call) && o.call !== rec) viaRef.set(o.call, rec);
421
+ }
422
+ }
423
+ if (receiver === '?' && !rec.rtype) {
424
+ if (outers) outers.push({ call: rec, end });
425
+ else chainOuters.set(start, [{ call: rec, end }]);
426
+ }
427
+ // `x = make()`, `let x = Foo::new()?`, `x, err := load()`: x gets the call's return type.
428
+ const bound = boundVariable(node);
429
+ if (bound) {
430
+ let map = callHints.get(caller);
431
+ if (!map) callHints.set(caller, (map = new Map()));
432
+ if (!map.has(bound)) map.set(bound, rec);
433
+ }
434
+ },
435
+
436
+ imp(src, node, names = []): void {
437
+ if (!src) return;
438
+ imports.push({ source: src.slice(0, 300), names: names.slice(0, 50), line: node.startPosition.row + 1 });
439
+ },
440
+
441
+ inherit(sym, name, kind = 'extends', arity?: number): void {
442
+ if (!name || sym < 0) return;
443
+ const clean = name.trim().replace(/<.*$/s, '');
444
+ if (arity === undefined) arity = genericArity(name);
445
+ if (!clean || clean.length > MAX_NAME) return;
446
+ // `pkg.mod.Base` / `ns::Base`: index by the bare name, keep the qualifier for resolution.
447
+ const parts = clean.split(/\.|::|\\/).filter(Boolean);
448
+ const bare = parts.pop();
449
+ if (!bare) return;
450
+ const qualifier = parts.length ? parts[parts.length - 1]! : null;
451
+ // (`include Thor::Actions` and `include Rails::Generators::Actions` are two different supertypes.)
452
+ if (heritage.some(h => h.symbol === sym && h.name === bare && (h.qualifier ?? null) === (qualifier ?? null))) return;
453
+ heritage.push({ symbol: sym, name: bare, qualifier, kind, arity });
454
+ },
455
+
456
+ parentKind(): SymbolKind | null {
457
+ for (let i = scopes.length - 1; i >= 0; i--) {
458
+ if (scopes[i]!.sym >= 0) return scopes[i]!.kind;
459
+ }
460
+ return null;
461
+ },
462
+
463
+ inCallable(): boolean {
464
+ for (const s of scopes) {
465
+ if (s.kind === 'function' || s.kind === 'method') return true;
466
+ }
467
+ return false;
468
+ },
469
+
470
+ depth(): number {
471
+ let d = 0;
472
+ for (const s of scopes) if (s.sym >= 0) d++;
473
+ return d;
474
+ },
475
+ };
476
+
477
+ const cursor = tree.walk();
478
+ let hadError = tree.rootNode.hasError;
479
+
480
+ // Iterative pre-order walk (no recursion: generated files nest tens of thousands deep).
481
+ // `open` holds, for every node whose children are being visited, the scope depth to restore on leaving it.
482
+ const open: number[] = [];
483
+ try {
484
+ let entering = true;
485
+ for (;;) {
486
+ if (entering) {
487
+ const typeId = cursor.nodeTypeId;
488
+ if (isIdent[typeId]) {
489
+ const name = source.slice(cursor.startIndex, cursor.endIndex);
490
+ if (name.length >= 2 && name.length <= MAX_NAME) {
491
+ addOcc(name, cursor.startPosition.row + 1);
492
+ }
493
+ // Identifiers are leaves in every supported grammar, but a few grammars
494
+ // wrap them (e.g. PHP `name`); fall through to children only if any.
495
+ }
496
+
497
+ const before = scopes.length;
498
+ const visitor = visitors[typeId];
499
+ let skip = false;
500
+ if (visitor) {
501
+ try {
502
+ if (visitor(cursor.currentNode, ctx) === 'skip') skip = true;
503
+ } catch {
504
+ // A visitor bug must never take down indexing of the whole file.
505
+ hadError = true;
506
+ }
507
+ }
508
+
509
+ if (!skip && cursor.gotoFirstChild()) {
510
+ open.push(before);
511
+ continue;
512
+ }
513
+ scopes.length = before; // leaf (or skipped subtree): its scope closes right away
514
+ }
515
+ if (cursor.gotoNextSibling()) {
516
+ entering = true;
517
+ continue;
518
+ }
519
+ if (open.length === 0) break;
520
+ cursor.gotoParent();
521
+ scopes.length = open.pop()!;
522
+ entering = false;
276
523
  }
524
+ } finally {
525
+ cursor.delete();
526
+ }
277
527
 
278
- // Extract call expressions
279
- if (CALL_TYPES.has(node.type)) {
280
- const calleeName = extractCallName(node);
281
- const enclosingFunc = findEnclosingFunctionIndex(node);
282
- if (calleeName && enclosingFunc !== null) {
283
- edges.push({
284
- callerNodeIndex: enclosingFunc,
285
- calleeName,
286
- line: node.startPosition.row + 1,
287
- });
528
+ // Resolve receivers to declared types: `x.foo()` with `x: Foo` in scope,
529
+ // `this.repo.find()` with a typed `repo` field on the enclosing class,
530
+ // `s.pool.Get()` where `s` is a typed receiver/parameter whose class is in this file.
531
+ if (hints.size) {
532
+ const SELF = new Set(['this', 'self', '$this', 'base', 'Self', 'super']);
533
+ let containerByName: Map<string, number> | null = null;
534
+ const containerNamed = (name: string): number | undefined => {
535
+ if (!containerByName) {
536
+ containerByName = new Map();
537
+ for (let i = 0; i < symbols.length; i++) {
538
+ const s = symbols[i]!;
539
+ if (CONTAINER_KINDS.has(s.kind) && !containerByName.has(s.name)) containerByName.set(s.name, i);
540
+ }
541
+ }
542
+ return containerByName.get(name);
543
+ };
544
+ /** Declared type of `name` visible from symbol `owner` (walks enclosing symbols, then the file). */
545
+ // Only where members can be named without `this.` is a class field in scope for a bare name:
546
+ // in TypeScript or Python a local `targets` is not the field `this.targets`.
547
+ const implicitThis = IMPLICIT_THIS_SPECS.has(compiled.spec.id);
548
+ const lookup = (owner: number, name: string): string | undefined => {
549
+ for (let cur = owner, guard = 0; guard < 12; guard++) {
550
+ const skip = !implicitThis && cur >= 0 && CONTAINER_KINDS.has(symbols[cur]!.kind);
551
+ const hit = skip ? undefined : hints.get(cur)?.get(name);
552
+ if (hit) return hit;
553
+ if (cur < 0) return undefined;
554
+ cur = symbols[cur]!.parent;
555
+ }
556
+ return undefined;
557
+ };
558
+ /** Class-like symbol a method belongs to: lexical parent, or by qualified name (Go receivers, Rust impl, C++ out-of-line). */
559
+ const containerOf = (sym: number): number | undefined => {
560
+ for (let cur = sym; cur >= 0; cur = symbols[cur]!.parent) {
561
+ if (CONTAINER_KINDS.has(symbols[cur]!.kind)) return cur;
562
+ }
563
+ if (sym < 0) return undefined;
564
+ const parts = symbols[sym]!.qname.split('.');
565
+ for (let i = parts.length - 2; i >= 0; i--) {
566
+ const hit = containerNamed(parts[i]!);
567
+ if (hit !== undefined) return hit;
288
568
  }
569
+ return undefined;
570
+ };
571
+ const fieldType = (container: number | undefined, name: string): string | undefined =>
572
+ container === undefined ? undefined : hints.get(container)?.get(name);
573
+
574
+ for (const call of callMap.values()) {
575
+ const r = call.receiver;
576
+ if (!r || r === '?' || call.rtype) continue;
577
+ const parts = r.replace(/->|::/g, '.').split('.');
578
+ if (parts.length > 2 || parts.some(p => !/^[$@]?[A-Za-z_]\w*$/.test(p))) continue;
579
+ let t: string | undefined;
580
+ if (parts.length === 1) {
581
+ const p = parts[0]!;
582
+ if (SELF.has(p)) continue;
583
+ // Ruby @ivar is a field of the enclosing class; anything else is a local / parameter.
584
+ t = p.startsWith('@')
585
+ ? fieldType(containerOf(call.caller), p.slice(1))
586
+ : lookup(call.caller, p.replace(/^\$/, ''));
587
+ } else {
588
+ const [a, b] = parts as [string, string];
589
+ const field = b.replace(/^[$@]/, '');
590
+ if (SELF.has(a)) {
591
+ t = fieldType(containerOf(call.caller), field);
592
+ } else {
593
+ const ta = lookup(call.caller, a.replace(/^[$@]/, ''));
594
+ if (ta) {
595
+ t = fieldType(containerNamed(ta), field);
596
+ // `opts.logger.info()` with `opts: Options` declared in another file: the type of the
597
+ // field is not known here. Leave "Options#logger" for the resolver, which sees every file.
598
+ if (!t && containerNamed(ta) === undefined) t = ta + '#' + field;
599
+ }
600
+ }
601
+ }
602
+ if (t) call.rtype = t;
289
603
  }
604
+ }
605
+ // Receivers that are variables initialised from a call take that call's return type
606
+ // (resolved at query time through the same link as call chains).
607
+ if (callHints.size) {
608
+ for (const call of callMap.values()) {
609
+ const r = call.receiver;
610
+ if (!r || r === '?' || call.rtype || viaRef.has(call)) continue;
611
+ if (!/^[$@]?[A-Za-z_]\w*$/.test(r)) continue;
612
+ const name = r.replace(/^[$@]/, '');
613
+ for (let cur = call.caller, guard = 0; guard < 6; guard++) {
614
+ const hit = callHints.get(cur)?.get(name);
615
+ if (hit && hit !== call) {
616
+ viaRef.set(call, hit);
617
+ break;
618
+ }
619
+ if (cur < 0) break;
620
+ const s = symbols[cur]!;
621
+ // Stop at the function boundary: locals of an enclosing class / file are not visible by bare name.
622
+ if (s.kind !== 'function' && s.kind !== 'method') break;
623
+ cur = s.parent;
624
+ if (cur >= 0 && symbols[cur]!.kind !== 'function' && symbols[cur]!.kind !== 'method') break;
625
+ }
626
+ }
627
+ }
290
628
 
291
- // Recurse into children
292
- for (let i = 0; i < node.childCount; i++) {
293
- const child = node.child(i);
294
- if (child) {
295
- walk(child, currentIndex);
629
+ // Merge chained calls that are the same edge (`x.a().b()` twice in one function), then
630
+ // express receiver links as indexes into the final list.
631
+ const calls: ExCall[] = [];
632
+ {
633
+ const kept = new Map<string, ExCall>();
634
+ const redirect = new Map<ExCall, ExCall>();
635
+ for (const call of callMap.values()) {
636
+ if (call.receiver !== '?') {
637
+ calls.push(call);
638
+ continue;
296
639
  }
640
+ const inner = viaRef.get(call);
641
+ const key =
642
+ call.caller + '\0' + call.name + '\0' + (inner ? inner.name + '\0' + (inner.receiver ?? '') : '') + '\0' +
643
+ (call.rtype ?? '') + '\0' + call.argc;
644
+ const first = kept.get(key);
645
+ if (first) {
646
+ first.count += call.count;
647
+ redirect.set(call, first);
648
+ } else {
649
+ kept.set(key, call);
650
+ calls.push(call);
651
+ }
652
+ }
653
+ const indexOf = new Map<ExCall, number>();
654
+ calls.forEach((c, i) => indexOf.set(c, i));
655
+ for (const [outer, innerRaw] of viaRef) {
656
+ if (redirect.has(outer)) continue;
657
+ const inner = redirect.get(innerRaw) ?? innerRaw;
658
+ const i = indexOf.get(inner);
659
+ if (i !== undefined) outer.via = i;
297
660
  }
298
661
  }
299
662
 
300
- walk(tree.rootNode, null);
663
+ // Field types of class-like symbols are exported so the resolver can type
664
+ // `this.x.foo()` / `x_->foo()` when the class lives in another file
665
+ // (C++ headers, Go structs in the same package, partial classes).
666
+ const fieldTypes: Array<[string, string, string]> = [];
667
+ for (const [owner, map] of hints) {
668
+ if (owner < 0 || !CONTAINER_KINDS.has(symbols[owner]!.kind)) continue;
669
+ const container = symbols[owner]!.name;
670
+ for (const [field, type] of map) {
671
+ if (fieldTypes.length < 4000) fieldTypes.push([container, field, type]);
672
+ }
673
+ }
301
674
 
302
- return { nodes, edges, comments };
675
+ let lineCount = 1;
676
+ for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1)) lineCount++;
677
+
678
+ return {
679
+ lang: compiled.spec.id,
680
+ lineCount,
681
+ symbols,
682
+ calls,
683
+ imports,
684
+ heritage,
685
+ fieldTypes,
686
+ defaultExport,
687
+ occ: [...occ.entries()].map(([name, lines]) => [name, lines.join(',')] as [string, string]),
688
+ hadError,
689
+ };
303
690
  }