clast-ai 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +11 -9
  3. package/README.md +468 -398
  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 +266 -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/migrate.d.ts +35 -0
  59. package/dist/migrate.js +194 -0
  60. package/dist/migrate.js.map +1 -0
  61. package/dist/parser/extractor.d.ts +21 -16
  62. package/dist/parser/extractor.js +700 -228
  63. package/dist/parser/extractor.js.map +1 -1
  64. package/dist/parser/index.d.ts +17 -21
  65. package/dist/parser/index.js +115 -171
  66. package/dist/parser/index.js.map +1 -1
  67. package/dist/parser/langs/c.d.ts +3 -0
  68. package/dist/parser/langs/c.js +879 -0
  69. package/dist/parser/langs/c.js.map +1 -0
  70. package/dist/parser/langs/csharp.d.ts +2 -0
  71. package/dist/parser/langs/csharp.js +548 -0
  72. package/dist/parser/langs/csharp.js.map +1 -0
  73. package/dist/parser/langs/dart.d.ts +2 -0
  74. package/dist/parser/langs/dart.js +893 -0
  75. package/dist/parser/langs/dart.js.map +1 -0
  76. package/dist/parser/langs/go.d.ts +2 -0
  77. package/dist/parser/langs/go.js +455 -0
  78. package/dist/parser/langs/go.js.map +1 -0
  79. package/dist/parser/langs/java.d.ts +2 -0
  80. package/dist/parser/langs/java.js +390 -0
  81. package/dist/parser/langs/java.js.map +1 -0
  82. package/dist/parser/langs/php.d.ts +2 -0
  83. package/dist/parser/langs/php.js +486 -0
  84. package/dist/parser/langs/php.js.map +1 -0
  85. package/dist/parser/langs/python.d.ts +2 -0
  86. package/dist/parser/langs/python.js +216 -0
  87. package/dist/parser/langs/python.js.map +1 -0
  88. package/dist/parser/langs/ruby.d.ts +2 -0
  89. package/dist/parser/langs/ruby.js +602 -0
  90. package/dist/parser/langs/ruby.js.map +1 -0
  91. package/dist/parser/langs/rust.d.ts +2 -0
  92. package/dist/parser/langs/rust.js +931 -0
  93. package/dist/parser/langs/rust.js.map +1 -0
  94. package/dist/parser/langs/swift.d.ts +2 -0
  95. package/dist/parser/langs/swift.js +616 -0
  96. package/dist/parser/langs/swift.js.map +1 -0
  97. package/dist/parser/langs/typescript.d.ts +4 -0
  98. package/dist/parser/langs/typescript.js +535 -0
  99. package/dist/parser/langs/typescript.js.map +1 -0
  100. package/dist/parser/registry.d.ts +16 -0
  101. package/dist/parser/registry.js +87 -0
  102. package/dist/parser/registry.js.map +1 -0
  103. package/dist/parser/returns.d.ts +9 -0
  104. package/dist/parser/returns.js +152 -0
  105. package/dist/parser/returns.js.map +1 -0
  106. package/dist/parser/spec.d.ts +165 -0
  107. package/dist/parser/spec.js +188 -0
  108. package/dist/parser/spec.js.map +1 -0
  109. package/dist/query/arity.d.ts +27 -0
  110. package/dist/query/arity.js +187 -0
  111. package/dist/query/arity.js.map +1 -0
  112. package/dist/query/callgraph.d.ts +113 -0
  113. package/dist/query/callgraph.js +332 -0
  114. package/dist/query/callgraph.js.map +1 -0
  115. package/dist/query/format.d.ts +36 -0
  116. package/dist/query/format.js +136 -0
  117. package/dist/query/format.js.map +1 -0
  118. package/dist/query/hierarchy.d.ts +44 -0
  119. package/dist/query/hierarchy.js +441 -0
  120. package/dist/query/hierarchy.js.map +1 -0
  121. package/dist/query/resolve.d.ts +265 -0
  122. package/dist/query/resolve.js +1957 -0
  123. package/dist/query/resolve.js.map +1 -0
  124. package/dist/query/search.d.ts +35 -0
  125. package/dist/query/search.js +379 -0
  126. package/dist/query/search.js.map +1 -0
  127. package/dist/query/words.d.ts +30 -0
  128. package/dist/query/words.js +201 -0
  129. package/dist/query/words.js.map +1 -0
  130. package/dist/server/index.d.ts +1 -3
  131. package/dist/server/index.js +313 -73
  132. package/dist/server/index.js.map +1 -1
  133. package/dist/tools/context.d.ts +35 -0
  134. package/dist/tools/context.js +79 -0
  135. package/dist/tools/context.js.map +1 -0
  136. package/dist/tools/explore.d.ts +51 -0
  137. package/dist/tools/explore.js +779 -0
  138. package/dist/tools/explore.js.map +1 -0
  139. package/dist/tools/impact.d.ts +21 -0
  140. package/dist/tools/impact.js +743 -0
  141. package/dist/tools/impact.js.map +1 -0
  142. package/dist/tools/search.d.ts +59 -0
  143. package/dist/tools/search.js +843 -0
  144. package/dist/tools/search.js.map +1 -0
  145. package/dist/tools/stats.d.ts +26 -0
  146. package/dist/tools/stats.js +604 -0
  147. package/dist/tools/stats.js.map +1 -0
  148. package/dist/types.d.ts +95 -40
  149. package/dist/types.js +9 -1
  150. package/dist/types.js.map +1 -1
  151. package/dist/version.d.ts +2 -0
  152. package/dist/version.js +3 -0
  153. package/dist/version.js.map +1 -0
  154. package/dist/watcher/index.d.ts +18 -16
  155. package/dist/watcher/index.js +94 -77
  156. package/dist/watcher/index.js.map +1 -1
  157. package/package.json +21 -24
  158. package/plugin/server.mjs +72 -0
  159. package/skills/audit/SKILL.md +37 -0
  160. package/skills/deadcode/SKILL.md +33 -0
  161. package/skills/explore/SKILL.md +54 -0
  162. package/skills/impact/SKILL.md +24 -0
  163. package/skills/status/SKILL.md +11 -0
  164. package/src/cli.ts +265 -0
  165. package/src/config/defaults.ts +50 -32
  166. package/src/config/index.ts +23 -14
  167. package/src/db/index.ts +540 -43
  168. package/src/engine.ts +30 -0
  169. package/src/indexer/imports.ts +241 -0
  170. package/src/indexer/index.ts +369 -0
  171. package/src/indexer/process.ts +63 -0
  172. package/src/indexer/scan.ts +226 -0
  173. package/src/indexer/worker.ts +24 -0
  174. package/src/llm/comment-generator.ts +52 -103
  175. package/src/llm/index.ts +80 -80
  176. package/src/llm/prompts.ts +95 -95
  177. package/src/migrate.ts +213 -0
  178. package/src/parser/extractor.ts +643 -256
  179. package/src/parser/index.ts +105 -205
  180. package/src/parser/langs/c.ts +860 -0
  181. package/src/parser/langs/csharp.ts +519 -0
  182. package/src/parser/langs/dart.ts +820 -0
  183. package/src/parser/langs/go.ts +437 -0
  184. package/src/parser/langs/java.ts +361 -0
  185. package/src/parser/langs/php.ts +450 -0
  186. package/src/parser/langs/python.ts +206 -0
  187. package/src/parser/langs/ruby.ts +554 -0
  188. package/src/parser/langs/rust.ts +904 -0
  189. package/src/parser/langs/swift.ts +590 -0
  190. package/src/parser/langs/typescript.ts +499 -0
  191. package/src/parser/registry.ts +88 -0
  192. package/src/parser/returns.ts +137 -0
  193. package/src/parser/spec.ts +309 -0
  194. package/src/query/arity.ts +173 -0
  195. package/src/query/callgraph.ts +473 -0
  196. package/src/query/format.ts +136 -0
  197. package/src/query/hierarchy.ts +447 -0
  198. package/src/query/resolve.ts +1972 -0
  199. package/src/query/search.ts +430 -0
  200. package/src/query/words.ts +183 -0
  201. package/src/server/index.ts +390 -79
  202. package/src/tools/context.ts +82 -0
  203. package/src/tools/explore.ts +860 -0
  204. package/src/tools/impact.ts +748 -0
  205. package/src/tools/search.ts +886 -0
  206. package/src/tools/stats.ts +633 -0
  207. package/src/types.ts +113 -45
  208. package/src/version.ts +2 -0
  209. package/src/watcher/index.ts +82 -88
  210. package/tsconfig.json +6 -3
  211. package/.mcp.json +0 -8
  212. package/commands/clast-status.md +0 -9
  213. package/dist/config/defaults.d.ts.map +0 -1
  214. package/dist/config/index.d.ts.map +0 -1
  215. package/dist/db/index.d.ts.map +0 -1
  216. package/dist/db/queries.d.ts +0 -44
  217. package/dist/db/queries.d.ts.map +0 -1
  218. package/dist/db/queries.js +0 -251
  219. package/dist/db/queries.js.map +0 -1
  220. package/dist/db/schema.d.ts +0 -3
  221. package/dist/db/schema.d.ts.map +0 -1
  222. package/dist/db/schema.js +0 -94
  223. package/dist/db/schema.js.map +0 -1
  224. package/dist/db/types.d.ts +0 -44
  225. package/dist/db/types.d.ts.map +0 -1
  226. package/dist/db/types.js +0 -2
  227. package/dist/db/types.js.map +0 -1
  228. package/dist/llm/comment-generator.d.ts.map +0 -1
  229. package/dist/llm/index.d.ts.map +0 -1
  230. package/dist/llm/prompts.d.ts.map +0 -1
  231. package/dist/parser/call-resolver.d.ts +0 -15
  232. package/dist/parser/call-resolver.d.ts.map +0 -1
  233. package/dist/parser/call-resolver.js +0 -16
  234. package/dist/parser/call-resolver.js.map +0 -1
  235. package/dist/parser/extractor.d.ts.map +0 -1
  236. package/dist/parser/index.d.ts.map +0 -1
  237. package/dist/parser/languages.d.ts +0 -11
  238. package/dist/parser/languages.d.ts.map +0 -1
  239. package/dist/parser/languages.js +0 -74
  240. package/dist/parser/languages.js.map +0 -1
  241. package/dist/server/index.d.ts.map +0 -1
  242. package/dist/server/register-tools.d.ts +0 -7
  243. package/dist/server/register-tools.d.ts.map +0 -1
  244. package/dist/server/register-tools.js +0 -17
  245. package/dist/server/register-tools.js.map +0 -1
  246. package/dist/server/tools/clast-call-graph.d.ts +0 -4
  247. package/dist/server/tools/clast-call-graph.d.ts.map +0 -1
  248. package/dist/server/tools/clast-call-graph.js +0 -70
  249. package/dist/server/tools/clast-call-graph.js.map +0 -1
  250. package/dist/server/tools/clast-comment.d.ts +0 -5
  251. package/dist/server/tools/clast-comment.d.ts.map +0 -1
  252. package/dist/server/tools/clast-comment.js +0 -64
  253. package/dist/server/tools/clast-comment.js.map +0 -1
  254. package/dist/server/tools/clast-file-summary.d.ts +0 -4
  255. package/dist/server/tools/clast-file-summary.d.ts.map +0 -1
  256. package/dist/server/tools/clast-file-summary.js +0 -42
  257. package/dist/server/tools/clast-file-summary.js.map +0 -1
  258. package/dist/server/tools/clast-get-context.d.ts +0 -4
  259. package/dist/server/tools/clast-get-context.d.ts.map +0 -1
  260. package/dist/server/tools/clast-get-context.js +0 -80
  261. package/dist/server/tools/clast-get-context.js.map +0 -1
  262. package/dist/server/tools/clast-reindex.d.ts +0 -5
  263. package/dist/server/tools/clast-reindex.d.ts.map +0 -1
  264. package/dist/server/tools/clast-reindex.js +0 -36
  265. package/dist/server/tools/clast-reindex.js.map +0 -1
  266. package/dist/server/tools/clast-search.d.ts +0 -4
  267. package/dist/server/tools/clast-search.d.ts.map +0 -1
  268. package/dist/server/tools/clast-search.js +0 -29
  269. package/dist/server/tools/clast-search.js.map +0 -1
  270. package/dist/server/tools/clast-status.d.ts +0 -5
  271. package/dist/server/tools/clast-status.d.ts.map +0 -1
  272. package/dist/server/tools/clast-status.js +0 -23
  273. package/dist/server/tools/clast-status.js.map +0 -1
  274. package/dist/types.d.ts.map +0 -1
  275. package/dist/watcher/gitignore.d.ts +0 -9
  276. package/dist/watcher/gitignore.d.ts.map +0 -1
  277. package/dist/watcher/gitignore.js +0 -38
  278. package/dist/watcher/gitignore.js.map +0 -1
  279. package/dist/watcher/index.d.ts.map +0 -1
  280. package/skills/clast-explore/SKILL.md +0 -41
  281. package/src/db/queries.ts +0 -332
  282. package/src/db/schema.ts +0 -106
  283. package/src/db/types.ts +0 -47
  284. package/src/parser/call-resolver.ts +0 -17
  285. package/src/parser/languages.ts +0 -86
  286. package/src/server/register-tools.ts +0 -30
  287. package/src/server/tools/clast-call-graph.ts +0 -82
  288. package/src/server/tools/clast-comment.ts +0 -76
  289. package/src/server/tools/clast-file-summary.ts +0 -59
  290. package/src/server/tools/clast-get-context.ts +0 -97
  291. package/src/server/tools/clast-reindex.ts +0 -52
  292. package/src/server/tools/clast-search.ts +0 -38
  293. package/src/server/tools/clast-status.ts +0 -37
  294. package/src/watcher/gitignore.ts +0 -42
@@ -0,0 +1,554 @@
1
+ import type { Node } from 'web-tree-sitter';
2
+ import type { Ctx, LangSpec, Visitor } from '../spec.js';
3
+ import { COMPLEX_RECEIVER, field } from '../spec.js';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Per-file state. Visitors get no "leave" callback, so class/module visitors
7
+ // pre-scan the direct children of their body once and remember which methods
8
+ // sit in a `private`/`protected` section. Reset by the `program` visitor.
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /** Ids of `method` nodes that follow a bare `private` / `protected`. */
12
+ const privateIds = new Set<number>();
13
+ /** `<body id>:<name>` for `private :foo, :bar` / `private_class_method :foo`. */
14
+ const privateNames = new Set<string>();
15
+ /** `<symbol>:<name>` of heritage entries already recorded (de-duplication). */
16
+ const heritageSeen = new Set<string>();
17
+
18
+ const MIXINS = new Set(['include', 'extend', 'prepend']);
19
+ const HIDERS = new Set(['private', 'protected', 'private_class_method']);
20
+
21
+ function inType(ctx: Ctx): boolean {
22
+ const pk = ctx.parentKind();
23
+ return pk === 'class' || pk === 'namespace';
24
+ }
25
+
26
+ /** `Foo` / `A::Foo` / `Foo[1]` / `Foo.new(...)` -> `Foo`. */
27
+ /**
28
+ * Like constName(), but keeps one level of qualification (`ActiveSupport::Callbacks`)
29
+ * so that same-named constants in different namespaces can be told apart.
30
+ */
31
+ function qualifiedConstName(node: Node | null | undefined): string | null {
32
+ if (node && node.type === 'scope_resolution') {
33
+ const name = field(node, 'name');
34
+ const scope = constName(field(node, 'scope'));
35
+ if (name && scope) return scope + '::' + name.text;
36
+ }
37
+ return constName(node);
38
+ }
39
+
40
+ function constName(node: Node | null | undefined): string | null {
41
+ for (let i = 0; node && i < 4; i++) {
42
+ switch (node.type) {
43
+ case 'constant': return node.text;
44
+ case 'scope_resolution': node = field(node, 'name'); break;
45
+ case 'element_reference': node = field(node, 'object'); break;
46
+ case 'call': node = field(node, 'receiver'); break;
47
+ default: return null;
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+
53
+ /** Literal value of `:sym`, `'str'` or `"str"` (no interpolation), else null. */
54
+ function literal(node: Node | null | undefined): string | null {
55
+ if (!node) return null;
56
+ if (node.type === 'simple_symbol') return node.text.slice(1);
57
+ if (node.type === 'string') {
58
+ if (node.namedChildCount === 0) return null;
59
+ if (node.namedChildCount !== 1) return null;
60
+ const c = node.namedChildren[0];
61
+ return c && c.type === 'string_content' ? c.text : null;
62
+ }
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * The grammar attaches comments that precede the FIRST statement of a
68
+ * class/module body to the class/module node itself (before `body`), so they
69
+ * are not siblings of the definition and the generic doc lookup misses them.
70
+ */
71
+ function firstChildDoc(range: Node): string | undefined {
72
+ const parent = range.parent;
73
+ if (!parent || parent.type !== 'body_statement' || parent.startIndex !== range.startIndex) return undefined;
74
+ const parts: string[] = [];
75
+ let nextRow = range.startPosition.row;
76
+ let prev = parent.previousSibling;
77
+ while (prev && prev.type === 'comment' && parts.length < 60) {
78
+ if (prev.endPosition.row < nextRow - 1) break;
79
+ nextRow = prev.startPosition.row;
80
+ // A comment trailing the `class Foo` line documents nothing below it.
81
+ const before = prev.previousSibling;
82
+ if (before && before.endPosition.row === nextRow) break;
83
+ parts.unshift(prev.text.replace(/^#+!? ?/, '').trimEnd());
84
+ prev = prev.previousSibling;
85
+ }
86
+ while (parts.length && !parts[0]) parts.shift();
87
+ while (parts.length && !parts[parts.length - 1]) parts.pop();
88
+ return parts.length ? parts.join('\n') : undefined;
89
+ }
90
+
91
+ function args(call: Node): Node[] {
92
+ const list = field(call, 'arguments');
93
+ if (!list) return [];
94
+ const out: Node[] = [];
95
+ for (const c of list.namedChildren) if (c) out.push(c);
96
+ return out;
97
+ }
98
+
99
+ function firstLine(ctx: Ctx, node: Node, end = node.endIndex): string {
100
+ const nl = ctx.src.indexOf('\n', node.startIndex);
101
+ return ctx.src.slice(node.startIndex, nl !== -1 && nl < end ? nl : end);
102
+ }
103
+
104
+ /**
105
+ * Record a superclass / mixin once per (symbol, name). A symbol never
106
+ * inherits from its own name (`extend self`-style self-extension,
107
+ * `module ClassMethods; include Foo::ClassMethods`).
108
+ */
109
+ function inherit(ctx: Ctx, sym: number, own: string, name: string | null, kind: 'extends' | 'implements'): void {
110
+ // `module Callbacks; include ActiveSupport::Callbacks` is a real mixin: only the bare own name is skipped.
111
+ if (!name || sym < 0 || name === own) return;
112
+ const key = sym + ':' + name;
113
+ if (heritageSeen.has(key)) return;
114
+ heritageSeen.add(key);
115
+ ctx.inherit(sym, name, kind);
116
+ }
117
+
118
+ /**
119
+ * Scan the direct children of a class/module body: record mixins as heritage
120
+ * of `sym` (when >= 0, `own` is its name) and remember which methods are private.
121
+ */
122
+ function scanBody(body: Node | null, sym: number, own: string, ctx: Ctx): void {
123
+ if (!body) return;
124
+ let hidden = false;
125
+ for (const c of body.namedChildren) {
126
+ if (!c) continue;
127
+ const t = c.type;
128
+ if (t === 'method') {
129
+ if (hidden) privateIds.add(c.id);
130
+ } else if (t === 'identifier') {
131
+ const w = c.text;
132
+ if (w === 'private' || w === 'protected') hidden = true;
133
+ else if (w === 'public') hidden = false;
134
+ } else if (t === 'call') {
135
+ if (field(c, 'receiver')) continue;
136
+ const m = field(c, 'method');
137
+ if (!m) continue;
138
+ const name = m.text;
139
+ if (MIXINS.has(name)) {
140
+ if (sym < 0) continue;
141
+ for (const a of args(c)) inherit(ctx, sym, own, qualifiedConstName(a), 'implements');
142
+ } else if (HIDERS.has(name)) {
143
+ for (const a of args(c)) {
144
+ const lit = literal(a);
145
+ if (lit) privateNames.add(body.id + ':' + lit);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ /** Open naming scopes for the `A::B` part of `class A::B::C`. */
153
+ function openQualifier(nameNode: Node, ctx: Ctx): string {
154
+ if (nameNode.type !== 'scope_resolution') return nameNode.text;
155
+ const scope = field(nameNode, 'scope');
156
+ if (scope) {
157
+ for (const part of scope.text.split('::')) {
158
+ const p = part.trim();
159
+ if (p) ctx.scope(p);
160
+ }
161
+ }
162
+ return field(nameNode, 'name')?.text ?? nameNode.text;
163
+ }
164
+
165
+ const program: Visitor = () => {
166
+ privateIds.clear();
167
+ privateNames.clear();
168
+ heritageSeen.clear();
169
+ };
170
+
171
+ const classDef: Visitor = (node, ctx) => {
172
+ const nameNode = field(node, 'name');
173
+ if (!nameNode) return;
174
+ const superclass = field(node, 'superclass');
175
+ const sig = ctx.src.slice(node.startIndex, (superclass ?? nameNode).endIndex);
176
+ const name = openQualifier(nameNode, ctx);
177
+ const sym = ctx.def(node, { name, kind: 'class', sig, doc: firstChildDoc(node), exported: !name.startsWith('_') });
178
+ if (superclass) inherit(ctx, sym, name, qualifiedConstName(superclass.namedChildren[0]), 'extends');
179
+ scanBody(field(node, 'body'), sym, name, ctx);
180
+ };
181
+
182
+ const moduleDef: Visitor = (node, ctx) => {
183
+ const nameNode = field(node, 'name');
184
+ if (!nameNode) return;
185
+ const sig = ctx.src.slice(node.startIndex, nameNode.endIndex);
186
+ const name = openQualifier(nameNode, ctx);
187
+ const sym = ctx.def(node, { name, kind: 'namespace', sig, doc: firstChildDoc(node), exported: !name.startsWith('_') });
188
+ scanBody(field(node, 'body'), sym, name, ctx);
189
+ };
190
+
191
+ /** `class << self`: no symbol and no scope, methods belong to the enclosing class. */
192
+ const singletonClass: Visitor = (node, ctx) => {
193
+ scanBody(field(node, 'body'), -1, '', ctx);
194
+ };
195
+
196
+ /** `def foo`, `def self.foo`, `private def foo`. */
197
+ const methodDef: Visitor = (node, ctx) => {
198
+ const nameNode = field(node, 'name');
199
+ if (!nameNode) return;
200
+ const name = nameNode.text;
201
+ const parent = node.parent;
202
+ let range: Node = node;
203
+ let hidden = privateIds.has(node.id);
204
+ if (parent) {
205
+ if (parent.type === 'argument_list') {
206
+ // `private def foo`, `memoize def foo`, `private_class_method def self.foo`
207
+ const wrapper = parent.parent;
208
+ if (wrapper && wrapper.type === 'call' && !field(wrapper, 'receiver')) {
209
+ range = wrapper;
210
+ const w = field(wrapper, 'method')?.text;
211
+ if (w && HIDERS.has(w)) hidden = true;
212
+ }
213
+ } else if (!hidden && privateNames.size > 0 && privateNames.has(parent.id + ':' + name)) {
214
+ hidden = true;
215
+ }
216
+ }
217
+ const end = field(node, 'parameters') ?? nameNode;
218
+ let sig = ctx.src.slice(range.startIndex, end.endIndex);
219
+ if (node.type === 'method') {
220
+ // Inside `class << self` show the receiver, like `def self.foo`.
221
+ const holder = range.parent?.parent;
222
+ if (holder && holder.type === 'singleton_class' && field(holder, 'value')?.type === 'self') {
223
+ sig = ctx.src.slice(range.startIndex, nameNode.startIndex) + 'self.' + ctx.src.slice(nameNode.startIndex, end.endIndex);
224
+ }
225
+ }
226
+ ctx.def(node, {
227
+ name,
228
+ kind: inType(ctx) ? 'method' : 'function',
229
+ range,
230
+ sig,
231
+ doc: firstChildDoc(range),
232
+ exported: !hidden && !name.startsWith('_'),
233
+ });
234
+ };
235
+
236
+ /** `alias new_name old_name` */
237
+ const aliasDef: Visitor = (node, ctx) => {
238
+ const nameNode = field(node, 'name');
239
+ if (!nameNode) return;
240
+ ctx.def(node, {
241
+ name: nameNode.text.replace(/^:/, ''),
242
+ kind: inType(ctx) ? 'method' : 'function',
243
+ body: null,
244
+ doc: firstChildDoc(node),
245
+ exported: true,
246
+ });
247
+ };
248
+
249
+ const CLASS_FACTORIES = new Set(['Struct', 'Class', 'Data', 'Module']);
250
+
251
+ /** `FOO = 1`, `Point = Struct.new(:x, :y) do ... end` */
252
+ const assignment: Visitor = (node, ctx) => {
253
+ const left = field(node, 'left');
254
+ if (!left) return;
255
+ const lt = left.type;
256
+ if (lt === 'identifier' || lt === 'instance_variable') {
257
+ // `x = Foo.new(...)`, `@x ||= Foo::Bar.new`: the only cheaply inferable types.
258
+ const right = field(node, 'right');
259
+ if (!right || right.type !== 'call') return;
260
+ const recv = field(right, 'receiver');
261
+ if (!recv || (recv.type !== 'constant' && recv.type !== 'scope_resolution')) return;
262
+ if (field(right, 'method')?.text !== 'new') return;
263
+ if (lt === 'identifier') ctx.typeHint(left.text, recv.text);
264
+ else ctx.typeHint(left.text.slice(1), recv.text, true);
265
+ return;
266
+ }
267
+ if (lt !== 'constant') return;
268
+ if (ctx.inCallable()) return;
269
+ const name = left.text;
270
+ const right = field(node, 'right');
271
+ if (right && right.type === 'call') {
272
+ const recv = field(right, 'receiver');
273
+ const m = field(right, 'method')?.text;
274
+ if (recv && recv.type === 'constant' && (m === 'new' || m === 'define') && CLASS_FACTORIES.has(recv.text)) {
275
+ const factory = recv.text;
276
+ const block = field(right, 'block');
277
+ const sym = ctx.def(node, {
278
+ name,
279
+ kind: factory === 'Module' ? 'namespace' : 'class',
280
+ sig: firstLine(ctx, node, block ? block.startIndex : node.endIndex),
281
+ doc: firstChildDoc(node),
282
+ exported: true,
283
+ });
284
+ if (factory === 'Class') inherit(ctx, sym, name, constName(args(right)[0]), 'extends');
285
+ else if (factory !== 'Module') inherit(ctx, sym, name, factory, 'extends');
286
+ scanBody(field(block, 'body'), sym, name, ctx);
287
+ return;
288
+ }
289
+ }
290
+ ctx.def(node, {
291
+ name,
292
+ kind: 'constant',
293
+ sig: firstLine(ctx, node),
294
+ doc: firstChildDoc(node),
295
+ exported: !name.startsWith('_'),
296
+ });
297
+ };
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // Receiver-less "keyword-like" calls. A handler returns true when the call was
301
+ // fully consumed and must not be recorded as a call.
302
+ // ---------------------------------------------------------------------------
303
+
304
+ type Dsl = (node: Node, ctx: Ctx, name: string) => boolean;
305
+
306
+ const consumed: Dsl = () => true;
307
+
308
+ const requireCall: Dsl = (node, ctx, name) => {
309
+ const src = literal(args(node)[0]);
310
+ if (src) ctx.imp(name === 'require_relative' && !/^\.\.?\//.test(src) ? './' + src : src, node);
311
+ return true;
312
+ };
313
+
314
+ const autoloadCall: Dsl = (node, ctx) => {
315
+ const a = args(node);
316
+ const path = a[1] && a[1].type === 'string' ? literal(a[1]) : null;
317
+ if (path) {
318
+ const name = literal(a[0]);
319
+ ctx.imp(path, node, name ? [name] : []);
320
+ }
321
+ return true;
322
+ };
323
+
324
+ /** Define one leaf symbol per literal symbol/string argument. */
325
+ function defEach(node: Node, ctx: Ctx, kind: 'property' | 'method', all: boolean): boolean {
326
+ if (!inType(ctx) || ctx.inCallable()) return false;
327
+ const list = args(node);
328
+ const names: string[] = [];
329
+ for (const a of list) {
330
+ if (a.type === 'pair') {
331
+ // `delegate :x, to: :y, prefix: true` generates differently named methods.
332
+ if (field(a, 'key')?.text === 'prefix') return false;
333
+ continue;
334
+ }
335
+ const lit = literal(a);
336
+ if (lit) names.push(lit);
337
+ if (!all) break;
338
+ }
339
+ if (names.length === 0) return false;
340
+ const sig = firstLine(ctx, node);
341
+ const doc = firstChildDoc(node);
342
+ for (const name of names) {
343
+ ctx.def(node, { name, kind, sig, doc, exported: !name.startsWith('_'), scope: false });
344
+ }
345
+ return true;
346
+ }
347
+
348
+ const attrCall: Dsl = (node, ctx) => defEach(node, ctx, 'property', true);
349
+ /** `has_many :comments, dependent: :destroy`: one association named by the first argument. */
350
+ const associationCall: Dsl = (node, ctx) => defEach(node, ctx, 'property', false);
351
+ const delegateCall: Dsl = (node, ctx) => defEach(node, ctx, 'method', true);
352
+ const aliasMethodCall: Dsl = (node, ctx) => defEach(node, ctx, 'method', false);
353
+
354
+ /** `define_method(:name) { ... }`, `scope :name, -> { ... }`: the block is the method body. */
355
+ const blockMethod: Dsl = (node, ctx, dsl) => {
356
+ if (!inType(ctx) || ctx.inCallable()) return false;
357
+ const list = args(node);
358
+ const first = list[0];
359
+ if (!first) return false;
360
+ if (dsl === 'scope' && (first.type !== 'simple_symbol' || list.length < 2)) return false;
361
+ const name = literal(first);
362
+ if (!name) return false;
363
+ ctx.def(node, {
364
+ name,
365
+ kind: 'method',
366
+ sig: ctx.src.slice(node.startIndex, first.endIndex) + (dsl === 'scope' ? '' : ')'),
367
+ doc: firstChildDoc(node),
368
+ exported: !name.startsWith('_'),
369
+ });
370
+ return true;
371
+ };
372
+
373
+ const DSL = new Map<string, Dsl>([
374
+ ['require', requireCall],
375
+ ['require_relative', requireCall],
376
+ ['load', requireCall],
377
+ ['autoload', autoloadCall],
378
+ ['attr_accessor', attrCall],
379
+ ['attr_reader', attrCall],
380
+ ['attr_writer', attrCall],
381
+ ['attr', attrCall],
382
+ ['attr_internal', attrCall],
383
+ ['mattr_accessor', attrCall],
384
+ ['mattr_reader', attrCall],
385
+ ['mattr_writer', attrCall],
386
+ ['cattr_accessor', attrCall],
387
+ ['cattr_reader', attrCall],
388
+ ['cattr_writer', attrCall],
389
+ ['class_attribute', attrCall],
390
+ ['thread_mattr_accessor', attrCall],
391
+ ['has_many', associationCall],
392
+ ['has_one', associationCall],
393
+ ['belongs_to', associationCall],
394
+ ['has_and_belongs_to_many', associationCall],
395
+ ['has_one_attached', associationCall],
396
+ ['has_many_attached', associationCall],
397
+ ['delegate', delegateCall],
398
+ ['alias_method', aliasMethodCall],
399
+ ['define_method', blockMethod],
400
+ ['scope', blockMethod],
401
+ ['include', consumed],
402
+ ['extend', consumed],
403
+ ['prepend', consumed],
404
+ ['private', consumed],
405
+ ['protected', consumed],
406
+ ['public', consumed],
407
+ ['module_function', consumed],
408
+ ['private_class_method', consumed],
409
+ ['public_class_method', consumed],
410
+ ['private_constant', consumed],
411
+ ]);
412
+
413
+ /** Receiver node types whose text is a useful, cheap hint. */
414
+ const SIMPLE_RECEIVERS = new Set([
415
+ 'identifier', 'self', 'instance_variable', 'class_variable', 'global_variable',
416
+ ]);
417
+
418
+ /** Nodes that bind local names: everything below them that is an identifier is a local variable. */
419
+ const BINDERS = new Set([
420
+ 'method_parameters', 'block_parameters', 'lambda_parameters', 'left_assignment_list', 'exception_variable',
421
+ 'destructured_left_assignment', 'rest_assignment',
422
+ ]);
423
+
424
+ /** Per file: method node id -> names that are local variables / parameters somewhere in that method. */
425
+ const methodLocals = new WeakMap<Ctx, Map<number, Set<string>>>();
426
+
427
+ function collectLocals(node: Node, out: Set<string>, binding: boolean, depth: number): void {
428
+ if (depth > 60) return;
429
+ if (node.type === 'identifier') {
430
+ if (binding) out.add(node.text);
431
+ return;
432
+ }
433
+ const binds = binding || BINDERS.has(node.type);
434
+ if (node.type === 'assignment' || node.type === 'operator_assignment') {
435
+ const left = field(node, 'left');
436
+ if (left && left.type === 'identifier') out.add(left.text);
437
+ } else if (node.type === 'for') {
438
+ const pattern = field(node, 'pattern');
439
+ if (pattern) collectLocals(pattern, out, true, depth + 1);
440
+ }
441
+ for (const c of node.namedChildren) if (c) collectLocals(c, out, binds, depth + 1);
442
+ }
443
+
444
+ /** Where a bare identifier is an expression (and so may be a method call with no arguments). */
445
+ const EXPRESSION_PARENTS = new Set([
446
+ 'body_statement', 'block_body', 'then', 'else', 'begin', 'ensure', 'do', 'binary', 'unary', 'argument_list',
447
+ 'return', 'if', 'unless', 'while', 'until', 'elsif', 'conditional', 'parenthesized_statements', 'pair', 'array',
448
+ 'interpolation', 'element_reference', 'if_modifier', 'unless_modifier', 'while_modifier', 'until_modifier',
449
+ 'rescue_modifier', 'when', 'case', 'not', 'range', 'splat_argument', 'hash_splat_argument', 'block_argument',
450
+ ]);
451
+
452
+ /**
453
+ * `save` / `_run_callbacks` written without receiver, arguments or parentheses is an identifier
454
+ * in the grammar: a call unless the method binds a local variable of that name.
455
+ */
456
+ const bareIdentifier: Visitor = (node, ctx) => {
457
+ const p = node.parent;
458
+ if (!p) return;
459
+ const name = node.text;
460
+ if (name.length < 2 || name === 'it' || /^_\d$/.test(name) || /^[A-Z]/.test(name)) return;
461
+ if (p.type === 'call') {
462
+ if (p.childForFieldName('receiver')?.id !== node.id) return; // the method name itself: handled by `call`
463
+ } else if (p.type === 'assignment' || p.type === 'operator_assignment') {
464
+ if (p.childForFieldName('right')?.id !== node.id) return;
465
+ } else if (!EXPRESSION_PARENTS.has(p.type)) {
466
+ return;
467
+ }
468
+ let method: Node | null = null;
469
+ for (let q: Node | null = p, i = 0; q && i < 60; q = q.parent, i++) {
470
+ if (q.type === 'method' || q.type === 'singleton_method') { method = q; break; }
471
+ }
472
+ if (!method) return; // class / file level: declarations and DSL, handled elsewhere
473
+ let byMethod = methodLocals.get(ctx);
474
+ if (!byMethod) methodLocals.set(ctx, (byMethod = new Map()));
475
+ let locals = byMethod.get(method.id);
476
+ if (!locals) {
477
+ locals = new Set<string>();
478
+ collectLocals(method, locals, false, 0);
479
+ byMethod.set(method.id, locals);
480
+ }
481
+ if (locals.has(name)) return;
482
+ ctx.call(name, node);
483
+ };
484
+
485
+ /** Name of the method a node is written in, or null at class / file level. */
486
+ function enclosingMethodName(node: Node): string | null {
487
+ for (let p = node.parent, i = 0; p && i < 40; p = p.parent, i++) {
488
+ if (p.type === 'method' || p.type === 'singleton_method') return field(p, 'name')?.text ?? null;
489
+ }
490
+ return null;
491
+ }
492
+
493
+ /** Bare `super` (arguments passed on implicitly): a call of the same method further up the ancestor chain. */
494
+ const superCall: Visitor = (node, ctx) => {
495
+ const p = node.parent;
496
+ if (p && p.type === 'call' && p.childForFieldName('method')?.id === node.id) return; // `super(args)`: handled as a call
497
+ const name = enclosingMethodName(node);
498
+ if (name) ctx.call(name, node, 'super');
499
+ };
500
+
501
+ const call: Visitor = (node, ctx) => {
502
+ const m = node.childForFieldName('method');
503
+ if (!m) return;
504
+ const name = m.text;
505
+ const recv = node.childForFieldName('receiver');
506
+ if (!recv && (m.type === 'super' || name === 'super')) {
507
+ const method = enclosingMethodName(node);
508
+ if (method) ctx.call(method, node, 'super');
509
+ return;
510
+ }
511
+ if (!recv) {
512
+ const dsl = DSL.get(name);
513
+ if (dsl && dsl(node, ctx, name)) return;
514
+ ctx.call(name, node);
515
+ return;
516
+ }
517
+ // An explicit receiver is always passed on (COMPLEX_RECEIVER for chains,
518
+ // literals, ...) so the call is never mistaken for an implicit-self call.
519
+ const rt = recv.type;
520
+ let receiver: string = COMPLEX_RECEIVER;
521
+ if (rt === 'constant') {
522
+ receiver = recv.text;
523
+ // Instantiation is how classes get used: `Foo.new` is also a call to `Foo`.
524
+ if (name === 'new') ctx.call(receiver, node);
525
+ } else if (rt === 'scope_resolution') {
526
+ if (recv.endIndex - recv.startIndex <= 40) receiver = recv.text.replace(/\s+/g, '');
527
+ if (name === 'new') ctx.call(field(recv, 'name')?.text, node);
528
+ } else if (SIMPLE_RECEIVERS.has(rt)) {
529
+ if (recv.endIndex - recv.startIndex <= 40) receiver = recv.text;
530
+ }
531
+ ctx.call(name, node, receiver);
532
+ };
533
+
534
+ export const ruby: LangSpec = {
535
+ id: 'ruby',
536
+ grammar: 'ruby',
537
+ extensions: ['.rb', '.rake', '.gemspec'],
538
+ identifiers: ['identifier', 'constant'],
539
+ comments: ['comment'],
540
+ visit: {
541
+ program,
542
+ class: classDef,
543
+ module: moduleDef,
544
+ singleton_class: singletonClass,
545
+ method: methodDef,
546
+ singleton_method: methodDef,
547
+ alias: aliasDef,
548
+ assignment,
549
+ operator_assignment: assignment,
550
+ call,
551
+ super: superCall,
552
+ identifier: bareIdentifier,
553
+ },
554
+ };