@nanonets/graft 0.12.1 → 0.15.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 (242) hide show
  1. package/README.md +20 -7
  2. package/dist/ai/crux.d.ts.map +1 -1
  3. package/dist/ai/crux.js +22 -1
  4. package/dist/ai/crux.js.map +1 -1
  5. package/dist/ai/llm/factory.d.ts +5 -1
  6. package/dist/ai/llm/factory.d.ts.map +1 -1
  7. package/dist/ai/llm/factory.js +8 -0
  8. package/dist/ai/llm/factory.js.map +1 -1
  9. package/dist/ai/llm/litellm.d.ts +34 -0
  10. package/dist/ai/llm/litellm.d.ts.map +1 -0
  11. package/dist/ai/llm/litellm.js +45 -0
  12. package/dist/ai/llm/litellm.js.map +1 -0
  13. package/dist/ai/llm/openai.d.ts.map +1 -1
  14. package/dist/ai/llm/openai.js +66 -7
  15. package/dist/ai/llm/openai.js.map +1 -1
  16. package/dist/ai/llm/recover-tool.d.ts +19 -0
  17. package/dist/ai/llm/recover-tool.d.ts.map +1 -0
  18. package/dist/ai/llm/recover-tool.js +109 -0
  19. package/dist/ai/llm/recover-tool.js.map +1 -0
  20. package/dist/ai/providers.d.ts.map +1 -1
  21. package/dist/ai/providers.js +2 -0
  22. package/dist/ai/providers.js.map +1 -1
  23. package/dist/ai/synthesize.d.ts.map +1 -1
  24. package/dist/ai/synthesize.js +19 -5
  25. package/dist/ai/synthesize.js.map +1 -1
  26. package/dist/app/checkout.d.ts +28 -0
  27. package/dist/app/checkout.d.ts.map +1 -0
  28. package/dist/app/checkout.js +100 -0
  29. package/dist/app/checkout.js.map +1 -0
  30. package/dist/app/events.d.ts +37 -0
  31. package/dist/app/events.d.ts.map +1 -0
  32. package/dist/app/events.js +59 -0
  33. package/dist/app/events.js.map +1 -0
  34. package/dist/app/identity.d.ts +54 -0
  35. package/dist/app/identity.d.ts.map +1 -0
  36. package/dist/app/identity.js +96 -0
  37. package/dist/app/identity.js.map +1 -0
  38. package/dist/app/main.d.ts +2 -0
  39. package/dist/app/main.d.ts.map +1 -0
  40. package/dist/app/main.js +56 -0
  41. package/dist/app/main.js.map +1 -0
  42. package/dist/app/pages.d.ts +39 -0
  43. package/dist/app/pages.d.ts.map +1 -0
  44. package/dist/app/pages.js +86 -0
  45. package/dist/app/pages.js.map +1 -0
  46. package/dist/app/queue.d.ts +40 -0
  47. package/dist/app/queue.d.ts.map +1 -0
  48. package/dist/app/queue.js +68 -0
  49. package/dist/app/queue.js.map +1 -0
  50. package/dist/app/review.d.ts +23 -0
  51. package/dist/app/review.d.ts.map +1 -0
  52. package/dist/app/review.js +129 -0
  53. package/dist/app/review.js.map +1 -0
  54. package/dist/app/server.d.ts +40 -0
  55. package/dist/app/server.d.ts.map +1 -0
  56. package/dist/app/server.js +117 -0
  57. package/dist/app/server.js.map +1 -0
  58. package/dist/ask/ask.d.ts +42 -0
  59. package/dist/ask/ask.d.ts.map +1 -1
  60. package/dist/ask/ask.js +442 -57
  61. package/dist/ask/ask.js.map +1 -1
  62. package/dist/ask/file-rank.d.ts +72 -0
  63. package/dist/ask/file-rank.d.ts.map +1 -0
  64. package/dist/ask/file-rank.js +160 -0
  65. package/dist/ask/file-rank.js.map +1 -0
  66. package/dist/ask/file-selection.d.ts +22 -0
  67. package/dist/ask/file-selection.d.ts.map +1 -0
  68. package/dist/ask/file-selection.js +66 -0
  69. package/dist/ask/file-selection.js.map +1 -0
  70. package/dist/ask/fuse.d.ts +16 -0
  71. package/dist/ask/fuse.d.ts.map +1 -1
  72. package/dist/ask/fuse.js +21 -4
  73. package/dist/ask/fuse.js.map +1 -1
  74. package/dist/ask/graphrank.d.ts +18 -0
  75. package/dist/ask/graphrank.d.ts.map +1 -1
  76. package/dist/ask/graphrank.js +65 -19
  77. package/dist/ask/graphrank.js.map +1 -1
  78. package/dist/blast/blast-cli.d.ts +5 -0
  79. package/dist/blast/blast-cli.d.ts.map +1 -1
  80. package/dist/blast/blast-cli.js +16 -1
  81. package/dist/blast/blast-cli.js.map +1 -1
  82. package/dist/blast/blast.d.ts +9 -0
  83. package/dist/blast/blast.d.ts.map +1 -1
  84. package/dist/blast/blast.js.map +1 -1
  85. package/dist/blast/evidence.d.ts +51 -0
  86. package/dist/blast/evidence.d.ts.map +1 -0
  87. package/dist/blast/evidence.js +162 -0
  88. package/dist/blast/evidence.js.map +1 -0
  89. package/dist/blast/owners.d.ts +85 -0
  90. package/dist/blast/owners.d.ts.map +1 -0
  91. package/dist/blast/owners.js +272 -0
  92. package/dist/blast/owners.js.map +1 -0
  93. package/dist/blast/render.d.ts +11 -2
  94. package/dist/blast/render.d.ts.map +1 -1
  95. package/dist/blast/render.js +125 -4
  96. package/dist/blast/render.js.map +1 -1
  97. package/dist/blast/viz.d.ts +20 -0
  98. package/dist/blast/viz.d.ts.map +1 -1
  99. package/dist/blast/viz.js +13 -157
  100. package/dist/blast/viz.js.map +1 -1
  101. package/dist/claude/format.js +1 -1
  102. package/dist/claude/format.js.map +1 -1
  103. package/dist/claude/hooks.d.ts.map +1 -1
  104. package/dist/claude/hooks.js +99 -11
  105. package/dist/claude/hooks.js.map +1 -1
  106. package/dist/claude/settings-merge.d.ts +11 -0
  107. package/dist/claude/settings-merge.d.ts.map +1 -1
  108. package/dist/claude/settings-merge.js +24 -10
  109. package/dist/claude/settings-merge.js.map +1 -1
  110. package/dist/claude/state.d.ts +14 -1
  111. package/dist/claude/state.d.ts.map +1 -1
  112. package/dist/claude/state.js +1 -1
  113. package/dist/claude/state.js.map +1 -1
  114. package/dist/claude/stats.d.ts.map +1 -1
  115. package/dist/claude/stats.js +2 -1
  116. package/dist/claude/stats.js.map +1 -1
  117. package/dist/claude/statusline.d.ts +5 -3
  118. package/dist/claude/statusline.d.ts.map +1 -1
  119. package/dist/claude/statusline.js +6 -4
  120. package/dist/claude/statusline.js.map +1 -1
  121. package/dist/claude/sync-run.d.ts.map +1 -1
  122. package/dist/claude/sync-run.js +10 -2
  123. package/dist/claude/sync-run.js.map +1 -1
  124. package/dist/claude/tally.d.ts +23 -0
  125. package/dist/claude/tally.d.ts.map +1 -0
  126. package/dist/claude/tally.js +138 -0
  127. package/dist/claude/tally.js.map +1 -0
  128. package/dist/cli.js +138 -1
  129. package/dist/cli.js.map +1 -1
  130. package/dist/context/build.d.ts.map +1 -1
  131. package/dist/context/build.js +23 -4
  132. package/dist/context/build.js.map +1 -1
  133. package/dist/context/check.d.ts.map +1 -1
  134. package/dist/context/check.js +2 -1
  135. package/dist/context/check.js.map +1 -1
  136. package/dist/context/node-file.d.ts +2 -1
  137. package/dist/context/node-file.d.ts.map +1 -1
  138. package/dist/context/node-file.js +37 -2
  139. package/dist/context/node-file.js.map +1 -1
  140. package/dist/engine.d.ts +2 -0
  141. package/dist/engine.d.ts.map +1 -1
  142. package/dist/engine.js +1 -0
  143. package/dist/engine.js.map +1 -1
  144. package/dist/graph/bindings.d.ts +25 -3
  145. package/dist/graph/bindings.d.ts.map +1 -1
  146. package/dist/graph/bindings.js +168 -2
  147. package/dist/graph/bindings.js.map +1 -1
  148. package/dist/graph/build.d.ts +4 -0
  149. package/dist/graph/build.d.ts.map +1 -1
  150. package/dist/graph/build.js +7 -4
  151. package/dist/graph/build.js.map +1 -1
  152. package/dist/graph/check.d.ts +3 -0
  153. package/dist/graph/check.d.ts.map +1 -1
  154. package/dist/graph/check.js +58 -11
  155. package/dist/graph/check.js.map +1 -1
  156. package/dist/graph/enrich.d.ts.map +1 -1
  157. package/dist/graph/enrich.js +29 -9
  158. package/dist/graph/enrich.js.map +1 -1
  159. package/dist/graph/extract.d.ts +22 -2
  160. package/dist/graph/extract.d.ts.map +1 -1
  161. package/dist/graph/extract.js +1444 -44
  162. package/dist/graph/extract.js.map +1 -1
  163. package/dist/graph/fingerprint.d.ts +6 -1
  164. package/dist/graph/fingerprint.d.ts.map +1 -1
  165. package/dist/graph/fingerprint.js +7 -3
  166. package/dist/graph/fingerprint.js.map +1 -1
  167. package/dist/graph/generic.d.ts.map +1 -1
  168. package/dist/graph/generic.js +39 -7
  169. package/dist/graph/generic.js.map +1 -1
  170. package/dist/graph/invariants.d.ts +3 -2
  171. package/dist/graph/invariants.d.ts.map +1 -1
  172. package/dist/graph/invariants.js +6 -3
  173. package/dist/graph/invariants.js.map +1 -1
  174. package/dist/graph/queries/dart.scm +42 -0
  175. package/dist/graph/queries/lua.scm +36 -0
  176. package/dist/graph/queries/nix.scm +32 -0
  177. package/dist/graph/refresh.d.ts.map +1 -1
  178. package/dist/graph/refresh.js +6 -2
  179. package/dist/graph/refresh.js.map +1 -1
  180. package/dist/graph/resolve.d.ts +1 -1
  181. package/dist/graph/resolve.d.ts.map +1 -1
  182. package/dist/graph/resolve.js +211 -26
  183. package/dist/graph/resolve.js.map +1 -1
  184. package/dist/graph/scopes.d.ts.map +1 -1
  185. package/dist/graph/scopes.js +2 -1
  186. package/dist/graph/scopes.js.map +1 -1
  187. package/dist/graph/source-files.d.ts +7 -2
  188. package/dist/graph/source-files.d.ts.map +1 -1
  189. package/dist/graph/source-files.js +19 -6
  190. package/dist/graph/source-files.js.map +1 -1
  191. package/dist/graph/traverse-cli.d.ts +13 -2
  192. package/dist/graph/traverse-cli.d.ts.map +1 -1
  193. package/dist/graph/traverse-cli.js +25 -4
  194. package/dist/graph/traverse-cli.js.map +1 -1
  195. package/dist/graph/workspace-cli.d.ts +2 -0
  196. package/dist/graph/workspace-cli.d.ts.map +1 -1
  197. package/dist/graph/workspace-cli.js +3 -0
  198. package/dist/graph/workspace-cli.js.map +1 -1
  199. package/dist/graph/workspace.d.ts.map +1 -1
  200. package/dist/graph/workspace.js +26 -2
  201. package/dist/graph/workspace.js.map +1 -1
  202. package/dist/hosts/mcp-config.d.ts +12 -0
  203. package/dist/hosts/mcp-config.d.ts.map +1 -1
  204. package/dist/hosts/mcp-config.js +54 -5
  205. package/dist/hosts/mcp-config.js.map +1 -1
  206. package/dist/hosts/registry.d.ts.map +1 -1
  207. package/dist/hosts/registry.js +20 -0
  208. package/dist/hosts/registry.js.map +1 -1
  209. package/dist/hosts/retract.d.ts +47 -0
  210. package/dist/hosts/retract.d.ts.map +1 -0
  211. package/dist/hosts/retract.js +473 -0
  212. package/dist/hosts/retract.js.map +1 -0
  213. package/dist/ingest/fs.d.ts +19 -5
  214. package/dist/ingest/fs.d.ts.map +1 -1
  215. package/dist/ingest/fs.js +35 -9
  216. package/dist/ingest/fs.js.map +1 -1
  217. package/dist/telemetry/contract.d.ts.map +1 -1
  218. package/dist/telemetry/contract.js +7 -2
  219. package/dist/telemetry/contract.js.map +1 -1
  220. package/dist/telemetry/sessions.d.ts.map +1 -1
  221. package/dist/telemetry/sessions.js +6 -0
  222. package/dist/telemetry/sessions.js.map +1 -1
  223. package/dist/util/state.d.ts +25 -2
  224. package/dist/util/state.d.ts.map +1 -1
  225. package/dist/util/state.js +24 -2
  226. package/dist/util/state.js.map +1 -1
  227. package/dist/viewer/app.js +2 -1
  228. package/dist/viewer/style.css +14 -0
  229. package/dist/viz/assemble.d.ts +17 -0
  230. package/dist/viz/assemble.d.ts.map +1 -1
  231. package/dist/viz/assemble.js.map +1 -1
  232. package/package.json +19 -4
  233. package/scripts/graph-quality.mjs +3 -3
  234. package/scripts/tally-audit.mjs +140 -0
  235. package/dist/ai/llm/claude-cli.d.ts +0 -60
  236. package/dist/ai/llm/claude-cli.d.ts.map +0 -1
  237. package/dist/ai/llm/claude-cli.js +0 -417
  238. package/dist/ai/llm/claude-cli.js.map +0 -1
  239. package/dist/util/num.d.ts +0 -3
  240. package/dist/util/num.d.ts.map +0 -1
  241. package/dist/util/num.js +0 -20
  242. package/dist/util/num.js.map +0 -1
@@ -10,7 +10,10 @@ import Parser from "tree-sitter";
10
10
  import TypeScript from "tree-sitter-typescript";
11
11
  import Python from "tree-sitter-python";
12
12
  import Go from "tree-sitter-go";
13
+ import R from "tree-sitter-r";
13
14
  import Java from "tree-sitter-java";
15
+ import Kotlin from "tree-sitter-kotlin";
16
+ import Swift from "tree-sitter-swift";
14
17
  import PHP from "tree-sitter-php";
15
18
  import { basename } from "node:path";
16
19
  import { contentHash } from "../util/id.js";
@@ -43,7 +46,13 @@ const EXTENSIONS = [
43
46
  { ext: ".py", grammar: "python", label: "python" },
44
47
  { ext: ".go", grammar: "go", label: "go" },
45
48
  { ext: ".java", grammar: "java", label: "java" },
49
+ { ext: ".kt", grammar: "kotlin", label: "kotlin" },
50
+ { ext: ".kts", grammar: "kotlin", label: "kotlin" },
51
+ { ext: ".swift", grammar: "swift", label: "swift" },
46
52
  { ext: ".php", grammar: "php", label: "php" },
53
+ // `entryFor` lower-cases the path before matching, so this one entry covers
54
+ // both `.R` (the conventional case in real R codebases) and `.r`.
55
+ { ext: ".r", grammar: "r", label: "r" },
47
56
  ];
48
57
  function entryFor(path) {
49
58
  const p = path.toLowerCase();
@@ -124,6 +133,11 @@ const GO_KINDS = {
124
133
  function_declaration: "function",
125
134
  method_declaration: "method",
126
135
  };
136
+ // R: `function_definition` carries no name field at all (unlike every other
137
+ // supported language) — its identifier always comes from context (an
138
+ // assignment's other side), resolved dynamically in describeR(). Empty, like
139
+ // Go's own table — never consulted, kept only to satisfy KINDS_BY_LANG's type.
140
+ const R_KINDS = {};
127
141
  // Java: a record is a nominal data carrier, so it takes "struct" — the same role
128
142
  // Go's struct plays — rather than "class", which would make a service and a DTO
129
143
  // indistinguishable in a repo where DTOs are most of the type surface.
@@ -140,6 +154,41 @@ const JAVA_KINDS = {
140
154
  /** Java type declarations: they set `enclosingClass` for the methods nested in them,
141
155
  * which "class"-only logic would miss for a record's or interface's members. */
142
156
  const JAVA_TYPE_KINDS = new Set(["class", "interface", "enum", "struct"]);
157
+ const KOTLIN_KINDS = {
158
+ class_declaration: "class", // → "interface" / "enum" / "interface" (annotation) in describeKotlin
159
+ object_declaration: "class", // a singleton object is class-like (companion objects included)
160
+ function_declaration: "function", // → "method" inside a type (resolved in the walk)
161
+ secondary_constructor: "method", // the class's own secondary constructor
162
+ type_alias: "type",
163
+ property_declaration: "variable", // top-level `val`/`var` only (fields resolved in the walk)
164
+ };
165
+ /** Kotlin type declarations: they set `enclosingClass` for the members nested in them.
166
+ * "class" also covers object_declaration (it maps to "class"); interface/enum are the
167
+ * same class_declaration node rekinded in describeKotlin, so all three land in the set. */
168
+ const KOTLIN_TYPE_KINDS = new Set(["class", "interface", "enum"]);
169
+ const SWIFT_KINDS = {
170
+ class_declaration: "class", // → "struct" / "enum" in describeSwift (one node type covers all five keywords)
171
+ protocol_declaration: "interface",
172
+ function_declaration: "function", // → "method" inside a type (resolved in the walk)
173
+ protocol_function_declaration: "method", // a protocol requirement is always a member
174
+ init_declaration: "method", // the type's own initializer (named after it, like a Java constructor)
175
+ typealias_declaration: "type",
176
+ property_declaration: "variable", // top-level `let`/`var` only (fields resolved in the walk)
177
+ };
178
+ /** Swift type declarations: they set `enclosingClass` for the members nested in them.
179
+ * class/struct/enum are the same class_declaration node rekinded in describeSwift
180
+ * (an actor takes "class"); protocols are "interface". "module" is an extension
181
+ * body — a member-contributing scope named after the extended type, deliberately
182
+ * NOT a type kind in the graph so it can never make the real declaration's name
183
+ * ambiguous (see describeSwift's extension branch) — but its members still
184
+ * promote to methods owned by that type, which is why it belongs in this set. */
185
+ const SWIFT_TYPE_KINDS = new Set([
186
+ "class",
187
+ "struct",
188
+ "enum",
189
+ "interface",
190
+ "module",
191
+ ]);
143
192
  // PHP: definition node types are all distinct (no py-style function→method
144
193
  // promotion needed — a class body uses `method_declaration`, not
145
194
  // `function_definition`). `trait_declaration` maps to the PHP-only `trait` kind.
@@ -156,7 +205,10 @@ const KINDS_BY_LANG = {
156
205
  tsx: TS_KINDS,
157
206
  python: PY_KINDS,
158
207
  go: GO_KINDS,
208
+ r: R_KINDS,
159
209
  java: JAVA_KINDS,
210
+ kotlin: KOTLIN_KINDS,
211
+ swift: SWIFT_KINDS,
160
212
  php: PHP_KINDS,
161
213
  };
162
214
  /**
@@ -174,12 +226,15 @@ const CALL_TYPES = {
174
226
  python: new Set(["call"]),
175
227
  go: new Set(["call_expression"]),
176
228
  java: new Set(["method_invocation", "object_creation_expression"]),
229
+ kotlin: new Set(["call_expression"]),
230
+ swift: new Set(["call_expression"]),
177
231
  php: new Set([
178
232
  "function_call_expression",
179
233
  "member_call_expression",
180
234
  "nullsafe_member_call_expression",
181
235
  "scoped_call_expression",
182
236
  ]),
237
+ r: new Set(["call"]),
183
238
  };
184
239
  const FUNCTION_VALUE_TYPES = new Set([
185
240
  "arrow_function",
@@ -187,13 +242,17 @@ const FUNCTION_VALUE_TYPES = new Set([
187
242
  "function_expression",
188
243
  "generator_function",
189
244
  ]);
245
+ const EMPTY_SET = new Set();
190
246
  const parser = new Parser();
191
247
  const GRAMMARS = {
192
248
  typescript: TypeScript.typescript,
193
249
  tsx: TypeScript.tsx,
194
250
  python: Python,
195
251
  go: Go,
252
+ r: R,
196
253
  java: Java,
254
+ kotlin: Kotlin,
255
+ swift: Swift,
197
256
  php: PHP.php,
198
257
  };
199
258
  /** tree-sitter's string `parse()` fails with "Invalid argument" on any input
@@ -210,6 +269,7 @@ export function extractFile(rel, source, lang) {
210
269
  const root = parseSource(source);
211
270
  const bindings = collectBindings(root, lang);
212
271
  const importedSymbols = collectImportedSymbols(root, lang);
272
+ const rGenerics = lang === "r" ? collectRGenerics(root) : EMPTY_SET;
213
273
  const nodes = [
214
274
  {
215
275
  id: rel,
@@ -240,6 +300,9 @@ export function extractFile(rel, source, lang) {
240
300
  enclosingClass: null,
241
301
  goReceiverVar: null,
242
302
  importedSymbols,
303
+ rR6Access: null,
304
+ rGenerics,
305
+ rSuperClass: null,
243
306
  };
244
307
  // Every id minted this file, seeded with the file node's own id (`rel`) so a
245
308
  // top-level definition can never collide with it. Threaded as its own
@@ -247,8 +310,7 @@ export function extractFile(rel, source, lang) {
247
310
  // childCtx, so a by-ref Set there would read as ordinary inherited context
248
311
  // when it's actually accidental shared mutable state across the whole walk.
249
312
  const minted = new Set([rel]);
250
- for (const child of root.namedChildren)
251
- walk(child, ctx, nodes, rawEdges, minted);
313
+ walkNamedChildren(root.namedChildren, ctx, nodes, rawEdges, minted);
252
314
  // nodes[0] is the file node; the rest are its symbols. Index the module-level
253
315
  // residual on the file node so a term outside every symbol still surfaces it.
254
316
  nodes[0].body_text = fileResidual(source, nodes.slice(1));
@@ -268,6 +330,97 @@ export function mintId(base, minted) {
268
330
  minted.add(id);
269
331
  return id;
270
332
  }
333
+ /**
334
+ * tree-sitter-php 0.23.x cannot parse a `const` inside an enum body (#145). An
335
+ * array initializer collapses the whole `enum_declaration` into ERROR; the
336
+ * method is recovered as a sibling `function_definition`. 0.24.2 parses this
337
+ * natively but is ABI 15 and cannot load on Graft's tree-sitter 0.21.1.
338
+ *
339
+ * Bound: only an ERROR that already contains `enum_case` + `name` is treated as
340
+ * a collapsed enum. Only `const_declaration` / `function_definition` /
341
+ * `method_declaration` / ERROR siblings are absorbed, stopping at a `}` ERROR.
342
+ * Unknown ERROR nodes are still walked, never mapped to a type. Clean
343
+ * class/enum trees are `class_declaration` / `enum_declaration` and skip this.
344
+ */
345
+ function phpCollapsedEnumName(node) {
346
+ if (node.type !== "ERROR")
347
+ return null;
348
+ if (!node.namedChildren.some((c) => c.type === "enum_case"))
349
+ return null;
350
+ return node.namedChildren.find((c) => c.type === "name")?.text ?? null;
351
+ }
352
+ function phpCollapsedEnumHold(node) {
353
+ return (node.type === "const_declaration" ||
354
+ node.type === "function_definition" ||
355
+ node.type === "method_declaration" ||
356
+ node.type === "ERROR");
357
+ }
358
+ function phpCollapsedEnumClose(node) {
359
+ return node.type === "ERROR" && node.text.trim() === "}";
360
+ }
361
+ function walkNamedChildren(children, ctx, out, edges, minted) {
362
+ if (ctx.lang !== "php") {
363
+ for (const child of children)
364
+ walk(child, ctx, out, edges, minted);
365
+ return;
366
+ }
367
+ for (let i = 0; i < children.length;) {
368
+ const n = children[i];
369
+ const enumName = phpCollapsedEnumName(n);
370
+ if (enumName) {
371
+ const group = [n];
372
+ let j = i + 1;
373
+ while (j < children.length && phpCollapsedEnumHold(children[j])) {
374
+ const next = children[j];
375
+ group.push(next);
376
+ j++;
377
+ if (phpCollapsedEnumClose(next))
378
+ break;
379
+ }
380
+ emitPhpCollapsedEnum(enumName, n, group, ctx, out, edges, minted);
381
+ i = j;
382
+ continue;
383
+ }
384
+ walk(n, ctx, out, edges, minted);
385
+ i++;
386
+ }
387
+ }
388
+ function emitPhpCollapsedEnum(name, errorNode, group, ctx, out, edges, minted) {
389
+ const last = group[group.length - 1];
390
+ const id = mintId(`${ctx.rel}#${[...ctx.scope, name].join(".")}`, minted);
391
+ const body = ctx.source.slice(errorNode.startIndex, last.endIndex);
392
+ out.push({
393
+ id,
394
+ name,
395
+ kind: "enum",
396
+ path: ctx.rel,
397
+ span: `L${errorNode.startPosition.row + 1}-L${last.endPosition.row + 1}`,
398
+ signature: `enum ${name}`,
399
+ exported: true,
400
+ origin: "ast",
401
+ body_hash: contentHash(body),
402
+ body_text: searchBody(body),
403
+ summary_state: "pending",
404
+ summary: null,
405
+ crux: null,
406
+ });
407
+ edges.push({ source: ctx.parentId, relation: "contains", targetId: id, file: ctx.rel });
408
+ const childCtx = {
409
+ ...ctx,
410
+ scope: [...ctx.scope, name],
411
+ enclosingKind: "enum",
412
+ parentId: id,
413
+ };
414
+ for (const g of group) {
415
+ if (phpCollapsedEnumClose(g))
416
+ continue;
417
+ if (phpCollapsedEnumName(g)) {
418
+ walkNamedChildren(g.namedChildren, childCtx, out, edges, minted);
419
+ continue;
420
+ }
421
+ walk(g, childCtx, out, edges, minted);
422
+ }
423
+ }
271
424
  function walk(node, ctx, out, edges, minted) {
272
425
  const desc = describe(node, ctx);
273
426
  if (desc) {
@@ -279,10 +432,14 @@ function walk(node, ctx, out, edges, minted) {
279
432
  const isGoMethod = ctx.lang === "go" && node.type === "method_declaration";
280
433
  // The bare name of this node's OWN immediate enclosing class/receiver — for a
281
434
  // Go method that's its receiver type (methods aren't nested, so ctx.enclosingClass
282
- // wouldn't see it); for every other method it's simply what the nearest ancestor
283
- // class already set as ctx.enclosingClass. Only method nodes carry it — resolve.ts's
435
+ // wouldn't see it); for an R S3/S4 method it's the qualifier/class describeR
436
+ // already resolved (desc.owner — these don't lexically nest inside their class
437
+ // either); for every other method it's simply what the nearest ancestor class
438
+ // already set as ctx.enclosingClass. Only method nodes carry it — resolve.ts's
284
439
  // ownerMethod index is the sole consumer (see NodeV1.owner's doc comment).
285
- const owner = desc.kind === "method" ? (isGoMethod ? (goReceiverType(node) ?? undefined) : (ctx.enclosingClass ?? undefined)) : undefined;
440
+ const owner = desc.kind === "method"
441
+ ? (isGoMethod ? (goReceiverType(node) ?? undefined) : (desc.owner ?? ctx.enclosingClass ?? undefined))
442
+ : undefined;
286
443
  out.push({
287
444
  id,
288
445
  name: desc.name,
@@ -294,11 +451,17 @@ function walk(node, ctx, out, edges, minted) {
294
451
  ? !desc.name.startsWith("_")
295
452
  : ctx.lang === "go"
296
453
  ? goExported(desc.name)
297
- : ctx.lang === "java"
298
- ? javaExported(node)
299
- : ctx.lang === "php"
300
- ? phpExported(node)
301
- : tsExported(node),
454
+ : ctx.lang === "r"
455
+ ? rExported(desc.name, ctx, node)
456
+ : ctx.lang === "java"
457
+ ? javaExported(node)
458
+ : ctx.lang === "kotlin"
459
+ ? kotlinExported(node)
460
+ : ctx.lang === "swift"
461
+ ? swiftExported(node)
462
+ : ctx.lang === "php"
463
+ ? phpExported(node)
464
+ : tsExported(node),
302
465
  origin: "ast",
303
466
  body_hash: contentHash(desc.hashNode.text),
304
467
  body_text: searchBody(desc.hashNode.text),
@@ -314,13 +477,19 @@ function walk(node, ctx, out, edges, minted) {
314
477
  // class heritage — in Java an interface may also `extends`, and a record/enum
315
478
  // may `implements`, so every type declaration is a heritage site, not just a class.
316
479
  const javaTypeDecl = ctx.lang === "java" && JAVA_TYPE_KINDS.has(desc.kind);
317
- if (desc.kind === "class" || javaTypeDecl)
480
+ const kotlinTypeDecl = ctx.lang === "kotlin" && KOTLIN_TYPE_KINDS.has(desc.kind);
481
+ const swiftTypeDecl = ctx.lang === "swift" && SWIFT_TYPE_KINDS.has(desc.kind);
482
+ if (desc.kind === "class" || javaTypeDecl || kotlinTypeDecl || swiftTypeDecl)
318
483
  edges.push(...heritageEdges(node, id, ctx));
319
- const enclosingClass = desc.kind === "class" || javaTypeDecl
484
+ if (ctx.lang === "php")
485
+ edges.push(...phpAttributeReferenceEdges(node, id, ctx));
486
+ if (ctx.lang === "java")
487
+ edges.push(...javaAnnotationReferenceEdges(node, id, ctx));
488
+ const enclosingClass = desc.kind === "class" || javaTypeDecl || kotlinTypeDecl || swiftTypeDecl
320
489
  ? desc.name
321
490
  : isGoMethod
322
491
  ? goReceiverType(node)
323
- : ctx.enclosingClass;
492
+ : (desc.owner ?? ctx.enclosingClass);
324
493
  const childCtx = {
325
494
  ...ctx,
326
495
  scope: [...ctx.scope, idPart],
@@ -331,15 +500,79 @@ function walk(node, ctx, out, edges, minted) {
331
500
  importedSymbols: desc.kind === "function" || desc.kind === "method"
332
501
  ? withoutShadowedImports(ctx.importedSymbols, node)
333
502
  : ctx.importedSymbols,
503
+ // Reset on every new definition — this is a purely local marker for "we're
504
+ // still inside THIS class-defining call's own public=/private=/active=
505
+ // argument chain," not something that should leak into a nested definition
506
+ // (a method's own body, or — vanishingly rare but possible — another class
507
+ // defined inside one).
508
+ rR6Access: null,
509
+ // Unlike rR6Access, only reset when entering a genuinely new class (so it
510
+ // stays live through a method's whole body, where super$ / super. calls
511
+ // actually happen) — inherited unchanged for every other definition kind.
512
+ // Swift reads it off the declaration's own `:` clause, so `super.ping()`
513
+ // resolves against the PARENT type, not the overriding current one.
514
+ rSuperClass: desc.kind === "class"
515
+ ? ctx.lang === "r"
516
+ ? rR6ParentClass(node)
517
+ : ctx.lang === "swift"
518
+ ? swiftSuperClassName(node)
519
+ : null
520
+ : ctx.rSuperClass,
334
521
  };
335
- for (const child of node.namedChildren)
336
- walk(child, childCtx, out, edges, minted);
522
+ walkNamedChildren(node.namedChildren, childCtx, out, edges, minted);
337
523
  return;
338
524
  }
525
+ // R6 (Phase 2): `public =`/`private =`/`active =` inside an R6Class(...) call's
526
+ // own arguments is a `list(...)` call whose named entries become methods —
527
+ // this is R's version of a class body, but structurally it's several levels of
528
+ // ordinary call/argument nodes rather than a dedicated grammar construct, so it
529
+ // needs its own interception (mirrors how every other stateful/pattern-matched
530
+ // R construct in this walk needs one). `ctx.enclosingKind === "class"` scopes
531
+ // this to the class-defining call's own direct structure — once we're inside
532
+ // an actual method's body, enclosingKind has moved on to "method" and an
533
+ // unrelated nested `list(public = list(fn = function() {}))` elsewhere won't
534
+ // be misread as another class body.
535
+ if (ctx.lang === "r" &&
536
+ ctx.enclosingKind === "class" &&
537
+ ctx.rR6Access === null &&
538
+ node.type === "argument") {
539
+ const argName = node.childForFieldName("name");
540
+ const value = node.childForFieldName("value");
541
+ if (argName?.type === "identifier" &&
542
+ (argName.text === "public" || argName.text === "private" || argName.text === "active") &&
543
+ value?.type === "call" &&
544
+ rCalleeName(value) === "list") {
545
+ const access = argName.text;
546
+ for (const entry of rCallArgs(value)) {
547
+ walk(entry, { ...ctx, rR6Access: access }, out, edges, minted);
548
+ }
549
+ return;
550
+ }
551
+ }
339
552
  // not a definition — capture calls/imports/references, then descend with the same context
553
+ // R's `call` node is also its ONLY vehicle for library()/require()/source() —
554
+ // there's no separate import-statement grammar construct to key off, so isImport
555
+ // must be checked before the generic calls path or every import call would be
556
+ // captured as a (harmlessly unresolvable, but wrong) `calls` edge instead.
340
557
  const callTypes = CALL_TYPES[ctx.lang];
341
- if (callTypes.has(node.type)) {
342
- const callee = calleeName(node, ctx.lang);
558
+ if (isImport(node, ctx.lang)) {
559
+ const spec = importSpecifier(node, ctx.lang);
560
+ if (spec)
561
+ edges.push({ source: ctx.rel, relation: "imports", specifier: spec, file: ctx.rel });
562
+ // Imported identifiers are declarations, not uses. The import-binding pass
563
+ // above already recorded them, so do not descend and emit false references.
564
+ return;
565
+ }
566
+ else if (callTypes.has(node.type)) {
567
+ // R6Class(...) / a Phase-5 mixin list(...) is already consumed by its
568
+ // enclosing binary_operator as the class definition (see describeR) — the
569
+ // walk still reaches this SAME call node again, recursing generically to
570
+ // find its public=/private=/active= arguments (there's no other path to
571
+ // them), and it must not ALSO be treated as an ordinary call to a
572
+ // function literally named "R6Class"/"list".
573
+ const consumedCallee = ctx.lang === "r" && node.type === "call" ? rCalleeName(node) : null;
574
+ const isConsumedRClassCall = consumedCallee === "R6Class" || (consumedCallee === "list" && rIsMixinContainer(node));
575
+ const callee = isConsumedRClassCall ? null : calleeName(node, ctx.lang);
343
576
  if (callee) {
344
577
  const callEdge = {
345
578
  source: ctx.parentId,
@@ -347,23 +580,48 @@ function walk(node, ctx, out, edges, minted) {
347
580
  name: callee.name,
348
581
  viaMember: callee.viaMember,
349
582
  file: ctx.rel,
583
+ ...(callee.kinds ? { kinds: callee.kinds } : {}),
350
584
  };
351
- // Java only: the call site's argument count, to pick the right overload.
352
- const argCount = ctx.lang === "java" ? javaArgCount(node) : undefined;
585
+ // Overloading languages: the call site's argument count, to pick the right
586
+ // overload (see RawEdge.argCount).
587
+ const argCount = ctx.lang === "java" ? javaArgCount(node) : ctx.lang === "swift" ? swiftArgCount(node) : undefined;
353
588
  if (argCount !== undefined)
354
589
  callEdge.argCount = argCount;
355
- const recvType = resolveRecvType(callee.receiver, ctx);
356
- edges.push(recvType ? { ...callEdge, recvType } : callEdge);
590
+ // Swift: a bare lowercase call inside a type body may be an implicit-`self`
591
+ // member call (`walk()` for `self.walk()`), syntactically indistinguishable
592
+ // from a free-function call — and Swift's own lookup is member-FIRST (inner
593
+ // scope wins). So the edge is emitted as the member reading, typed to the
594
+ // enclosing class — resolved through the owner-qualified method index and
595
+ // the class's in-repo ancestor chain (`clearLogs()` in a test subclass
596
+ // finds the base class's method) — with `implicitSelf` letting resolve.ts
597
+ // fall back to the free-function reading only when no member exists on the
598
+ // chain. One edge, both readings, language-order precedence. This is
599
+ // deliberately NOT a bare-name kind widening: dogfooding on
600
+ // swift-composable-architecture, a global unique-name match bound
601
+ // `contains(element)` inside `extension Set` — a stdlib call — to an
602
+ // unrelated type's only in-repo `contains`. And not for an UpperCamelCase
603
+ // callee: that is an initializer call (`Text("hi")`), which takes
604
+ // resolve.ts's class/struct/enum fallback instead — extension nodes (kind
605
+ // "module") can never false-match it.
606
+ const swiftImplicitSelf = ctx.lang === "swift" &&
607
+ !callee.viaMember &&
608
+ !callee.kinds &&
609
+ ctx.enclosingClass &&
610
+ !/^[A-Z]/.test(callee.name);
611
+ if (swiftImplicitSelf) {
612
+ edges.push({
613
+ ...callEdge,
614
+ viaMember: true,
615
+ recvType: ctx.enclosingClass,
616
+ implicitSelf: true,
617
+ });
618
+ }
619
+ else {
620
+ const recvType = resolveRecvType(callee.receiver, ctx);
621
+ edges.push(recvType ? { ...callEdge, recvType } : callEdge);
622
+ }
357
623
  }
358
624
  }
359
- else if (isImport(node, ctx.lang)) {
360
- const spec = importSpecifier(node, ctx.lang);
361
- if (spec)
362
- edges.push({ source: ctx.rel, relation: "imports", specifier: spec, file: ctx.rel });
363
- // Imported identifiers are declarations, not uses. The import-binding pass
364
- // above already recorded them, so do not descend and emit false references.
365
- return;
366
- }
367
625
  else if (ctx.lang === "php" && node.type === "use_declaration") {
368
626
  // Trait composition inside a class body (`use HasFactory, Notifiable;`).
369
627
  // Modelled as `implements`: like an interface, a trait is a contract of
@@ -389,6 +647,54 @@ function walk(node, ctx, out, edges, minted) {
389
647
  });
390
648
  }
391
649
  }
650
+ // Java anonymous class (`new Type() { … }`): tree-sitter-java has no
651
+ // `anonymous_class` node (unlike PHP) — the body is an optional `class_body`
652
+ // on `object_creation_expression`. Mint `{anonymous}` (mirroring PHP #144 /
653
+ // `{closure}`) so nested methods take that owner instead of the enclosing
654
+ // type's, which otherwise pollutes `ownerMethod` and steals real call edges
655
+ // (#161). The constructor call edge above still fires for `new Type()`.
656
+ if (ctx.lang === "java" && node.type === "object_creation_expression") {
657
+ const body = node.namedChildren.find((c) => c.type === "class_body");
658
+ if (body) {
659
+ const idPart = "{anonymous}";
660
+ const base = `${ctx.rel}#${[...ctx.scope, idPart].join(".")}`;
661
+ const id = mintId(base, minted);
662
+ out.push({
663
+ id,
664
+ name: "{anonymous}",
665
+ kind: "class",
666
+ path: ctx.rel,
667
+ span: `L${node.startPosition.row + 1}-L${node.endPosition.row + 1}`,
668
+ signature: clean(ctx.source.slice(node.startIndex, body.startIndex)),
669
+ exported: false,
670
+ origin: "ast",
671
+ body_hash: contentHash(node.text),
672
+ body_text: searchBody(node.text),
673
+ summary_state: "pending",
674
+ summary: null,
675
+ crux: null,
676
+ });
677
+ edges.push({ source: ctx.parentId, relation: "contains", targetId: id, file: ctx.rel });
678
+ // Single supertype from `new Type()`: emit `implements` so an interface
679
+ // target resolves (adapters are the common case; a class target drops
680
+ // under resolve's implements kind filter — drop-not-guess).
681
+ const superName = javaConstructedTypeName(node.childForFieldName("type"));
682
+ if (superName) {
683
+ edges.push({ source: id, relation: "implements", name: superName, file: ctx.rel });
684
+ }
685
+ const anonCtx = {
686
+ ...ctx,
687
+ scope: [...ctx.scope, idPart],
688
+ enclosingKind: "class",
689
+ parentId: id,
690
+ enclosingClass: "{anonymous}",
691
+ };
692
+ for (const child of node.namedChildren) {
693
+ walk(child, child.type === "class_body" ? anonCtx : ctx, out, edges, minted);
694
+ }
695
+ return;
696
+ }
697
+ }
392
698
  for (const child of node.namedChildren)
393
699
  walk(child, ctx, out, edges, minted);
394
700
  }
@@ -398,15 +704,32 @@ function walk(node, ctx, out, edges, minted) {
398
704
  * the exported symbol name, so wiring them would require guessing.
399
705
  */
400
706
  function collectImportedSymbols(root, lang) {
401
- const out = new Map();
402
- if (lang !== "typescript" && lang !== "tsx")
707
+ if (lang === "typescript" || lang === "tsx") {
708
+ const out = new Map();
709
+ const visit = (node) => {
710
+ if (node.type === "import_statement") {
711
+ const specifier = importSpecifier(node, lang);
712
+ if (!specifier)
713
+ return;
714
+ collectTsImportBindings(node, specifier, out);
715
+ return;
716
+ }
717
+ for (const child of node.namedChildren)
718
+ visit(child);
719
+ };
720
+ visit(root);
403
721
  return out;
722
+ }
723
+ if (lang === "php")
724
+ return collectPhpImportedSymbols(root);
725
+ return new Map();
726
+ }
727
+ /** PHP `use` bindings: local alias → { exported name, FQN specifier }. */
728
+ function collectPhpImportedSymbols(root) {
729
+ const out = new Map();
404
730
  const visit = (node) => {
405
- if (node.type === "import_statement") {
406
- const specifier = importSpecifier(node, lang);
407
- if (!specifier)
408
- return;
409
- collectTsImportBindings(node, specifier, out);
731
+ if (node.type === "namespace_use_declaration") {
732
+ collectPhpUseDeclaration(node, out);
410
733
  return;
411
734
  }
412
735
  for (const child of node.namedChildren)
@@ -415,6 +738,123 @@ function collectImportedSymbols(root, lang) {
415
738
  visit(root);
416
739
  return out;
417
740
  }
741
+ function collectPhpUseDeclaration(decl, out) {
742
+ const prefix = decl.namedChildren.find((c) => c.type === "namespace_name")?.text.replace(/\\$/, "") ?? "";
743
+ const clauses = [];
744
+ for (const child of decl.namedChildren) {
745
+ if (child.type === "namespace_use_clause")
746
+ clauses.push(child);
747
+ if (child.type === "namespace_use_group") {
748
+ for (const c of child.namedChildren) {
749
+ if (c.type === "namespace_use_clause")
750
+ clauses.push(c);
751
+ }
752
+ }
753
+ }
754
+ for (const clause of clauses) {
755
+ const binding = phpUseClauseBinding(clause, prefix);
756
+ if (binding)
757
+ out.set(binding.local, { name: binding.name, specifier: binding.specifier });
758
+ }
759
+ }
760
+ function phpUseClauseBinding(clause, prefix) {
761
+ const names = clause.namedChildren.filter((c) => c.type === "name");
762
+ const qualified = clause.namedChildren.find((c) => c.type === "qualified_name");
763
+ let fqn;
764
+ let importedName;
765
+ if (qualified) {
766
+ fqn = qualified.text.replace(/^\\/, "");
767
+ importedName = fqn.replace(/^.*\\/, "");
768
+ }
769
+ else if (names[0]) {
770
+ importedName = names[0].text;
771
+ fqn = prefix ? `${prefix}\\${importedName}` : importedName;
772
+ }
773
+ else {
774
+ return null;
775
+ }
776
+ const alias = qualified && names.length >= 1
777
+ ? names[names.length - 1].text
778
+ : names.length >= 2
779
+ ? names[1].text
780
+ : undefined;
781
+ const local = alias ?? importedName;
782
+ return { local, name: importedName, specifier: fqn };
783
+ }
784
+ /** PHP 8 attributes on a definition → `references` edges to the attribute class. */
785
+ function phpAttributeReferenceEdges(node, sourceId, ctx) {
786
+ const edges = [];
787
+ for (const child of node.namedChildren) {
788
+ if (child.type !== "attribute_list")
789
+ continue;
790
+ for (const group of child.namedChildren) {
791
+ if (group.type !== "attribute_group")
792
+ continue;
793
+ for (const attr of group.namedChildren) {
794
+ if (attr.type !== "attribute")
795
+ continue;
796
+ const ref = phpAttributeClassRef(attr, ctx);
797
+ if (ref) {
798
+ edges.push({
799
+ source: sourceId,
800
+ relation: "references",
801
+ name: ref.name,
802
+ ...(ref.specifier ? { specifier: ref.specifier } : {}),
803
+ file: ctx.rel,
804
+ });
805
+ }
806
+ }
807
+ }
808
+ }
809
+ return edges;
810
+ }
811
+ function phpAttributeClassRef(attr, ctx) {
812
+ const nameNode = attr.childForFieldName("name") ??
813
+ attr.namedChildren.find((c) => c.type === "name" || c.type === "qualified_name");
814
+ if (!nameNode)
815
+ return null;
816
+ if (nameNode.type === "qualified_name") {
817
+ const fqn = nameNode.text.replace(/^\\/, "");
818
+ return { name: fqn.replace(/^.*\\/, ""), specifier: fqn };
819
+ }
820
+ const bare = nameNode.text;
821
+ const imported = ctx.importedSymbols.get(bare);
822
+ if (imported)
823
+ return { name: imported.name, specifier: imported.specifier };
824
+ return { name: bare };
825
+ }
826
+ /** Java annotations on a definition → `references` edges to the annotation type. */
827
+ function javaAnnotationReferenceEdges(node, sourceId, ctx) {
828
+ const edges = [];
829
+ const mods = node.namedChildren.find((c) => c.type === "modifiers");
830
+ if (!mods)
831
+ return edges;
832
+ for (const child of mods.namedChildren) {
833
+ if (child.type !== "marker_annotation" && child.type !== "annotation")
834
+ continue;
835
+ const name = javaAnnotationTypeName(child);
836
+ if (name) {
837
+ edges.push({
838
+ source: sourceId,
839
+ relation: "references",
840
+ name,
841
+ file: ctx.rel,
842
+ });
843
+ }
844
+ }
845
+ return edges;
846
+ }
847
+ /** The type named by `@Foo` / `@a.b.Foo(...)`. Arguments are ignored (issue #89).
848
+ * A scoped name is kept whole, matching heritage: a bare last segment would
849
+ * false-match an unrelated in-repo type (#103). */
850
+ function javaAnnotationTypeName(anno) {
851
+ const nameNode = anno.childForFieldName("name");
852
+ if (!nameNode)
853
+ return null;
854
+ if (nameNode.type === "identifier" || nameNode.type === "scoped_identifier")
855
+ return nameNode.text;
856
+ return null;
857
+ }
418
858
  function collectTsImportBindings(node, specifier, out) {
419
859
  if (node.type === "import_specifier") {
420
860
  const name = node.childForFieldName("name")?.text;
@@ -505,8 +945,14 @@ function isDeclarationName(node) {
505
945
  function describe(node, ctx) {
506
946
  if (ctx.lang === "go")
507
947
  return describeGo(node, ctx);
948
+ if (ctx.lang === "r")
949
+ return describeR(node, ctx);
508
950
  if (ctx.lang === "java")
509
951
  return describeJava(node, ctx);
952
+ if (ctx.lang === "kotlin")
953
+ return describeKotlin(node, ctx);
954
+ if (ctx.lang === "swift")
955
+ return describeSwift(node, ctx);
510
956
  // PHP closures: `$h = function () {…}` / `fn() => …`, and bare callbacks
511
957
  // (`$routes->get('/x', function () {…})`). Captured as function nodes so a
512
958
  // closure-only file (a routing table, a DI container) keeps its structure
@@ -520,6 +966,21 @@ function describe(node, ctx) {
520
966
  hashNode: node,
521
967
  };
522
968
  }
969
+ // PHP anonymous classes (`new class implements I {…}`): minted as a class
970
+ // node named `{anonymous}` (mirroring `{closure}`, deduplicated per file by
971
+ // mintId). Without this the type vanished — no node, no heritage edge — and
972
+ // its methods mis-attributed to the enclosing function (issue #144). The
973
+ // class kind makes the walk emit heritageEdges (base_clause /
974
+ // class_interface_clause are direct children) and own the nested methods.
975
+ if (ctx.lang === "php" && node.type === "anonymous_class") {
976
+ const body = node.childForFieldName("body");
977
+ return {
978
+ name: "{anonymous}",
979
+ kind: "class",
980
+ headerEnd: body ? body.startIndex : node.endIndex,
981
+ hashNode: node,
982
+ };
983
+ }
523
984
  const mapped = ctx.kinds[node.type];
524
985
  if (mapped) {
525
986
  const name = node.childForFieldName("name")?.text;
@@ -529,6 +990,12 @@ function describe(node, ctx) {
529
990
  if (ctx.lang === "python" && mapped === "function" && ctx.enclosingKind === "class") {
530
991
  kind = "method";
531
992
  }
993
+ // tree-sitter-php 0.23.x recovers a collapsed enum method as function_definition
994
+ // at program scope; walkNamedChildren reparents it under the enum, and this
995
+ // promotion is what keeps the kind `method` rather than a leaked `function`.
996
+ if (ctx.lang === "php" && mapped === "function" && ctx.enclosingKind === "enum") {
997
+ kind = "method";
998
+ }
532
999
  const body = node.childForFieldName("body");
533
1000
  return { name, kind, headerEnd: body ? body.startIndex : node.endIndex, hashNode: node };
534
1001
  }
@@ -591,6 +1058,432 @@ function describeGo(node, _ctx) {
591
1058
  }
592
1059
  return null;
593
1060
  }
1061
+ /**
1062
+ * R definition shapes. `function_definition` carries no name field at all, so
1063
+ * unlike every other supported language the name always comes from an
1064
+ * enclosing assignment, detected here. The plain-function assignment check
1065
+ * (op-filtering `binary_operator`, the right-assign body-swap) is duplicated
1066
+ * in bindings.ts's own `rDefName` rather than imported — same reasoning as
1067
+ * this file's Go receiver helpers: bindings.ts can't take a value import back
1068
+ * on extract.ts. bindings.ts doesn't need the S3/S4/R6 half of this at all
1069
+ * (no handleR binding collector exists — see bindings.ts's own doc comment).
1070
+ *
1071
+ * Phase 1 (flat extraction — every named function is a plain `function` node)
1072
+ * plus Phase 2 (S3/S4/R6 class awareness, R's class systems being library
1073
+ * *convention* rather than grammar syntax, unlike every other language graft
1074
+ * supports):
1075
+ * - left-assign (`<-`/`<<-`/`=`) / right-assign (`->`/`->>`) function
1076
+ * assignment — Phase 1's shape, see the two `binary_operator`/
1077
+ * `function_definition` branches below. Right-assign's AST shape does NOT
1078
+ * mirror left-assign's the way it looks like it should (confirmed
1079
+ * empirically, not assumed — R's `->` has low enough precedence that it's
1080
+ * absorbed into the function's own `body` field as a `binary_operator`
1081
+ * instead of the function sitting inside an outer wrapper); only an
1082
+ * explicitly parenthesized `(function() {}) -> foo` produces the
1083
+ * "expected" outer-wrapping shape, which isn't specially handled (falls
1084
+ * through as an anonymous function).
1085
+ * - `name.Class <- function() {}` — an S3 method, IF `name` is a known
1086
+ * generic (registered locally via `UseMethod()` in this file, or one of a
1087
+ * curated set of common base-R generics — see `rS3Split`'s doc comment
1088
+ * for the false-positive risk this guards against).
1089
+ * - `Foo <- R6::R6Class("Foo", public = list(...), private = list(...))` —
1090
+ * an R6 class; its `public =`/`private =`/`active =` list entries become
1091
+ * methods, handled by walk()'s own `argument`-node interception (this
1092
+ * function only recognizes the class itself; the "a call defines a
1093
+ * symbol" list-walking lives in walk() since it needs to mint several
1094
+ * nodes, not describe a single one).
1095
+ * - `Foo <- list(public = list(...), private = list(...))` (Phase 5) — a
1096
+ * plain-list "mixin"/"extension" bundle, NOT wrapped in `R6::R6Class(...)`
1097
+ * at all: a real, deliberate convention found dogfooding against a real
1098
+ * R6-heavy corpus (25 files, 11 of them entirely invisible to the graph
1099
+ * without this) for sharing method bundles across classes via splicing
1100
+ * (`public = c(Foo$public, list(...))`) rather than `inherit =`. Only
1101
+ * recognized when the list actually has a `public =`/`private =` entry
1102
+ * (see `rIsMixinContainer`) — an ordinary data/config list never matches.
1103
+ * Reuses kind "class" (nothing better-fitting exists, and everything
1104
+ * downstream — the method-list walking, visibility — only cares that
1105
+ * ctx.enclosingKind is "class", not how the container was spelled); no
1106
+ * heritage edge, since splicing isn't `inherit =`-based inheritance.
1107
+ * - `setClass("Foo", ...)` / `setMethod("generic", "Foo", function() {})`
1108
+ * — S4 class/method calls, recognized as bare top-level `call` nodes
1109
+ * (setClass/setMethod have side effects registering with the S4 system;
1110
+ * they're essentially never assigned to a variable). `setGeneric()` is
1111
+ * NOT specially extracted — it doesn't naturally map to a class or method
1112
+ * kind, and the plan flags it as a case not worth the design risk.
1113
+ */
1114
+ function describeR(node, ctx) {
1115
+ if (node.type === "binary_operator") {
1116
+ const op = node.childForFieldName("operator")?.text;
1117
+ if (!op || !R_ASSIGN_OPS.has(op))
1118
+ return null;
1119
+ const lhs = node.childForFieldName("lhs");
1120
+ const rhs = node.childForFieldName("rhs");
1121
+ if (lhs?.type !== "identifier")
1122
+ return null;
1123
+ if (rhs?.type === "function_definition") {
1124
+ return rFunctionDescriptor(lhs.text, rhs, rhs.childForFieldName("body"), ctx);
1125
+ }
1126
+ if (rhs?.type === "call" && rCalleeName(rhs) === "R6Class") {
1127
+ // The class node itself; its public=/private=/active= method lists are
1128
+ // handled by walk()'s own `argument`-node interception, not here.
1129
+ return { name: lhs.text, kind: "class", headerEnd: rhs.endIndex, hashNode: rhs };
1130
+ }
1131
+ if (rhs?.type === "call" && rCalleeName(rhs) === "list" && rIsMixinContainer(rhs)) {
1132
+ // Phase 5: a plain-list mixin/extension bundle — same treatment as R6Class.
1133
+ return { name: lhs.text, kind: "class", headerEnd: rhs.endIndex, hashNode: rhs };
1134
+ }
1135
+ return null;
1136
+ }
1137
+ if (node.type === "function_definition") {
1138
+ // Right-assign (`function() {} -> foo`): see this function's own doc
1139
+ // comment for why this doesn't mirror the binary_operator branch above.
1140
+ const body = node.childForFieldName("body");
1141
+ if (body?.type !== "binary_operator")
1142
+ return null;
1143
+ const op = body.childForFieldName("operator")?.text;
1144
+ if (!op || !R_RIGHT_ASSIGN_OPS.has(op))
1145
+ return null;
1146
+ const rhs = body.childForFieldName("rhs");
1147
+ if (rhs?.type !== "identifier")
1148
+ return null;
1149
+ return rFunctionDescriptor(rhs.text, node, body.childForFieldName("lhs"), ctx);
1150
+ }
1151
+ if (node.type === "call") {
1152
+ return describeRTopLevelCall(node);
1153
+ }
1154
+ // R6 (Phase 2): reached via walk()'s own `argument`-node interception for a
1155
+ // `public =`/`private =`/`active =` list entry — see the special case there
1156
+ // for why this can't just be a flat kind-table/node-type check like every
1157
+ // other definition shape.
1158
+ if (node.type === "argument" && ctx.rR6Access !== null) {
1159
+ const argName = node.childForFieldName("name");
1160
+ const value = node.childForFieldName("value");
1161
+ if (argName?.type !== "identifier" || value?.type !== "function_definition")
1162
+ return null;
1163
+ const body = value.childForFieldName("body");
1164
+ return {
1165
+ name: argName.text,
1166
+ kind: "method",
1167
+ headerEnd: body ? body.startIndex : value.endIndex,
1168
+ hashNode: value,
1169
+ // owner deliberately unset — R6 methods DO lexically nest inside the
1170
+ // class-defining call, so ctx.enclosingClass already has it.
1171
+ };
1172
+ }
1173
+ return null;
1174
+ }
1175
+ const R_ASSIGN_OPS = new Set(["<-", "<<-", "="]);
1176
+ const R_RIGHT_ASSIGN_OPS = new Set(["->", "->>"]);
1177
+ /** A plain function assignment (left- or right-assign), OR — if `name` matches
1178
+ * a known S3 generic's `generic.Class` pattern — an S3 method instead. `body`
1179
+ * is the function's REAL content node (already resolved by the caller for
1180
+ * either assignment direction), used only for `headerEnd`; `hashNode` is
1181
+ * always the `function_definition` itself. */
1182
+ function rFunctionDescriptor(name, hashNode, body, ctx) {
1183
+ const headerEnd = body ? body.startIndex : hashNode.endIndex;
1184
+ const s3 = rS3Split(name, ctx.rGenerics);
1185
+ if (s3) {
1186
+ return {
1187
+ name: s3.generic,
1188
+ idName: `${s3.className}.${s3.generic}`,
1189
+ kind: "method",
1190
+ headerEnd,
1191
+ hashNode,
1192
+ owner: s3.className,
1193
+ };
1194
+ }
1195
+ return { name, kind: "function", headerEnd, hashNode };
1196
+ }
1197
+ /**
1198
+ * S3 dispatch detection: does `name` split as `generic.Class` for some KNOWN
1199
+ * generic? Tries the longest possible generic prefix first (so a dotted
1200
+ * generic itself, like `as.character`, is found before a shorter false match)
1201
+ * and only ever matches a generic that's either registered locally via
1202
+ * `UseMethod()` in this file (see `collectRGenerics`) or in the small curated
1203
+ * `R_BASE_GENERICS` set below.
1204
+ *
1205
+ * This is the genuinely ambiguous part of R support the plan calls out:
1206
+ * `read.csv`, `data.frame`, and `as.character` used as an ordinary helper
1207
+ * name are NOT S3 dispatch, and nothing in the grammar distinguishes them
1208
+ * from `print.MyClass`. Erring toward the curated set staying small — a
1209
+ * missed S3 method (false negative, falls back to an ordinary `function`
1210
+ * node) is a much smaller problem than a false positive misfiling an
1211
+ * unrelated dotted-name function as some other class's method.
1212
+ */
1213
+ function rS3Split(name, generics) {
1214
+ const parts = name.split(".");
1215
+ if (parts.length < 2)
1216
+ return null;
1217
+ for (let i = parts.length - 1; i >= 1; i--) {
1218
+ const generic = parts.slice(0, i).join(".");
1219
+ if (generics.has(generic) || R_BASE_GENERICS.has(generic)) {
1220
+ return { generic, className: parts.slice(i).join(".") };
1221
+ }
1222
+ }
1223
+ return null;
1224
+ }
1225
+ /** Common base-R S3 generics worth assuming even without local evidence —
1226
+ * print.Foo/format.Foo etc. are the single most common real-world S3
1227
+ * pattern, and a local `UseMethod()` call will never exist for them (they
1228
+ * ship in base/methods/stats, not the user's own repo). Deliberately small
1229
+ * and unsurprising rather than exhaustive — see `rS3Split`'s doc comment. */
1230
+ const R_BASE_GENERICS = new Set([
1231
+ "print",
1232
+ "format",
1233
+ "summary",
1234
+ "plot",
1235
+ "str",
1236
+ "toString",
1237
+ "as.character",
1238
+ "as.list",
1239
+ "as.data.frame",
1240
+ "as.vector",
1241
+ "as.numeric",
1242
+ "as.matrix",
1243
+ "length",
1244
+ "dim",
1245
+ "names",
1246
+ "rev",
1247
+ "sort",
1248
+ "unique",
1249
+ "predict",
1250
+ "coef",
1251
+ "residuals",
1252
+ "fitted",
1253
+ "update",
1254
+ "merge",
1255
+ "all.equal",
1256
+ "anova",
1257
+ "confint",
1258
+ "vcov",
1259
+ "logLik",
1260
+ ]);
1261
+ /** Every S3 generic THIS file registers via a local `UseMethod()` call, so
1262
+ * `rS3Split` can recognize `generic.Class` methods for a repo's own generics,
1263
+ * not just the base-R ones. Runs once per file, ahead of the main walk (same
1264
+ * pre-pass shape as `collectImportedSymbols`). Cross-file generics — a
1265
+ * generic defined in one file, dispatched on in another — aren't found this
1266
+ * way; that would need a whole-repo pass extractFile has no visibility into,
1267
+ * the same limitation Go/C++'s per-file bindings already accept. */
1268
+ function collectRGenerics(root) {
1269
+ const generics = new Set();
1270
+ const visit = (node) => {
1271
+ let fnDef = null;
1272
+ let ownName = null;
1273
+ if (node.type === "binary_operator") {
1274
+ const op = node.childForFieldName("operator")?.text;
1275
+ const lhs = node.childForFieldName("lhs");
1276
+ const rhs = node.childForFieldName("rhs");
1277
+ if (op && R_ASSIGN_OPS.has(op) && lhs?.type === "identifier" && rhs?.type === "function_definition") {
1278
+ fnDef = rhs;
1279
+ ownName = lhs.text;
1280
+ }
1281
+ }
1282
+ else if (node.type === "function_definition") {
1283
+ const body = node.childForFieldName("body");
1284
+ if (body?.type === "binary_operator") {
1285
+ const op = body.childForFieldName("operator")?.text;
1286
+ const rhs = body.childForFieldName("rhs");
1287
+ if (op && R_RIGHT_ASSIGN_OPS.has(op) && rhs?.type === "identifier") {
1288
+ fnDef = node;
1289
+ ownName = rhs.text;
1290
+ }
1291
+ }
1292
+ }
1293
+ if (fnDef && ownName) {
1294
+ const arg = findUseMethodArg(fnDef.childForFieldName("body"));
1295
+ if (arg !== undefined)
1296
+ generics.add(arg || ownName); // "" means UseMethod() with no args
1297
+ }
1298
+ for (const child of node.namedChildren)
1299
+ visit(child);
1300
+ };
1301
+ visit(root);
1302
+ return generics;
1303
+ }
1304
+ /** Searches a function body for a `UseMethod(...)` call and returns its
1305
+ * string-literal generic-name argument, `""` if called with no arguments
1306
+ * (defaults to the enclosing function's own name), or `undefined` if no
1307
+ * `UseMethod` call is found at all. */
1308
+ function findUseMethodArg(node) {
1309
+ if (!node)
1310
+ return undefined;
1311
+ if (node.type === "call" && rCalleeName(node) === "UseMethod") {
1312
+ const first = rCallArgs(node)[0]?.childForFieldName("value");
1313
+ return first?.type === "string" ? (rStringContent(first) ?? "") : "";
1314
+ }
1315
+ for (const child of node.namedChildren) {
1316
+ const found = findUseMethodArg(child);
1317
+ if (found !== undefined)
1318
+ return found;
1319
+ }
1320
+ return undefined;
1321
+ }
1322
+ /** S4: `setClass("Foo", ...)` → a class; `setMethod("generic", "Foo",
1323
+ * function() {})` → a method owned by "Foo". Both are ordinary top-level
1324
+ * `call` nodes — S4's registration functions have side effects and are
1325
+ * essentially never assigned to a variable, unlike R6Class. */
1326
+ function describeRTopLevelCall(node) {
1327
+ const callee = rCalleeName(node);
1328
+ if (callee === "setClass") {
1329
+ const first = rCallArgs(node)[0]?.childForFieldName("value");
1330
+ const name = first?.type === "string" ? rStringContent(first) : null;
1331
+ if (!name)
1332
+ return null;
1333
+ return { name, kind: "class", headerEnd: node.endIndex, hashNode: node };
1334
+ }
1335
+ if (callee === "setMethod") {
1336
+ const args = rCallArgs(node);
1337
+ const generic = args[0] ? rStringContent(args[0].childForFieldName("value") ?? null) : null;
1338
+ const className = args[1] ? rStringContent(args[1].childForFieldName("value") ?? null) : null;
1339
+ const defArg = args.find((a) => a.childForFieldName("name")?.text === "definition") ?? args[2];
1340
+ const fnDef = defArg?.childForFieldName("value");
1341
+ if (!generic || !className || fnDef?.type !== "function_definition")
1342
+ return null;
1343
+ const body = fnDef.childForFieldName("body");
1344
+ return {
1345
+ name: generic,
1346
+ idName: `${className}.${generic}`,
1347
+ kind: "method",
1348
+ headerEnd: body ? body.startIndex : fnDef.endIndex,
1349
+ hashNode: fnDef,
1350
+ owner: className,
1351
+ };
1352
+ }
1353
+ return null;
1354
+ }
1355
+ /** A call's callee name, whether bare (`R6Class(...)`) or namespace-qualified
1356
+ * (`R6::R6Class(...)`). Null if the callee isn't a simple name (e.g. itself a
1357
+ * call, or a `$`-based access). */
1358
+ function rCalleeName(node) {
1359
+ const fn = node.childForFieldName("function");
1360
+ if (fn?.type === "identifier")
1361
+ return fn.text;
1362
+ if (fn?.type === "namespace_operator") {
1363
+ const rhs = fn.childForFieldName("rhs");
1364
+ return rhs?.type === "identifier" ? rhs.text : null;
1365
+ }
1366
+ return null;
1367
+ }
1368
+ /** A call's positional/named `argument` children (skipping the `,`/`(`/`)`
1369
+ * punctuation tokens that share the `arguments` node's child list). */
1370
+ function rCallArgs(node) {
1371
+ return node.childForFieldName("arguments")?.namedChildren.filter((c) => c.type === "argument") ?? [];
1372
+ }
1373
+ /** An R `string` node's unquoted text, or null if `node` isn't a string. */
1374
+ function rStringContent(node) {
1375
+ if (node?.type !== "string")
1376
+ return null;
1377
+ const content = node.namedChildren.find((c) => c.type === "string_content");
1378
+ return content?.text ?? null;
1379
+ }
1380
+ /** The value of a call's named argument (`setClass("Foo", contains = "Base")`
1381
+ * → the `contains` argument), or null if absent. */
1382
+ function rNamedArg(node, argName) {
1383
+ const arg = rCallArgs(node).find((a) => a.childForFieldName("name")?.text === argName);
1384
+ return arg?.childForFieldName("value") ?? null;
1385
+ }
1386
+ /** An R6 class-defining node's `inherit =` parent class name (a bare
1387
+ * identifier — the parent's own generator variable, not a string), for
1388
+ * `super$` call resolution (Phase 3). `node` is whatever describeR matched: a
1389
+ * binary_operator for R6 (`Foo <- R6::R6Class(...)`) or, for any other kind of
1390
+ * class (S4's setClass, which has no `super`), the call itself — always null
1391
+ * there since `rCalleeName(call) !== "R6Class"`. */
1392
+ function rR6ParentClass(node) {
1393
+ const call = node.type === "binary_operator" ? node.childForFieldName("rhs") : node;
1394
+ if (call?.type !== "call" || rCalleeName(call) !== "R6Class")
1395
+ return null;
1396
+ const value = rNamedArg(call, "inherit");
1397
+ return value?.type === "identifier" ? value.text : null;
1398
+ }
1399
+ /** Does this `list(...)` call look like a Phase-5 mixin/extension bundle —
1400
+ * i.e. does it have a `public =` or `private =` entry whose own value is
1401
+ * itself a `list(...)` call? This is the one check standing between "class-
1402
+ * like container" and an ordinary data/config list (`list(a = 1, b = 2)`,
1403
+ * or even one that happens to have a field named "public" holding something
1404
+ * else) — real code never coincidentally shapes plain data this way, so it's
1405
+ * a safe, precise signal without needing a naming-convention heuristic. */
1406
+ function rIsMixinContainer(node) {
1407
+ return rCallArgs(node).some((a) => {
1408
+ const name = a.childForFieldName("name")?.text;
1409
+ if (name !== "public" && name !== "private")
1410
+ return false;
1411
+ const value = a.childForFieldName("value");
1412
+ return value?.type === "call" && rCalleeName(value) === "list";
1413
+ });
1414
+ }
1415
+ /** A base-class name list from either a bare string (`contains = "Base"`) or
1416
+ * a `c(...)` call of strings (`contains = c("Base1", "Base2")`) — S4's
1417
+ * multiple-inheritance form. */
1418
+ function rStringOrCVector(value) {
1419
+ if (!value)
1420
+ return [];
1421
+ const single = rStringContent(value);
1422
+ if (single)
1423
+ return [single];
1424
+ if (value.type === "call" && rCalleeName(value) === "c") {
1425
+ return rCallArgs(value)
1426
+ .map((a) => rStringContent(a.childForFieldName("value")))
1427
+ .filter((s) => !!s);
1428
+ }
1429
+ return [];
1430
+ }
1431
+ /** R6 visibility follows the `public =`/`private =`/`active =` section a
1432
+ * method was declared in (see walk()'s `argument`-node interception) —
1433
+ * `ctx.rR6Access` is only set for that direct span, so a plain function or an
1434
+ * S3/S4 method (neither of which has a real visibility concept) checks for a
1435
+ * roxygen `@export` tag next (Phase 3), falling back to the leading-dot naming
1436
+ * convention only when there's no roxygen evidence to go on at all. `node` is
1437
+ * the exact node describeR matched — see `rRoxygenExported`'s doc comment for
1438
+ * why that's always the right one to check for a preceding comment block. */
1439
+ function rExported(name, ctx, node) {
1440
+ if (ctx.rR6Access !== null)
1441
+ return ctx.rR6Access !== "private";
1442
+ const roxygen = rRoxygenExported(node);
1443
+ if (roxygen !== null)
1444
+ return roxygen;
1445
+ return !name.startsWith(".");
1446
+ }
1447
+ /**
1448
+ * Roxygen `@export` detection (Phase 3): does `node` — a top-level definition
1449
+ * statement (a `binary_operator` assignment, a right-assigned
1450
+ * `function_definition`, or an S4 `setClass`/`setMethod` call) — have a
1451
+ * roxygen doc block immediately preceding it, and if so, is it tagged
1452
+ * `@export`?
1453
+ *
1454
+ * `comment` is a grammar EXTRA in this grammar (floats loosely between
1455
+ * sibling nodes rather than attaching to "the next statement" via a field),
1456
+ * so this walks backward through `previousNamedSibling` collecting a
1457
+ * contiguous run of `comment` nodes — the run ends at the first non-comment
1458
+ * sibling, or at the first comment that isn't itself a roxygen (`#'`) line,
1459
+ * either of which is roxygen's own "this block documents the next statement"
1460
+ * boundary.
1461
+ *
1462
+ * Returns:
1463
+ * - `true` — a roxygen block was found and it contains `@export`.
1464
+ * - `false` — a roxygen block was found but it does NOT contain `@export`.
1465
+ * This is deliberately a confident "not exported," not "unknown": roxygen
1466
+ * generates a package's NAMESPACE from exactly its `@export`-tagged items,
1467
+ * so a documented-but-untagged function is an explicit "internal, for
1468
+ * maintainers only" signal, not an absence of evidence.
1469
+ * - `null` — no roxygen block at all, so the caller should fall back to the
1470
+ * leading-dot naming convention instead of guessing.
1471
+ */
1472
+ function rRoxygenExported(node) {
1473
+ let sib = node.previousNamedSibling;
1474
+ let sawRoxygen = false;
1475
+ let exported = false;
1476
+ while (sib?.type === "comment") {
1477
+ const text = sib.text.trim();
1478
+ if (!text.startsWith("#'"))
1479
+ break; // an ordinary # comment ends the roxygen block
1480
+ sawRoxygen = true;
1481
+ if (/^#'\s*@export\b/.test(text))
1482
+ exported = true;
1483
+ sib = sib.previousNamedSibling;
1484
+ }
1485
+ return sawRoxygen ? exported : null;
1486
+ }
594
1487
  /** Java definition shapes. Uniform in a way Go's are not: every declaration carries
595
1488
  * a `name` field and (for types and most members) a `body`, so one mapped lookup
596
1489
  * covers classes, interfaces, enums, records, methods, and constructors. Methods are
@@ -622,6 +1515,202 @@ function describeJava(node, ctx) {
622
1515
  }
623
1516
  return desc;
624
1517
  }
1518
+ /** Kotlin definition shapes. Unlike Java's, tree-sitter-kotlin exposes no `name`
1519
+ * or `body` fields: a definition's name is an unnamed `simple_identifier` (functions)
1520
+ * or `type_identifier` (types) child, and its body is a `class_body` / `function_body`
1521
+ * / `statements` child. `class_declaration` also folds classes, interfaces, and enum
1522
+ * classes into one node type — the kind is read off the declaration's own keywords. */
1523
+ function describeKotlin(node, ctx) {
1524
+ // The first direct `type_identifier` is the declared name (type parameters, primary
1525
+ // constructor parameters and delegation specifiers are all nested beneath it).
1526
+ const typeName = () => node.namedChildren.find((c) => c.type === "type_identifier")?.text ?? null;
1527
+ // The first direct `simple_identifier` is the function name (receiver type, params
1528
+ // and type parameters are all nested beneath other child nodes).
1529
+ const funcName = () => node.namedChildren.find((c) => c.type === "simple_identifier")?.text ?? null;
1530
+ // `class X : A, B()` heritage lives in `delegation_specifier` children; a nested
1531
+ // type parameter's identifier is one of the same node type, so only direct children
1532
+ // count as the declared name.
1533
+ const headEnd = (type) => {
1534
+ const body = node.namedChildren.find((c) => c.type === type);
1535
+ return body ? body.startIndex : node.endIndex;
1536
+ };
1537
+ if (node.type === "class_declaration") {
1538
+ const name = typeName();
1539
+ if (!name)
1540
+ return null;
1541
+ let kind = "class";
1542
+ if (node.namedChildren.some((c) => c.type === "enum_class_body"))
1543
+ kind = "enum";
1544
+ else if (node.children.some((c) => c.type === "interface"))
1545
+ kind = "interface";
1546
+ else {
1547
+ const mods = node.namedChildren.find((c) => c.type === "modifiers");
1548
+ // `annotation class` → the interface role Java's annotation_type_declaration plays.
1549
+ if (mods?.namedChildren.some((c) => c.type === "class_modifier" && c.text === "annotation"))
1550
+ kind = "interface";
1551
+ }
1552
+ const body = node.namedChildren.find((c) => c.type === "class_body" || c.type === "enum_class_body");
1553
+ return { name, kind, headerEnd: body ? body.startIndex : node.endIndex, hashNode: node };
1554
+ }
1555
+ if (node.type === "object_declaration") {
1556
+ const name = typeName();
1557
+ if (!name)
1558
+ return null;
1559
+ return { name, kind: "class", headerEnd: headEnd("class_body"), hashNode: node };
1560
+ }
1561
+ if (node.type === "function_declaration") {
1562
+ const name = funcName();
1563
+ if (!name)
1564
+ return null;
1565
+ const kind = KOTLIN_TYPE_KINDS.has(ctx.enclosingKind ?? "file") ? "method" : "function";
1566
+ return { name, kind, headerEnd: headEnd("function_body"), hashNode: node };
1567
+ }
1568
+ if (node.type === "secondary_constructor") {
1569
+ // Constructors carry no name of their own — they are the class's own, so scope the
1570
+ // node under the enclosing class the same way Java's constructor_declaration does.
1571
+ if (!ctx.enclosingClass)
1572
+ return null;
1573
+ return {
1574
+ name: ctx.enclosingClass,
1575
+ kind: "method",
1576
+ headerEnd: headEnd("statements"),
1577
+ hashNode: node,
1578
+ };
1579
+ }
1580
+ if (node.type === "type_alias") {
1581
+ const name = typeName();
1582
+ if (!name)
1583
+ return null;
1584
+ return { name, kind: "type", headerEnd: node.endIndex, hashNode: node };
1585
+ }
1586
+ if (node.type === "property_declaration") {
1587
+ // Top-level `val`/`var` only — a class property is a field, not a definition node
1588
+ // (no depth tier emits fields), so it must not become one.
1589
+ if (ctx.enclosingKind !== null)
1590
+ return null;
1591
+ const decl = node.namedChildren.find((c) => c.type === "variable_declaration");
1592
+ const name = decl?.namedChildren.find((c) => c.type === "simple_identifier")?.text;
1593
+ if (!name)
1594
+ return null;
1595
+ return { name, kind: "variable", headerEnd: node.endIndex, hashNode: node };
1596
+ }
1597
+ return null;
1598
+ }
1599
+ /** Swift definition shapes. Like Kotlin's, tree-sitter-swift exposes no `name` or
1600
+ * `body` fields: a definition's name is a direct `type_identifier` (types) or
1601
+ * `simple_identifier` (functions) child, and its body is a `class_body` /
1602
+ * `enum_class_body` / `protocol_body` / `function_body` child. One
1603
+ * `class_declaration` node type covers `class`, `struct`, `enum`, `actor` AND
1604
+ * `extension` — the declaration's own keyword token tells them apart. */
1605
+ function describeSwift(node, ctx) {
1606
+ // The first direct `type_identifier` is the declared name (generic parameters and
1607
+ // inheritance specifiers are all nested beneath other child nodes).
1608
+ const typeName = () => node.namedChildren.find((c) => c.type === "type_identifier")?.text ?? null;
1609
+ // The first direct `simple_identifier` is the function name (parameters and
1610
+ // generic parameters are all nested beneath other child nodes).
1611
+ const funcName = () => node.namedChildren.find((c) => c.type === "simple_identifier")?.text ?? null;
1612
+ const headEnd = (...types) => {
1613
+ const body = node.namedChildren.find((c) => types.includes(c.type));
1614
+ return body ? body.startIndex : node.endIndex;
1615
+ };
1616
+ if (node.type === "class_declaration") {
1617
+ const kw = node.children.find((c) => c.type === "class" ||
1618
+ c.type === "struct" ||
1619
+ c.type === "enum" ||
1620
+ c.type === "actor" ||
1621
+ c.type === "extension")?.type;
1622
+ let name;
1623
+ if (kw === "extension") {
1624
+ // `extension Point { … }` has no name of its own — the extended type's IS its
1625
+ // identity, so the node takes that name: members mint as `Point.method`,
1626
+ // `enclosingClass` becomes `Point`, and a member call on a Point receiver
1627
+ // resolves to them exactly as if they were declared on the type. A qualified
1628
+ // target (`extension Swift.Array`) reduces to its last component, matching
1629
+ // how the extended type is itself named in the graph.
1630
+ //
1631
+ // Its KIND is "module", not "class": the type usually already has a real
1632
+ // declaration, and a second same-named "class" node would make the name
1633
+ // AMBIGUOUS to resolveName — every `Point(...)` initializer call and every
1634
+ // `: Point` heritage target would then drop instead of resolving (resolve
1635
+ // never guesses between same-named candidates). "module" keeps the node out
1636
+ // of type-name resolution entirely while SWIFT_TYPE_KINDS still makes it
1637
+ // own its members; typed member calls are untouched either way, since they
1638
+ // go through the owner-qualified method index, not the type's own node.
1639
+ const ut = node.namedChildren.find((c) => c.type === "user_type");
1640
+ const ids = ut?.namedChildren.filter((c) => c.type === "type_identifier") ?? [];
1641
+ name = ids.length ? ids[ids.length - 1].text : null;
1642
+ }
1643
+ else {
1644
+ name = typeName();
1645
+ }
1646
+ if (!name)
1647
+ return null;
1648
+ // An actor is class-like (reference semantics, methods) and there is no
1649
+ // dedicated actor kind, so it takes "class".
1650
+ const kind = kw === "extension" ? "module" : kw === "struct" ? "struct" : kw === "enum" ? "enum" : "class";
1651
+ return { name, kind, headerEnd: headEnd("class_body", "enum_class_body"), hashNode: node };
1652
+ }
1653
+ if (node.type === "protocol_declaration") {
1654
+ const name = typeName();
1655
+ if (!name)
1656
+ return null;
1657
+ return { name, kind: "interface", headerEnd: headEnd("protocol_body"), hashNode: node };
1658
+ }
1659
+ // A protocol requirement (`protocol_function_declaration`) has no body and is
1660
+ // always a member; an ordinary `function_declaration` is a method exactly when
1661
+ // it is nested in a type (or an extension of one).
1662
+ if (node.type === "function_declaration" || node.type === "protocol_function_declaration") {
1663
+ const name = funcName();
1664
+ if (!name)
1665
+ return null;
1666
+ const kind = node.type === "protocol_function_declaration" ||
1667
+ SWIFT_TYPE_KINDS.has(ctx.enclosingKind ?? "file")
1668
+ ? "method"
1669
+ : "function";
1670
+ return {
1671
+ name,
1672
+ kind,
1673
+ headerEnd: headEnd("function_body"),
1674
+ hashNode: node,
1675
+ ...swiftArity(node),
1676
+ };
1677
+ }
1678
+ if (node.type === "init_declaration") {
1679
+ // Initializers carry no name of their own — they are the type's own, so scope
1680
+ // the node under the enclosing type the same way Java's constructor_declaration
1681
+ // does. (A protocol's `init` requirement lands here too, owned by the protocol.)
1682
+ if (!ctx.enclosingClass)
1683
+ return null;
1684
+ return {
1685
+ name: ctx.enclosingClass,
1686
+ kind: "method",
1687
+ headerEnd: headEnd("function_body"),
1688
+ hashNode: node,
1689
+ ...swiftArity(node),
1690
+ };
1691
+ }
1692
+ if (node.type === "typealias_declaration") {
1693
+ const name = typeName();
1694
+ if (!name)
1695
+ return null;
1696
+ return { name, kind: "type", headerEnd: node.endIndex, hashNode: node };
1697
+ }
1698
+ if (node.type === "property_declaration") {
1699
+ // Top-level `let`/`var` only — a stored/computed property inside a type is a
1700
+ // field, not a definition node (no depth tier emits fields), so it must not
1701
+ // become one. `deinit` and `subscript` are likewise skipped: neither is ever
1702
+ // the target of a resolvable call edge, and neither carries a usable name.
1703
+ if (ctx.enclosingKind !== null)
1704
+ return null;
1705
+ const name = node.namedChildren
1706
+ .find((c) => c.type === "pattern")
1707
+ ?.namedChildren.find((c) => c.type === "simple_identifier")?.text;
1708
+ if (!name)
1709
+ return null;
1710
+ return { name, kind: "variable", headerEnd: node.endIndex, hashNode: node };
1711
+ }
1712
+ return null;
1713
+ }
625
1714
  /** Java visibility: `public` (or `protected`) on the declaration's own modifier list.
626
1715
  * A package-private or private member is not part of the API surface. Read off the
627
1716
  * `modifiers` child's tokens, ignoring annotations, which live in the same node. */
@@ -631,6 +1720,65 @@ function javaExported(node) {
631
1720
  return false;
632
1721
  return mods.children.some((c) => c.type === "public" || c.type === "protected");
633
1722
  }
1723
+ /** Kotlin visibility: exported by default (`public` is implicit); only an explicit
1724
+ * `internal` / `private` / `protected` visibility modifier hides a definition. */
1725
+ function kotlinExported(node) {
1726
+ const mods = node.namedChildren.find((c) => c.type === "modifiers");
1727
+ if (!mods)
1728
+ return true;
1729
+ const vis = mods.namedChildren.find((c) => c.type === "visibility_modifier");
1730
+ return !vis || vis.text === "public";
1731
+ }
1732
+ /** Declared parameter count for a Swift callable, for overload disambiguation
1733
+ * (the same role Java's `arity`/`argCount` pair plays). `parameter` nodes are
1734
+ * direct children of the declaration; a default value's `=` sits as a SIBLING
1735
+ * token after its parameter, and a variadic `...` sits inside its parameter.
1736
+ * `arity` is the REQUIRED minimum (parameters minus defaults) and `variadic`
1737
+ * marks any default or variadic parameter, so `narrowByArity`'s at-least
1738
+ * semantics keeps every overload a call of that shape could reach. */
1739
+ function swiftArity(node) {
1740
+ const params = node.children.filter((c) => c.type === "parameter");
1741
+ const defaults = node.children.filter((c) => c.type === "=").length;
1742
+ const hasVariadic = params.some((p) => p.children.some((c) => c.type === "..."));
1743
+ const arity = Math.max(0, params.length - defaults);
1744
+ return hasVariadic || defaults > 0 ? { arity, variadic: true } : { arity };
1745
+ }
1746
+ /** Argument count at a Swift call site: the `value_argument`s plus one for a
1747
+ * trailing closure (`run(x) { … }` calls a two-parameter function). */
1748
+ function swiftArgCount(node) {
1749
+ const suffix = node.namedChildren.find((c) => c.type === "call_suffix");
1750
+ if (!suffix)
1751
+ return undefined;
1752
+ const args = suffix.namedChildren
1753
+ .find((c) => c.type === "value_arguments")
1754
+ ?.namedChildren.filter((c) => c.type === "value_argument").length ?? 0;
1755
+ const trailing = suffix.namedChildren.some((c) => c.type === "lambda_literal") ? 1 : 0;
1756
+ return args + trailing;
1757
+ }
1758
+ /** Swift visibility: the default (`internal`) is module-wide, and a repo is
1759
+ * typically one module — so `public` / `open` / `package` / `internal` all count
1760
+ * as API surface, and only an explicit `private` / `fileprivate` hides a
1761
+ * definition. A setter-only restriction (`private(set)`) leaves the getter
1762
+ * visible, so it does not hide the symbol either. */
1763
+ function swiftExported(node) {
1764
+ const mods = node.namedChildren.find((c) => c.type === "modifiers");
1765
+ const vis = mods?.namedChildren.find((c) => c.type === "visibility_modifier");
1766
+ return !vis || (vis.text !== "private" && vis.text !== "fileprivate");
1767
+ }
1768
+ /** The superclass a Swift class declaration names: its FIRST inheritance
1769
+ * specifier — Swift's grammar requires the superclass to precede any protocol
1770
+ * in the `:` list, so when a superclass exists it is always this entry. A
1771
+ * class conforming only to protocols yields that protocol's name instead, but
1772
+ * `super` is illegal in such a class, so no call site ever consults it. Null
1773
+ * for a bare `class Foo` (and for an extension, whose declaration carries no
1774
+ * heritage for the original type — `super` inside one stays unresolved). */
1775
+ function swiftSuperClassName(node) {
1776
+ const spec = node.namedChildren.find((c) => c.type === "inheritance_specifier");
1777
+ const ids = spec?.namedChildren
1778
+ .find((c) => c.type === "user_type")
1779
+ ?.namedChildren.filter((c) => c.type === "type_identifier");
1780
+ return ids?.length ? ids[ids.length - 1].text : null;
1781
+ }
634
1782
  /** The receiver's base type name for a Go method, unwrapping a pointer receiver
635
1783
  * (`func (u *User) …` → `User`). Null if it can't be read. */
636
1784
  function goReceiverType(node) {
@@ -679,6 +1827,7 @@ function heritageEdges(node, classId, ctx) {
679
1827
  // `superclass` holds `extends X`; `super_interfaces` holds `implements A, B`
680
1828
  // (and, on an interface declaration, `extends A, B` — which tree-sitter-java
681
1829
  // still spells `extends_interfaces`).
1830
+ const typeParams = javaTypeParameterNames(node);
682
1831
  for (const child of node.namedChildren) {
683
1832
  const relation = child.type === "superclass"
684
1833
  ? "extends"
@@ -687,12 +1836,56 @@ function heritageEdges(node, classId, ctx) {
687
1836
  : null;
688
1837
  if (!relation)
689
1838
  continue;
690
- for (const t of typeIdentifiersIn(child)) {
691
- edges.push({ source: classId, relation, name: t, file: ctx.rel });
1839
+ for (const entry of javaSupertypeEntries(child)) {
1840
+ const name = javaSupertypeName(entry);
1841
+ // Belt-and-braces. A type VARIABLE is never a supertype, and erasing the
1842
+ // arguments already removes every case measured on gson and spring-petclinic
1843
+ // (identical output with this filter removed) — Java cannot extend or implement
1844
+ // a type variable, so a surviving `T` would have to come from a shape neither
1845
+ // repo contains. Kept because a wrong supertype is not a cosmetic edge: it
1846
+ // feeds `classParents` and from there call resolution.
1847
+ if (!name || typeParams.has(name))
1848
+ continue;
1849
+ edges.push({ source: classId, relation, name, file: ctx.rel });
692
1850
  }
693
1851
  }
694
1852
  return edges;
695
1853
  }
1854
+ if (ctx.lang === "kotlin") {
1855
+ // The `:` clause is a list of `delegation_specifier`s — a superclass construction
1856
+ // (`class A : B()`), an interface, or `by` delegation. The first `type_identifier`
1857
+ // under each names the type; everything else (type args, delegation target) is not
1858
+ // the heritage target, so only the head type counts.
1859
+ for (const child of node.namedChildren) {
1860
+ if (child.type !== "delegation_specifier")
1861
+ continue;
1862
+ const t = child.namedChildren.find((c) => c.type === "user_type")?.namedChildren.find((c) => c.type === "type_identifier");
1863
+ if (t)
1864
+ edges.push({ source: classId, relation: "extends", name: t.text, file: ctx.rel });
1865
+ }
1866
+ return edges;
1867
+ }
1868
+ if (ctx.lang === "swift") {
1869
+ // `class A: B, C` — each `inheritance_specifier` (a direct child of the
1870
+ // declaration; protocols and extensions carry them too) wraps a `user_type`
1871
+ // whose LAST direct `type_identifier` is the bare supertype name: a
1872
+ // module-qualified `Foundation.NSObject` reduces to `NSObject`, and generic
1873
+ // arguments live in nested nodes so they never leak in. Swift cannot say
1874
+ // syntactically whether a specifier is the superclass or a protocol
1875
+ // conformance (that needs the target's kind), so every edge is `extends` —
1876
+ // the same collapse Kotlin's delegation specifiers make.
1877
+ for (const child of node.namedChildren) {
1878
+ if (child.type !== "inheritance_specifier")
1879
+ continue;
1880
+ const ids = child.namedChildren
1881
+ .find((c) => c.type === "user_type")
1882
+ ?.namedChildren.filter((c) => c.type === "type_identifier");
1883
+ const t = ids?.length ? ids[ids.length - 1] : undefined;
1884
+ if (t)
1885
+ edges.push({ source: classId, relation: "extends", name: t.text, file: ctx.rel });
1886
+ }
1887
+ return edges;
1888
+ }
696
1889
  if (ctx.lang === "python") {
697
1890
  const supers = node.childForFieldName("superclasses"); // argument_list
698
1891
  for (const c of supers?.namedChildren ?? []) {
@@ -702,6 +1895,31 @@ function heritageEdges(node, classId, ctx) {
702
1895
  }
703
1896
  return edges;
704
1897
  }
1898
+ if (ctx.lang === "r") {
1899
+ // `node` is whatever describeR matched: a binary_operator for R6
1900
+ // (`Foo <- R6::R6Class(...)`) or the call itself for S4 (`setClass(...)`
1901
+ // is a bare top-level statement, essentially never assigned).
1902
+ const call = node.type === "binary_operator" ? node.childForFieldName("rhs") : node;
1903
+ if (call?.type !== "call")
1904
+ return edges;
1905
+ const callee = rCalleeName(call);
1906
+ if (callee === "R6Class") {
1907
+ // `inherit = ParentClass` — a bare identifier (the parent's own
1908
+ // generator variable), not a string; R6 supports single inheritance only.
1909
+ const value = rNamedArg(call, "inherit");
1910
+ if (value?.type === "identifier") {
1911
+ edges.push({ source: classId, relation: "extends", name: value.text, file: ctx.rel });
1912
+ }
1913
+ }
1914
+ else if (callee === "setClass") {
1915
+ // `contains = "Base"` or `contains = c("Base1", "Base2")` — S4 supports
1916
+ // multiple inheritance.
1917
+ for (const name of rStringOrCVector(rNamedArg(call, "contains"))) {
1918
+ edges.push({ source: classId, relation: "extends", name, file: ctx.rel });
1919
+ }
1920
+ }
1921
+ return edges;
1922
+ }
705
1923
  if (ctx.lang === "php") {
706
1924
  // `class C extends B implements I, J` → base_clause (extends) +
707
1925
  // class_interface_clause (implements); names may be namespace-qualified.
@@ -734,17 +1952,58 @@ function heritageEdges(node, classId, ctx) {
734
1952
  }
735
1953
  return edges;
736
1954
  }
737
- /** Every `type_identifier` under a heritage clause, so `implements A, B<C>` yields
738
- * each named type rather than the clause's raw text. */
739
- function typeIdentifiersIn(node) {
740
- const out = [];
1955
+ /**
1956
+ * The supertypes a heritage clause names, one node each — NOT every `type_identifier`
1957
+ * beneath it.
1958
+ *
1959
+ * `superclass` wraps a single type; `super_interfaces`/`extends_interfaces` wrap a
1960
+ * `type_list` of them. Descending blindly instead walked into `type_arguments`, so
1961
+ * `implements Comparable<Item>` reported `Item` as a supertype too.
1962
+ */
1963
+ function javaSupertypeEntries(clause) {
1964
+ const list = clause.namedChildren.find((c) => c.type === "type_list");
1965
+ return list ? [...list.namedChildren] : [...clause.namedChildren];
1966
+ }
1967
+ /**
1968
+ * What a supertype entry is CALLED, or null when this pass cannot say.
1969
+ *
1970
+ * Type arguments are erased, because they are not part of the supertype's identity:
1971
+ *
1972
+ * Base | Base<Item> -> Base
1973
+ *
1974
+ * A qualified name is kept WHOLE rather than reduced to its final segment:
1975
+ *
1976
+ * Outer.Inner | Outer.Inner<K> -> Outer.Inner
1977
+ *
1978
+ * Heritage keeps an unresolved base as the edge target by design ("usually an
1979
+ * external/imported type — keep the name"), so the full string is both truthful and
1980
+ * unable to false-match a node id, where a bare `Inner` could collide with an
1981
+ * unrelated in-repo type. That differs from construction (#103), which drops a
1982
+ * qualified name instead — construction has no keep-the-name contract to fall back on.
1983
+ */
1984
+ function javaSupertypeName(node) {
1985
+ if (!node)
1986
+ return null;
1987
+ if (node.type === "generic_type")
1988
+ return javaSupertypeName(node.namedChildren[0]);
1989
+ if (node.type === "scoped_type_identifier")
1990
+ return node.text;
1991
+ return node.type === "type_identifier" ? node.text : null;
1992
+ }
1993
+ /** The names a declaration binds as its own type parameters (`class C<T, U>` → T, U),
1994
+ * so they can never be mistaken for supertypes. */
1995
+ function javaTypeParameterNames(decl) {
1996
+ const params = decl.childForFieldName("type_parameters");
1997
+ if (!params)
1998
+ return new Set();
1999
+ const out = new Set();
741
2000
  const visit = (n) => {
742
2001
  if (n.type === "type_identifier")
743
- out.push(n.text);
2002
+ out.add(n.text);
744
2003
  for (const c of n.namedChildren)
745
2004
  visit(c);
746
2005
  };
747
- visit(node);
2006
+ visit(params);
748
2007
  return out;
749
2008
  }
750
2009
  function calleeName(node, lang) {
@@ -772,6 +2031,67 @@ function calleeName(node, lang) {
772
2031
  return { name: nameNode.text, viaMember: true, receiver: "this" };
773
2032
  return { name: nameNode.text, viaMember: true, receiver: javaReceiver(obj) };
774
2033
  }
2034
+ if (lang === "kotlin") {
2035
+ // `call_expression` = callee expression + `call_suffix`. A bare `foo()` names a
2036
+ // plain call; `obj.foo()` is a `navigation_expression` whose trailing
2037
+ // `navigation_suffix` holds the method name and whose object is the receiver.
2038
+ const target = node.namedChildren[0];
2039
+ if (target?.type === "simple_identifier")
2040
+ return { name: target.text, viaMember: false };
2041
+ if (target?.type === "navigation_expression") {
2042
+ const suffix = target.namedChildren.find((c) => c.type === "navigation_suffix");
2043
+ const name = suffix?.namedChildren.find((c) => c.type === "simple_identifier");
2044
+ const receiver = target.namedChildren[0];
2045
+ if (!name)
2046
+ return null;
2047
+ if (receiver?.type === "simple_identifier")
2048
+ return { name: name.text, viaMember: true, receiver: receiver.text };
2049
+ if (receiver?.type === "this_expression" || receiver?.type === "super_expression")
2050
+ return { name: name.text, viaMember: true, receiver: receiver.type === "this_expression" ? "this" : "super" };
2051
+ }
2052
+ return null;
2053
+ }
2054
+ if (lang === "swift") {
2055
+ // Same shape as Kotlin's: `call_expression` = callee expression + `call_suffix`.
2056
+ // A bare `foo()` names a plain call (this also covers `Animal()` initializer
2057
+ // calls, which have no distinguishing syntax); `obj.foo()` is a
2058
+ // `navigation_expression` whose trailing `navigation_suffix` holds the member
2059
+ // name and whose head is the receiver.
2060
+ const target = node.namedChildren[0];
2061
+ if (target?.type === "simple_identifier")
2062
+ return { name: target.text, viaMember: false };
2063
+ if (target?.type === "navigation_expression") {
2064
+ const suffix = target.namedChildren.find((c) => c.type === "navigation_suffix");
2065
+ const name = suffix?.namedChildren.find((c) => c.type === "simple_identifier");
2066
+ const receiver = target.namedChildren[0];
2067
+ if (!name)
2068
+ return null;
2069
+ if (receiver?.type === "simple_identifier")
2070
+ return { name: name.text, viaMember: true, receiver: receiver.text };
2071
+ if (receiver?.type === "self_expression" || receiver?.type === "super_expression")
2072
+ return {
2073
+ name: name.text,
2074
+ viaMember: true,
2075
+ receiver: receiver.type === "self_expression" ? "self" : "super",
2076
+ };
2077
+ if (receiver?.type === "navigation_expression") {
2078
+ // `self.repo.save()` — one hop off self is a field access and binds like
2079
+ // TS's `this.x`. Deeper chains and call-result receivers carry no
2080
+ // confident local clue, so those fall through with no receiver.
2081
+ const head = receiver.namedChildren[0];
2082
+ const field = receiver.namedChildren
2083
+ .find((c) => c.type === "navigation_suffix")
2084
+ ?.namedChildren.find((c) => c.type === "simple_identifier");
2085
+ if (head?.type === "self_expression" && field)
2086
+ return { name: name.text, viaMember: true, receiver: `self.${field.text}` };
2087
+ }
2088
+ // Still a member call even with an unknowable receiver (a chained call, a
2089
+ // literal, a subscript): recvType stays unset and resolve drops it rather
2090
+ // than guessing — same contract as Java's and TS's unknown receivers.
2091
+ return { name: name.text, viaMember: true };
2092
+ }
2093
+ return null;
2094
+ }
775
2095
  if (lang === "php")
776
2096
  return phpCallee(node);
777
2097
  const fn = node.childForFieldName("function");
@@ -794,6 +2114,48 @@ function calleeName(node, lang) {
794
2114
  const p = fn.childForFieldName("property") ?? fn.namedChildren.at(-1);
795
2115
  return p ? { name: p.text, viaMember: true, receiver: tsReceiver(fn) } : null;
796
2116
  }
2117
+ if (lang === "r" && (fn.type === "extract_operator" || fn.type === "namespace_operator")) {
2118
+ const rhs = fn.childForFieldName("rhs");
2119
+ if (rhs?.type !== "identifier")
2120
+ return null;
2121
+ if (fn.type === "extract_operator") {
2122
+ const lhs = fn.childForFieldName("lhs");
2123
+ if (lhs?.type === "identifier" && (lhs.text === "self" || lhs.text === "private")) {
2124
+ // R6 (Phase 2): `self$method()` / `private$method()` — resolves directly to
2125
+ // the enclosing class via ctx.enclosingClass, same mechanism (and same
2126
+ // magic receiver string) as Python/TS's self/cls/this — see
2127
+ // resolveRecvType, which already special-cases "self" generically.
2128
+ return { name: rhs.text, viaMember: true, receiver: "self" };
2129
+ }
2130
+ if (lhs?.type === "identifier" && lhs.text === "super") {
2131
+ // R6 (Phase 3): `super$method()` — R6's inheritance-dispatch keyword,
2132
+ // resolves directly to the PARENT class via ctx.rSuperClass (NOT
2133
+ // ctx.enclosingClass — that would wrongly find the current class's own
2134
+ // same-named override instead of climbing to the parent).
2135
+ return { name: rhs.text, viaMember: true, receiver: "super" };
2136
+ }
2137
+ // Any other `obj$method()` (Phase 4): still a PLAIN name match, not a
2138
+ // typed member call — there's no general field-type-binding table for
2139
+ // R6 composition (`private$other_obj$method()`), and a real codebase's
2140
+ // dominant field-assignment shape (constructor-parameter pass-through,
2141
+ // `do.call(class_var$new, ...)` dynamic dispatch) turned out to defeat
2142
+ // the simple "field <- SomeClass$new()" pattern every other language's
2143
+ // binding table relies on anyway — see plan_r_language_support.md's
2144
+ // Phase 2 "known gaps" and the follow-up investigation against a real
2145
+ // R6-heavy corpus. What DOES help: bare-name resolution must be allowed
2146
+ // to match a "method" node here, not just "function" — R6 methods are
2147
+ // always kind "method", so without `kinds` below, EVERY untyped `$`
2148
+ // call would be unconditionally unresolvable rather than just
2149
+ // occasionally ambiguous (resolve.ts already drops a genuinely
2150
+ // ambiguous bare-name match rather than guessing, so this only adds
2151
+ // resolutions for uniquely-named methods, never a wrong-class guess).
2152
+ return { name: rhs.text, viaMember: false, kinds: ["function", "method"] };
2153
+ }
2154
+ // `pkg::fun()` (qualified call) — always a real function/exported symbol,
2155
+ // never an R6 method (those are only ever reached via `$` on an instance),
2156
+ // so no need to widen the match kinds here.
2157
+ return { name: rhs.text, viaMember: false };
2158
+ }
797
2159
  return null;
798
2160
  }
799
2161
  /** The number of arguments at a Java call site (`method_invocation` or
@@ -938,13 +2300,29 @@ function tsReceiver(fn) {
938
2300
  }
939
2301
  return undefined;
940
2302
  }
2303
+ /** R has no import statement at the grammar level — `library(x)`, `require(x)`,
2304
+ * and `source("f.R")` are ordinary `call` nodes, indistinguishable from any other
2305
+ * call except by their callee name. This is call-SITE pattern matching, a first
2306
+ * for this function's normal node-type switch — every other language's import
2307
+ * shape is a dedicated grammar construct. */
2308
+ const R_IMPORT_CALLS = new Set(["library", "require", "source"]);
941
2309
  function isImport(node, lang) {
942
2310
  // Go: match the per-import leaf, so single (`import "fmt"`) and grouped
943
2311
  // (`import ( … )`) forms each yield one edge as the walk recurses into the list.
944
2312
  if (lang === "go")
945
2313
  return node.type === "import_spec";
2314
+ if (lang === "r") {
2315
+ if (node.type !== "call")
2316
+ return false;
2317
+ const fn = node.childForFieldName("function");
2318
+ return fn?.type === "identifier" && R_IMPORT_CALLS.has(fn.text);
2319
+ }
946
2320
  if (lang === "java")
947
2321
  return node.type === "import_declaration";
2322
+ if (lang === "kotlin")
2323
+ return node.type === "import_header";
2324
+ if (lang === "swift")
2325
+ return node.type === "import_declaration";
948
2326
  // PHP: one edge per imported symbol — the clause leaf inside a (possibly
949
2327
  // grouped) `use A\B, C\D;` / `use A\{B, C};` declaration.
950
2328
  if (lang === "php")
@@ -967,6 +2345,14 @@ function importSpecifier(node, lang) {
967
2345
  const path = node.childForFieldName("path") ?? node.namedChildren.at(-1);
968
2346
  return path ? path.text.replace(/^["`]|["`]$/g, "") : null;
969
2347
  }
2348
+ if (lang === "r") {
2349
+ // library(pkg) / library("pkg") / require(pkg) / source("f.R") — the target is
2350
+ // always the first (and normally only) positional argument, bare symbol or string.
2351
+ const value = rCallArgs(node)[0]?.childForFieldName("value") ?? null;
2352
+ if (value?.type === "identifier")
2353
+ return value.text;
2354
+ return rStringContent(value);
2355
+ }
970
2356
  if (lang === "java") {
971
2357
  // `import a.b.C;` / `import static a.b.C.d;` / `import a.b.*;` — the fully
972
2358
  // qualified name is the scoped_identifier; a wildcard `*` is a separate token
@@ -974,6 +2360,20 @@ function importSpecifier(node, lang) {
974
2360
  const id = node.namedChildren.find((c) => c.type === "scoped_identifier" || c.type === "identifier");
975
2361
  return id?.text ?? null;
976
2362
  }
2363
+ if (lang === "kotlin") {
2364
+ // `import com.example.Foo` — the dotted path is the `identifier` child. A
2365
+ // wildcard (`import a.b.*`) and an `as` alias are separate children, so the
2366
+ // identifier text is already the module path (wildcards dropped, like Java).
2367
+ return node.namedChildren.find((c) => c.type === "identifier")?.text ?? null;
2368
+ }
2369
+ if (lang === "swift") {
2370
+ // `import UIKit` / `import struct Foundation.Date` — the dotted path is the
2371
+ // `identifier` child (an import-kind keyword like `struct` is a separate
2372
+ // token). Swift imports name MODULES, not files, so the specifier resolves
2373
+ // to a repo file only when a same-named module target exists; external
2374
+ // frameworks stay as unresolved (but truthful) import intents.
2375
+ return node.namedChildren.find((c) => c.type === "identifier")?.text ?? null;
2376
+ }
977
2377
  const str = node.namedChildren.find((c) => c.type === "string");
978
2378
  if (!str)
979
2379
  return null;