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