@nanonets/graft 0.12.0 → 0.13.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.
- package/README.md +8 -6
- package/dist/ai/crux.d.ts.map +1 -1
- package/dist/ai/crux.js +64 -1
- package/dist/ai/crux.js.map +1 -1
- package/dist/ask/ask.d.ts +7 -0
- package/dist/ask/ask.d.ts.map +1 -1
- package/dist/ask/ask.js +30 -9
- package/dist/ask/ask.js.map +1 -1
- package/dist/ask/file-selection.d.ts +18 -0
- package/dist/ask/file-selection.d.ts.map +1 -0
- package/dist/ask/file-selection.js +41 -0
- package/dist/ask/file-selection.js.map +1 -0
- package/dist/blast/blast-cli.js +1 -1
- package/dist/blast/blast-cli.js.map +1 -1
- package/dist/blast/evidence.d.ts +51 -0
- package/dist/blast/evidence.d.ts.map +1 -0
- package/dist/blast/evidence.js +162 -0
- package/dist/blast/evidence.js.map +1 -0
- package/dist/blast/render.d.ts +11 -2
- package/dist/blast/render.d.ts.map +1 -1
- package/dist/blast/render.js +20 -4
- package/dist/blast/render.js.map +1 -1
- package/dist/blast/viz.d.ts +20 -0
- package/dist/blast/viz.d.ts.map +1 -1
- package/dist/blast/viz.js +3 -157
- package/dist/blast/viz.js.map +1 -1
- package/dist/claude/hooks.d.ts.map +1 -1
- package/dist/claude/hooks.js +56 -8
- package/dist/claude/hooks.js.map +1 -1
- package/dist/claude/state.d.ts +1 -1
- package/dist/claude/state.d.ts.map +1 -1
- package/dist/claude/state.js +1 -1
- package/dist/claude/state.js.map +1 -1
- package/dist/claude/stats.d.ts.map +1 -1
- package/dist/claude/stats.js +2 -1
- package/dist/claude/stats.js.map +1 -1
- package/dist/claude/sync-run.d.ts.map +1 -1
- package/dist/claude/sync-run.js +10 -2
- package/dist/claude/sync-run.js.map +1 -1
- package/dist/cli.js +22 -0
- package/dist/cli.js.map +1 -1
- package/dist/graph/bindings.d.ts +24 -3
- package/dist/graph/bindings.d.ts.map +1 -1
- package/dist/graph/bindings.js +56 -2
- package/dist/graph/bindings.js.map +1 -1
- package/dist/graph/check.d.ts +3 -0
- package/dist/graph/check.d.ts.map +1 -1
- package/dist/graph/check.js +22 -5
- package/dist/graph/check.js.map +1 -1
- package/dist/graph/enrich.d.ts.map +1 -1
- package/dist/graph/enrich.js +29 -9
- package/dist/graph/enrich.js.map +1 -1
- package/dist/graph/extract.d.ts +14 -1
- package/dist/graph/extract.d.ts.map +1 -1
- package/dist/graph/extract.js +1096 -40
- package/dist/graph/extract.js.map +1 -1
- package/dist/graph/generic.d.ts.map +1 -1
- package/dist/graph/generic.js +39 -6
- package/dist/graph/generic.js.map +1 -1
- package/dist/graph/queries/dart.scm +42 -0
- package/dist/graph/queries/lua.scm +36 -0
- package/dist/graph/queries/nix.scm +32 -0
- package/dist/graph/resolve.d.ts.map +1 -1
- package/dist/graph/resolve.js +106 -22
- package/dist/graph/resolve.js.map +1 -1
- package/dist/graph/traverse-cli.d.ts +13 -2
- package/dist/graph/traverse-cli.d.ts.map +1 -1
- package/dist/graph/traverse-cli.js +25 -4
- package/dist/graph/traverse-cli.js.map +1 -1
- package/dist/graph/workspace.d.ts.map +1 -1
- package/dist/graph/workspace.js +26 -2
- package/dist/graph/workspace.js.map +1 -1
- package/dist/hosts/mcp-config.d.ts.map +1 -1
- package/dist/hosts/mcp-config.js +8 -0
- package/dist/hosts/mcp-config.js.map +1 -1
- package/dist/hosts/registry.d.ts.map +1 -1
- package/dist/hosts/registry.js +20 -0
- package/dist/hosts/registry.js.map +1 -1
- package/dist/ingest/fs.d.ts.map +1 -1
- package/dist/ingest/fs.js +6 -0
- package/dist/ingest/fs.js.map +1 -1
- package/dist/telemetry/key.js +1 -1
- package/dist/util/state.d.ts +13 -0
- package/dist/util/state.d.ts.map +1 -1
- package/dist/util/state.js +20 -2
- package/dist/util/state.js.map +1 -1
- package/package.json +12 -5
package/dist/graph/extract.js
CHANGED
|
@@ -10,7 +10,9 @@ 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";
|
|
14
16
|
import PHP from "tree-sitter-php";
|
|
15
17
|
import { basename } from "node:path";
|
|
16
18
|
import { contentHash } from "../util/id.js";
|
|
@@ -43,7 +45,12 @@ const EXTENSIONS = [
|
|
|
43
45
|
{ ext: ".py", grammar: "python", label: "python" },
|
|
44
46
|
{ ext: ".go", grammar: "go", label: "go" },
|
|
45
47
|
{ ext: ".java", grammar: "java", label: "java" },
|
|
48
|
+
{ ext: ".kt", grammar: "kotlin", label: "kotlin" },
|
|
49
|
+
{ ext: ".kts", grammar: "kotlin", label: "kotlin" },
|
|
46
50
|
{ ext: ".php", grammar: "php", label: "php" },
|
|
51
|
+
// `entryFor` lower-cases the path before matching, so this one entry covers
|
|
52
|
+
// both `.R` (the conventional case in real R codebases) and `.r`.
|
|
53
|
+
{ ext: ".r", grammar: "r", label: "r" },
|
|
47
54
|
];
|
|
48
55
|
function entryFor(path) {
|
|
49
56
|
const p = path.toLowerCase();
|
|
@@ -124,6 +131,11 @@ const GO_KINDS = {
|
|
|
124
131
|
function_declaration: "function",
|
|
125
132
|
method_declaration: "method",
|
|
126
133
|
};
|
|
134
|
+
// R: `function_definition` carries no name field at all (unlike every other
|
|
135
|
+
// supported language) — its identifier always comes from context (an
|
|
136
|
+
// assignment's other side), resolved dynamically in describeR(). Empty, like
|
|
137
|
+
// Go's own table — never consulted, kept only to satisfy KINDS_BY_LANG's type.
|
|
138
|
+
const R_KINDS = {};
|
|
127
139
|
// Java: a record is a nominal data carrier, so it takes "struct" — the same role
|
|
128
140
|
// Go's struct plays — rather than "class", which would make a service and a DTO
|
|
129
141
|
// indistinguishable in a repo where DTOs are most of the type surface.
|
|
@@ -140,6 +152,18 @@ const JAVA_KINDS = {
|
|
|
140
152
|
/** Java type declarations: they set `enclosingClass` for the methods nested in them,
|
|
141
153
|
* which "class"-only logic would miss for a record's or interface's members. */
|
|
142
154
|
const JAVA_TYPE_KINDS = new Set(["class", "interface", "enum", "struct"]);
|
|
155
|
+
const KOTLIN_KINDS = {
|
|
156
|
+
class_declaration: "class", // → "interface" / "enum" / "interface" (annotation) in describeKotlin
|
|
157
|
+
object_declaration: "class", // a singleton object is class-like (companion objects included)
|
|
158
|
+
function_declaration: "function", // → "method" inside a type (resolved in the walk)
|
|
159
|
+
secondary_constructor: "method", // the class's own secondary constructor
|
|
160
|
+
type_alias: "type",
|
|
161
|
+
property_declaration: "variable", // top-level `val`/`var` only (fields resolved in the walk)
|
|
162
|
+
};
|
|
163
|
+
/** Kotlin type declarations: they set `enclosingClass` for the members nested in them.
|
|
164
|
+
* "class" also covers object_declaration (it maps to "class"); interface/enum are the
|
|
165
|
+
* same class_declaration node rekinded in describeKotlin, so all three land in the set. */
|
|
166
|
+
const KOTLIN_TYPE_KINDS = new Set(["class", "interface", "enum"]);
|
|
143
167
|
// PHP: definition node types are all distinct (no py-style function→method
|
|
144
168
|
// promotion needed — a class body uses `method_declaration`, not
|
|
145
169
|
// `function_definition`). `trait_declaration` maps to the PHP-only `trait` kind.
|
|
@@ -156,7 +180,9 @@ const KINDS_BY_LANG = {
|
|
|
156
180
|
tsx: TS_KINDS,
|
|
157
181
|
python: PY_KINDS,
|
|
158
182
|
go: GO_KINDS,
|
|
183
|
+
r: R_KINDS,
|
|
159
184
|
java: JAVA_KINDS,
|
|
185
|
+
kotlin: KOTLIN_KINDS,
|
|
160
186
|
php: PHP_KINDS,
|
|
161
187
|
};
|
|
162
188
|
/**
|
|
@@ -174,12 +200,14 @@ const CALL_TYPES = {
|
|
|
174
200
|
python: new Set(["call"]),
|
|
175
201
|
go: new Set(["call_expression"]),
|
|
176
202
|
java: new Set(["method_invocation", "object_creation_expression"]),
|
|
203
|
+
kotlin: new Set(["call_expression"]),
|
|
177
204
|
php: new Set([
|
|
178
205
|
"function_call_expression",
|
|
179
206
|
"member_call_expression",
|
|
180
207
|
"nullsafe_member_call_expression",
|
|
181
208
|
"scoped_call_expression",
|
|
182
209
|
]),
|
|
210
|
+
r: new Set(["call"]),
|
|
183
211
|
};
|
|
184
212
|
const FUNCTION_VALUE_TYPES = new Set([
|
|
185
213
|
"arrow_function",
|
|
@@ -187,13 +215,16 @@ const FUNCTION_VALUE_TYPES = new Set([
|
|
|
187
215
|
"function_expression",
|
|
188
216
|
"generator_function",
|
|
189
217
|
]);
|
|
218
|
+
const EMPTY_SET = new Set();
|
|
190
219
|
const parser = new Parser();
|
|
191
220
|
const GRAMMARS = {
|
|
192
221
|
typescript: TypeScript.typescript,
|
|
193
222
|
tsx: TypeScript.tsx,
|
|
194
223
|
python: Python,
|
|
195
224
|
go: Go,
|
|
225
|
+
r: R,
|
|
196
226
|
java: Java,
|
|
227
|
+
kotlin: Kotlin,
|
|
197
228
|
php: PHP.php,
|
|
198
229
|
};
|
|
199
230
|
/** tree-sitter's string `parse()` fails with "Invalid argument" on any input
|
|
@@ -210,6 +241,7 @@ export function extractFile(rel, source, lang) {
|
|
|
210
241
|
const root = parseSource(source);
|
|
211
242
|
const bindings = collectBindings(root, lang);
|
|
212
243
|
const importedSymbols = collectImportedSymbols(root, lang);
|
|
244
|
+
const rGenerics = lang === "r" ? collectRGenerics(root) : EMPTY_SET;
|
|
213
245
|
const nodes = [
|
|
214
246
|
{
|
|
215
247
|
id: rel,
|
|
@@ -240,6 +272,9 @@ export function extractFile(rel, source, lang) {
|
|
|
240
272
|
enclosingClass: null,
|
|
241
273
|
goReceiverVar: null,
|
|
242
274
|
importedSymbols,
|
|
275
|
+
rR6Access: null,
|
|
276
|
+
rGenerics,
|
|
277
|
+
rSuperClass: null,
|
|
243
278
|
};
|
|
244
279
|
// Every id minted this file, seeded with the file node's own id (`rel`) so a
|
|
245
280
|
// top-level definition can never collide with it. Threaded as its own
|
|
@@ -247,8 +282,7 @@ export function extractFile(rel, source, lang) {
|
|
|
247
282
|
// childCtx, so a by-ref Set there would read as ordinary inherited context
|
|
248
283
|
// when it's actually accidental shared mutable state across the whole walk.
|
|
249
284
|
const minted = new Set([rel]);
|
|
250
|
-
|
|
251
|
-
walk(child, ctx, nodes, rawEdges, minted);
|
|
285
|
+
walkNamedChildren(root.namedChildren, ctx, nodes, rawEdges, minted);
|
|
252
286
|
// nodes[0] is the file node; the rest are its symbols. Index the module-level
|
|
253
287
|
// residual on the file node so a term outside every symbol still surfaces it.
|
|
254
288
|
nodes[0].body_text = fileResidual(source, nodes.slice(1));
|
|
@@ -268,6 +302,97 @@ export function mintId(base, minted) {
|
|
|
268
302
|
minted.add(id);
|
|
269
303
|
return id;
|
|
270
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* tree-sitter-php 0.23.x cannot parse a `const` inside an enum body (#145). An
|
|
307
|
+
* array initializer collapses the whole `enum_declaration` into ERROR; the
|
|
308
|
+
* method is recovered as a sibling `function_definition`. 0.24.2 parses this
|
|
309
|
+
* natively but is ABI 15 and cannot load on Graft's tree-sitter 0.21.1.
|
|
310
|
+
*
|
|
311
|
+
* Bound: only an ERROR that already contains `enum_case` + `name` is treated as
|
|
312
|
+
* a collapsed enum. Only `const_declaration` / `function_definition` /
|
|
313
|
+
* `method_declaration` / ERROR siblings are absorbed, stopping at a `}` ERROR.
|
|
314
|
+
* Unknown ERROR nodes are still walked, never mapped to a type. Clean
|
|
315
|
+
* class/enum trees are `class_declaration` / `enum_declaration` and skip this.
|
|
316
|
+
*/
|
|
317
|
+
function phpCollapsedEnumName(node) {
|
|
318
|
+
if (node.type !== "ERROR")
|
|
319
|
+
return null;
|
|
320
|
+
if (!node.namedChildren.some((c) => c.type === "enum_case"))
|
|
321
|
+
return null;
|
|
322
|
+
return node.namedChildren.find((c) => c.type === "name")?.text ?? null;
|
|
323
|
+
}
|
|
324
|
+
function phpCollapsedEnumHold(node) {
|
|
325
|
+
return (node.type === "const_declaration" ||
|
|
326
|
+
node.type === "function_definition" ||
|
|
327
|
+
node.type === "method_declaration" ||
|
|
328
|
+
node.type === "ERROR");
|
|
329
|
+
}
|
|
330
|
+
function phpCollapsedEnumClose(node) {
|
|
331
|
+
return node.type === "ERROR" && node.text.trim() === "}";
|
|
332
|
+
}
|
|
333
|
+
function walkNamedChildren(children, ctx, out, edges, minted) {
|
|
334
|
+
if (ctx.lang !== "php") {
|
|
335
|
+
for (const child of children)
|
|
336
|
+
walk(child, ctx, out, edges, minted);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
for (let i = 0; i < children.length;) {
|
|
340
|
+
const n = children[i];
|
|
341
|
+
const enumName = phpCollapsedEnumName(n);
|
|
342
|
+
if (enumName) {
|
|
343
|
+
const group = [n];
|
|
344
|
+
let j = i + 1;
|
|
345
|
+
while (j < children.length && phpCollapsedEnumHold(children[j])) {
|
|
346
|
+
const next = children[j];
|
|
347
|
+
group.push(next);
|
|
348
|
+
j++;
|
|
349
|
+
if (phpCollapsedEnumClose(next))
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
emitPhpCollapsedEnum(enumName, n, group, ctx, out, edges, minted);
|
|
353
|
+
i = j;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
walk(n, ctx, out, edges, minted);
|
|
357
|
+
i++;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function emitPhpCollapsedEnum(name, errorNode, group, ctx, out, edges, minted) {
|
|
361
|
+
const last = group[group.length - 1];
|
|
362
|
+
const id = mintId(`${ctx.rel}#${[...ctx.scope, name].join(".")}`, minted);
|
|
363
|
+
const body = ctx.source.slice(errorNode.startIndex, last.endIndex);
|
|
364
|
+
out.push({
|
|
365
|
+
id,
|
|
366
|
+
name,
|
|
367
|
+
kind: "enum",
|
|
368
|
+
path: ctx.rel,
|
|
369
|
+
span: `L${errorNode.startPosition.row + 1}-L${last.endPosition.row + 1}`,
|
|
370
|
+
signature: `enum ${name}`,
|
|
371
|
+
exported: true,
|
|
372
|
+
origin: "ast",
|
|
373
|
+
body_hash: contentHash(body),
|
|
374
|
+
body_text: searchBody(body),
|
|
375
|
+
summary_state: "pending",
|
|
376
|
+
summary: null,
|
|
377
|
+
crux: null,
|
|
378
|
+
});
|
|
379
|
+
edges.push({ source: ctx.parentId, relation: "contains", targetId: id, file: ctx.rel });
|
|
380
|
+
const childCtx = {
|
|
381
|
+
...ctx,
|
|
382
|
+
scope: [...ctx.scope, name],
|
|
383
|
+
enclosingKind: "enum",
|
|
384
|
+
parentId: id,
|
|
385
|
+
};
|
|
386
|
+
for (const g of group) {
|
|
387
|
+
if (phpCollapsedEnumClose(g))
|
|
388
|
+
continue;
|
|
389
|
+
if (phpCollapsedEnumName(g)) {
|
|
390
|
+
walkNamedChildren(g.namedChildren, childCtx, out, edges, minted);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
walk(g, childCtx, out, edges, minted);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
271
396
|
function walk(node, ctx, out, edges, minted) {
|
|
272
397
|
const desc = describe(node, ctx);
|
|
273
398
|
if (desc) {
|
|
@@ -279,10 +404,14 @@ function walk(node, ctx, out, edges, minted) {
|
|
|
279
404
|
const isGoMethod = ctx.lang === "go" && node.type === "method_declaration";
|
|
280
405
|
// The bare name of this node's OWN immediate enclosing class/receiver — for a
|
|
281
406
|
// Go method that's its receiver type (methods aren't nested, so ctx.enclosingClass
|
|
282
|
-
// wouldn't see it); for
|
|
283
|
-
//
|
|
407
|
+
// wouldn't see it); for an R S3/S4 method it's the qualifier/class describeR
|
|
408
|
+
// already resolved (desc.owner — these don't lexically nest inside their class
|
|
409
|
+
// either); for every other method it's simply what the nearest ancestor class
|
|
410
|
+
// already set as ctx.enclosingClass. Only method nodes carry it — resolve.ts's
|
|
284
411
|
// ownerMethod index is the sole consumer (see NodeV1.owner's doc comment).
|
|
285
|
-
const owner = desc.kind === "method"
|
|
412
|
+
const owner = desc.kind === "method"
|
|
413
|
+
? (isGoMethod ? (goReceiverType(node) ?? undefined) : (desc.owner ?? ctx.enclosingClass ?? undefined))
|
|
414
|
+
: undefined;
|
|
286
415
|
out.push({
|
|
287
416
|
id,
|
|
288
417
|
name: desc.name,
|
|
@@ -294,11 +423,15 @@ function walk(node, ctx, out, edges, minted) {
|
|
|
294
423
|
? !desc.name.startsWith("_")
|
|
295
424
|
: ctx.lang === "go"
|
|
296
425
|
? goExported(desc.name)
|
|
297
|
-
: ctx.lang === "
|
|
298
|
-
?
|
|
299
|
-
: ctx.lang === "
|
|
300
|
-
?
|
|
301
|
-
:
|
|
426
|
+
: ctx.lang === "r"
|
|
427
|
+
? rExported(desc.name, ctx, node)
|
|
428
|
+
: ctx.lang === "java"
|
|
429
|
+
? javaExported(node)
|
|
430
|
+
: ctx.lang === "kotlin"
|
|
431
|
+
? kotlinExported(node)
|
|
432
|
+
: ctx.lang === "php"
|
|
433
|
+
? phpExported(node)
|
|
434
|
+
: tsExported(node),
|
|
302
435
|
origin: "ast",
|
|
303
436
|
body_hash: contentHash(desc.hashNode.text),
|
|
304
437
|
body_text: searchBody(desc.hashNode.text),
|
|
@@ -314,13 +447,16 @@ function walk(node, ctx, out, edges, minted) {
|
|
|
314
447
|
// class heritage — in Java an interface may also `extends`, and a record/enum
|
|
315
448
|
// may `implements`, so every type declaration is a heritage site, not just a class.
|
|
316
449
|
const javaTypeDecl = ctx.lang === "java" && JAVA_TYPE_KINDS.has(desc.kind);
|
|
317
|
-
|
|
450
|
+
const kotlinTypeDecl = ctx.lang === "kotlin" && KOTLIN_TYPE_KINDS.has(desc.kind);
|
|
451
|
+
if (desc.kind === "class" || javaTypeDecl || kotlinTypeDecl)
|
|
318
452
|
edges.push(...heritageEdges(node, id, ctx));
|
|
319
|
-
|
|
453
|
+
if (ctx.lang === "php")
|
|
454
|
+
edges.push(...phpAttributeReferenceEdges(node, id, ctx));
|
|
455
|
+
const enclosingClass = desc.kind === "class" || javaTypeDecl || kotlinTypeDecl
|
|
320
456
|
? desc.name
|
|
321
457
|
: isGoMethod
|
|
322
458
|
? goReceiverType(node)
|
|
323
|
-
: ctx.enclosingClass;
|
|
459
|
+
: (desc.owner ?? ctx.enclosingClass);
|
|
324
460
|
const childCtx = {
|
|
325
461
|
...ctx,
|
|
326
462
|
scope: [...ctx.scope, idPart],
|
|
@@ -331,15 +467,71 @@ function walk(node, ctx, out, edges, minted) {
|
|
|
331
467
|
importedSymbols: desc.kind === "function" || desc.kind === "method"
|
|
332
468
|
? withoutShadowedImports(ctx.importedSymbols, node)
|
|
333
469
|
: ctx.importedSymbols,
|
|
470
|
+
// Reset on every new definition — this is a purely local marker for "we're
|
|
471
|
+
// still inside THIS class-defining call's own public=/private=/active=
|
|
472
|
+
// argument chain," not something that should leak into a nested definition
|
|
473
|
+
// (a method's own body, or — vanishingly rare but possible — another class
|
|
474
|
+
// defined inside one).
|
|
475
|
+
rR6Access: null,
|
|
476
|
+
// Unlike rR6Access, only reset when entering a genuinely new class (so it
|
|
477
|
+
// stays live through a method's whole body, where super$ calls actually
|
|
478
|
+
// happen) — inherited unchanged for every other definition kind.
|
|
479
|
+
rSuperClass: desc.kind === "class" ? (ctx.lang === "r" ? rR6ParentClass(node) : null) : ctx.rSuperClass,
|
|
334
480
|
};
|
|
335
|
-
|
|
336
|
-
walk(child, childCtx, out, edges, minted);
|
|
481
|
+
walkNamedChildren(node.namedChildren, childCtx, out, edges, minted);
|
|
337
482
|
return;
|
|
338
483
|
}
|
|
484
|
+
// R6 (Phase 2): `public =`/`private =`/`active =` inside an R6Class(...) call's
|
|
485
|
+
// own arguments is a `list(...)` call whose named entries become methods —
|
|
486
|
+
// this is R's version of a class body, but structurally it's several levels of
|
|
487
|
+
// ordinary call/argument nodes rather than a dedicated grammar construct, so it
|
|
488
|
+
// needs its own interception (mirrors how every other stateful/pattern-matched
|
|
489
|
+
// R construct in this walk needs one). `ctx.enclosingKind === "class"` scopes
|
|
490
|
+
// this to the class-defining call's own direct structure — once we're inside
|
|
491
|
+
// an actual method's body, enclosingKind has moved on to "method" and an
|
|
492
|
+
// unrelated nested `list(public = list(fn = function() {}))` elsewhere won't
|
|
493
|
+
// be misread as another class body.
|
|
494
|
+
if (ctx.lang === "r" &&
|
|
495
|
+
ctx.enclosingKind === "class" &&
|
|
496
|
+
ctx.rR6Access === null &&
|
|
497
|
+
node.type === "argument") {
|
|
498
|
+
const argName = node.childForFieldName("name");
|
|
499
|
+
const value = node.childForFieldName("value");
|
|
500
|
+
if (argName?.type === "identifier" &&
|
|
501
|
+
(argName.text === "public" || argName.text === "private" || argName.text === "active") &&
|
|
502
|
+
value?.type === "call" &&
|
|
503
|
+
rCalleeName(value) === "list") {
|
|
504
|
+
const access = argName.text;
|
|
505
|
+
for (const entry of rCallArgs(value)) {
|
|
506
|
+
walk(entry, { ...ctx, rR6Access: access }, out, edges, minted);
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
339
511
|
// not a definition — capture calls/imports/references, then descend with the same context
|
|
512
|
+
// R's `call` node is also its ONLY vehicle for library()/require()/source() —
|
|
513
|
+
// there's no separate import-statement grammar construct to key off, so isImport
|
|
514
|
+
// must be checked before the generic calls path or every import call would be
|
|
515
|
+
// captured as a (harmlessly unresolvable, but wrong) `calls` edge instead.
|
|
340
516
|
const callTypes = CALL_TYPES[ctx.lang];
|
|
341
|
-
if (
|
|
342
|
-
const
|
|
517
|
+
if (isImport(node, ctx.lang)) {
|
|
518
|
+
const spec = importSpecifier(node, ctx.lang);
|
|
519
|
+
if (spec)
|
|
520
|
+
edges.push({ source: ctx.rel, relation: "imports", specifier: spec, file: ctx.rel });
|
|
521
|
+
// Imported identifiers are declarations, not uses. The import-binding pass
|
|
522
|
+
// above already recorded them, so do not descend and emit false references.
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
else if (callTypes.has(node.type)) {
|
|
526
|
+
// R6Class(...) / a Phase-5 mixin list(...) is already consumed by its
|
|
527
|
+
// enclosing binary_operator as the class definition (see describeR) — the
|
|
528
|
+
// walk still reaches this SAME call node again, recursing generically to
|
|
529
|
+
// find its public=/private=/active= arguments (there's no other path to
|
|
530
|
+
// them), and it must not ALSO be treated as an ordinary call to a
|
|
531
|
+
// function literally named "R6Class"/"list".
|
|
532
|
+
const consumedCallee = ctx.lang === "r" && node.type === "call" ? rCalleeName(node) : null;
|
|
533
|
+
const isConsumedRClassCall = consumedCallee === "R6Class" || (consumedCallee === "list" && rIsMixinContainer(node));
|
|
534
|
+
const callee = isConsumedRClassCall ? null : calleeName(node, ctx.lang);
|
|
343
535
|
if (callee) {
|
|
344
536
|
const callEdge = {
|
|
345
537
|
source: ctx.parentId,
|
|
@@ -347,6 +539,7 @@ function walk(node, ctx, out, edges, minted) {
|
|
|
347
539
|
name: callee.name,
|
|
348
540
|
viaMember: callee.viaMember,
|
|
349
541
|
file: ctx.rel,
|
|
542
|
+
...(callee.kinds ? { kinds: callee.kinds } : {}),
|
|
350
543
|
};
|
|
351
544
|
// Java only: the call site's argument count, to pick the right overload.
|
|
352
545
|
const argCount = ctx.lang === "java" ? javaArgCount(node) : undefined;
|
|
@@ -356,14 +549,6 @@ function walk(node, ctx, out, edges, minted) {
|
|
|
356
549
|
edges.push(recvType ? { ...callEdge, recvType } : callEdge);
|
|
357
550
|
}
|
|
358
551
|
}
|
|
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
552
|
else if (ctx.lang === "php" && node.type === "use_declaration") {
|
|
368
553
|
// Trait composition inside a class body (`use HasFactory, Notifiable;`).
|
|
369
554
|
// Modelled as `implements`: like an interface, a trait is a contract of
|
|
@@ -389,6 +574,54 @@ function walk(node, ctx, out, edges, minted) {
|
|
|
389
574
|
});
|
|
390
575
|
}
|
|
391
576
|
}
|
|
577
|
+
// Java anonymous class (`new Type() { … }`): tree-sitter-java has no
|
|
578
|
+
// `anonymous_class` node (unlike PHP) — the body is an optional `class_body`
|
|
579
|
+
// on `object_creation_expression`. Mint `{anonymous}` (mirroring PHP #144 /
|
|
580
|
+
// `{closure}`) so nested methods take that owner instead of the enclosing
|
|
581
|
+
// type's, which otherwise pollutes `ownerMethod` and steals real call edges
|
|
582
|
+
// (#161). The constructor call edge above still fires for `new Type()`.
|
|
583
|
+
if (ctx.lang === "java" && node.type === "object_creation_expression") {
|
|
584
|
+
const body = node.namedChildren.find((c) => c.type === "class_body");
|
|
585
|
+
if (body) {
|
|
586
|
+
const idPart = "{anonymous}";
|
|
587
|
+
const base = `${ctx.rel}#${[...ctx.scope, idPart].join(".")}`;
|
|
588
|
+
const id = mintId(base, minted);
|
|
589
|
+
out.push({
|
|
590
|
+
id,
|
|
591
|
+
name: "{anonymous}",
|
|
592
|
+
kind: "class",
|
|
593
|
+
path: ctx.rel,
|
|
594
|
+
span: `L${node.startPosition.row + 1}-L${node.endPosition.row + 1}`,
|
|
595
|
+
signature: clean(ctx.source.slice(node.startIndex, body.startIndex)),
|
|
596
|
+
exported: false,
|
|
597
|
+
origin: "ast",
|
|
598
|
+
body_hash: contentHash(node.text),
|
|
599
|
+
body_text: searchBody(node.text),
|
|
600
|
+
summary_state: "pending",
|
|
601
|
+
summary: null,
|
|
602
|
+
crux: null,
|
|
603
|
+
});
|
|
604
|
+
edges.push({ source: ctx.parentId, relation: "contains", targetId: id, file: ctx.rel });
|
|
605
|
+
// Single supertype from `new Type()`: emit `implements` so an interface
|
|
606
|
+
// target resolves (adapters are the common case; a class target drops
|
|
607
|
+
// under resolve's implements kind filter — drop-not-guess).
|
|
608
|
+
const superName = javaConstructedTypeName(node.childForFieldName("type"));
|
|
609
|
+
if (superName) {
|
|
610
|
+
edges.push({ source: id, relation: "implements", name: superName, file: ctx.rel });
|
|
611
|
+
}
|
|
612
|
+
const anonCtx = {
|
|
613
|
+
...ctx,
|
|
614
|
+
scope: [...ctx.scope, idPart],
|
|
615
|
+
enclosingKind: "class",
|
|
616
|
+
parentId: id,
|
|
617
|
+
enclosingClass: "{anonymous}",
|
|
618
|
+
};
|
|
619
|
+
for (const child of node.namedChildren) {
|
|
620
|
+
walk(child, child.type === "class_body" ? anonCtx : ctx, out, edges, minted);
|
|
621
|
+
}
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
392
625
|
for (const child of node.namedChildren)
|
|
393
626
|
walk(child, ctx, out, edges, minted);
|
|
394
627
|
}
|
|
@@ -398,15 +631,32 @@ function walk(node, ctx, out, edges, minted) {
|
|
|
398
631
|
* the exported symbol name, so wiring them would require guessing.
|
|
399
632
|
*/
|
|
400
633
|
function collectImportedSymbols(root, lang) {
|
|
401
|
-
|
|
402
|
-
|
|
634
|
+
if (lang === "typescript" || lang === "tsx") {
|
|
635
|
+
const out = new Map();
|
|
636
|
+
const visit = (node) => {
|
|
637
|
+
if (node.type === "import_statement") {
|
|
638
|
+
const specifier = importSpecifier(node, lang);
|
|
639
|
+
if (!specifier)
|
|
640
|
+
return;
|
|
641
|
+
collectTsImportBindings(node, specifier, out);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
for (const child of node.namedChildren)
|
|
645
|
+
visit(child);
|
|
646
|
+
};
|
|
647
|
+
visit(root);
|
|
403
648
|
return out;
|
|
649
|
+
}
|
|
650
|
+
if (lang === "php")
|
|
651
|
+
return collectPhpImportedSymbols(root);
|
|
652
|
+
return new Map();
|
|
653
|
+
}
|
|
654
|
+
/** PHP `use` bindings: local alias → { exported name, FQN specifier }. */
|
|
655
|
+
function collectPhpImportedSymbols(root) {
|
|
656
|
+
const out = new Map();
|
|
404
657
|
const visit = (node) => {
|
|
405
|
-
if (node.type === "
|
|
406
|
-
|
|
407
|
-
if (!specifier)
|
|
408
|
-
return;
|
|
409
|
-
collectTsImportBindings(node, specifier, out);
|
|
658
|
+
if (node.type === "namespace_use_declaration") {
|
|
659
|
+
collectPhpUseDeclaration(node, out);
|
|
410
660
|
return;
|
|
411
661
|
}
|
|
412
662
|
for (const child of node.namedChildren)
|
|
@@ -415,6 +665,91 @@ function collectImportedSymbols(root, lang) {
|
|
|
415
665
|
visit(root);
|
|
416
666
|
return out;
|
|
417
667
|
}
|
|
668
|
+
function collectPhpUseDeclaration(decl, out) {
|
|
669
|
+
const prefix = decl.namedChildren.find((c) => c.type === "namespace_name")?.text.replace(/\\$/, "") ?? "";
|
|
670
|
+
const clauses = [];
|
|
671
|
+
for (const child of decl.namedChildren) {
|
|
672
|
+
if (child.type === "namespace_use_clause")
|
|
673
|
+
clauses.push(child);
|
|
674
|
+
if (child.type === "namespace_use_group") {
|
|
675
|
+
for (const c of child.namedChildren) {
|
|
676
|
+
if (c.type === "namespace_use_clause")
|
|
677
|
+
clauses.push(c);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
for (const clause of clauses) {
|
|
682
|
+
const binding = phpUseClauseBinding(clause, prefix);
|
|
683
|
+
if (binding)
|
|
684
|
+
out.set(binding.local, { name: binding.name, specifier: binding.specifier });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function phpUseClauseBinding(clause, prefix) {
|
|
688
|
+
const names = clause.namedChildren.filter((c) => c.type === "name");
|
|
689
|
+
const qualified = clause.namedChildren.find((c) => c.type === "qualified_name");
|
|
690
|
+
let fqn;
|
|
691
|
+
let importedName;
|
|
692
|
+
if (qualified) {
|
|
693
|
+
fqn = qualified.text.replace(/^\\/, "");
|
|
694
|
+
importedName = fqn.replace(/^.*\\/, "");
|
|
695
|
+
}
|
|
696
|
+
else if (names[0]) {
|
|
697
|
+
importedName = names[0].text;
|
|
698
|
+
fqn = prefix ? `${prefix}\\${importedName}` : importedName;
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
const alias = qualified && names.length >= 1
|
|
704
|
+
? names[names.length - 1].text
|
|
705
|
+
: names.length >= 2
|
|
706
|
+
? names[1].text
|
|
707
|
+
: undefined;
|
|
708
|
+
const local = alias ?? importedName;
|
|
709
|
+
return { local, name: importedName, specifier: fqn };
|
|
710
|
+
}
|
|
711
|
+
/** PHP 8 attributes on a definition → `references` edges to the attribute class. */
|
|
712
|
+
function phpAttributeReferenceEdges(node, sourceId, ctx) {
|
|
713
|
+
const edges = [];
|
|
714
|
+
for (const child of node.namedChildren) {
|
|
715
|
+
if (child.type !== "attribute_list")
|
|
716
|
+
continue;
|
|
717
|
+
for (const group of child.namedChildren) {
|
|
718
|
+
if (group.type !== "attribute_group")
|
|
719
|
+
continue;
|
|
720
|
+
for (const attr of group.namedChildren) {
|
|
721
|
+
if (attr.type !== "attribute")
|
|
722
|
+
continue;
|
|
723
|
+
const ref = phpAttributeClassRef(attr, ctx);
|
|
724
|
+
if (ref) {
|
|
725
|
+
edges.push({
|
|
726
|
+
source: sourceId,
|
|
727
|
+
relation: "references",
|
|
728
|
+
name: ref.name,
|
|
729
|
+
...(ref.specifier ? { specifier: ref.specifier } : {}),
|
|
730
|
+
file: ctx.rel,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return edges;
|
|
737
|
+
}
|
|
738
|
+
function phpAttributeClassRef(attr, ctx) {
|
|
739
|
+
const nameNode = attr.childForFieldName("name") ??
|
|
740
|
+
attr.namedChildren.find((c) => c.type === "name" || c.type === "qualified_name");
|
|
741
|
+
if (!nameNode)
|
|
742
|
+
return null;
|
|
743
|
+
if (nameNode.type === "qualified_name") {
|
|
744
|
+
const fqn = nameNode.text.replace(/^\\/, "");
|
|
745
|
+
return { name: fqn.replace(/^.*\\/, ""), specifier: fqn };
|
|
746
|
+
}
|
|
747
|
+
const bare = nameNode.text;
|
|
748
|
+
const imported = ctx.importedSymbols.get(bare);
|
|
749
|
+
if (imported)
|
|
750
|
+
return { name: imported.name, specifier: imported.specifier };
|
|
751
|
+
return { name: bare };
|
|
752
|
+
}
|
|
418
753
|
function collectTsImportBindings(node, specifier, out) {
|
|
419
754
|
if (node.type === "import_specifier") {
|
|
420
755
|
const name = node.childForFieldName("name")?.text;
|
|
@@ -505,8 +840,12 @@ function isDeclarationName(node) {
|
|
|
505
840
|
function describe(node, ctx) {
|
|
506
841
|
if (ctx.lang === "go")
|
|
507
842
|
return describeGo(node, ctx);
|
|
843
|
+
if (ctx.lang === "r")
|
|
844
|
+
return describeR(node, ctx);
|
|
508
845
|
if (ctx.lang === "java")
|
|
509
846
|
return describeJava(node, ctx);
|
|
847
|
+
if (ctx.lang === "kotlin")
|
|
848
|
+
return describeKotlin(node, ctx);
|
|
510
849
|
// PHP closures: `$h = function () {…}` / `fn() => …`, and bare callbacks
|
|
511
850
|
// (`$routes->get('/x', function () {…})`). Captured as function nodes so a
|
|
512
851
|
// closure-only file (a routing table, a DI container) keeps its structure
|
|
@@ -520,6 +859,21 @@ function describe(node, ctx) {
|
|
|
520
859
|
hashNode: node,
|
|
521
860
|
};
|
|
522
861
|
}
|
|
862
|
+
// PHP anonymous classes (`new class implements I {…}`): minted as a class
|
|
863
|
+
// node named `{anonymous}` (mirroring `{closure}`, deduplicated per file by
|
|
864
|
+
// mintId). Without this the type vanished — no node, no heritage edge — and
|
|
865
|
+
// its methods mis-attributed to the enclosing function (issue #144). The
|
|
866
|
+
// class kind makes the walk emit heritageEdges (base_clause /
|
|
867
|
+
// class_interface_clause are direct children) and own the nested methods.
|
|
868
|
+
if (ctx.lang === "php" && node.type === "anonymous_class") {
|
|
869
|
+
const body = node.childForFieldName("body");
|
|
870
|
+
return {
|
|
871
|
+
name: "{anonymous}",
|
|
872
|
+
kind: "class",
|
|
873
|
+
headerEnd: body ? body.startIndex : node.endIndex,
|
|
874
|
+
hashNode: node,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
523
877
|
const mapped = ctx.kinds[node.type];
|
|
524
878
|
if (mapped) {
|
|
525
879
|
const name = node.childForFieldName("name")?.text;
|
|
@@ -529,6 +883,12 @@ function describe(node, ctx) {
|
|
|
529
883
|
if (ctx.lang === "python" && mapped === "function" && ctx.enclosingKind === "class") {
|
|
530
884
|
kind = "method";
|
|
531
885
|
}
|
|
886
|
+
// tree-sitter-php 0.23.x recovers a collapsed enum method as function_definition
|
|
887
|
+
// at program scope; walkNamedChildren reparents it under the enum, and this
|
|
888
|
+
// promotion is what keeps the kind `method` rather than a leaked `function`.
|
|
889
|
+
if (ctx.lang === "php" && mapped === "function" && ctx.enclosingKind === "enum") {
|
|
890
|
+
kind = "method";
|
|
891
|
+
}
|
|
532
892
|
const body = node.childForFieldName("body");
|
|
533
893
|
return { name, kind, headerEnd: body ? body.startIndex : node.endIndex, hashNode: node };
|
|
534
894
|
}
|
|
@@ -591,6 +951,432 @@ function describeGo(node, _ctx) {
|
|
|
591
951
|
}
|
|
592
952
|
return null;
|
|
593
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* R definition shapes. `function_definition` carries no name field at all, so
|
|
956
|
+
* unlike every other supported language the name always comes from an
|
|
957
|
+
* enclosing assignment, detected here. The plain-function assignment check
|
|
958
|
+
* (op-filtering `binary_operator`, the right-assign body-swap) is duplicated
|
|
959
|
+
* in bindings.ts's own `rDefName` rather than imported — same reasoning as
|
|
960
|
+
* this file's Go receiver helpers: bindings.ts can't take a value import back
|
|
961
|
+
* on extract.ts. bindings.ts doesn't need the S3/S4/R6 half of this at all
|
|
962
|
+
* (no handleR binding collector exists — see bindings.ts's own doc comment).
|
|
963
|
+
*
|
|
964
|
+
* Phase 1 (flat extraction — every named function is a plain `function` node)
|
|
965
|
+
* plus Phase 2 (S3/S4/R6 class awareness, R's class systems being library
|
|
966
|
+
* *convention* rather than grammar syntax, unlike every other language graft
|
|
967
|
+
* supports):
|
|
968
|
+
* - left-assign (`<-`/`<<-`/`=`) / right-assign (`->`/`->>`) function
|
|
969
|
+
* assignment — Phase 1's shape, see the two `binary_operator`/
|
|
970
|
+
* `function_definition` branches below. Right-assign's AST shape does NOT
|
|
971
|
+
* mirror left-assign's the way it looks like it should (confirmed
|
|
972
|
+
* empirically, not assumed — R's `->` has low enough precedence that it's
|
|
973
|
+
* absorbed into the function's own `body` field as a `binary_operator`
|
|
974
|
+
* instead of the function sitting inside an outer wrapper); only an
|
|
975
|
+
* explicitly parenthesized `(function() {}) -> foo` produces the
|
|
976
|
+
* "expected" outer-wrapping shape, which isn't specially handled (falls
|
|
977
|
+
* through as an anonymous function).
|
|
978
|
+
* - `name.Class <- function() {}` — an S3 method, IF `name` is a known
|
|
979
|
+
* generic (registered locally via `UseMethod()` in this file, or one of a
|
|
980
|
+
* curated set of common base-R generics — see `rS3Split`'s doc comment
|
|
981
|
+
* for the false-positive risk this guards against).
|
|
982
|
+
* - `Foo <- R6::R6Class("Foo", public = list(...), private = list(...))` —
|
|
983
|
+
* an R6 class; its `public =`/`private =`/`active =` list entries become
|
|
984
|
+
* methods, handled by walk()'s own `argument`-node interception (this
|
|
985
|
+
* function only recognizes the class itself; the "a call defines a
|
|
986
|
+
* symbol" list-walking lives in walk() since it needs to mint several
|
|
987
|
+
* nodes, not describe a single one).
|
|
988
|
+
* - `Foo <- list(public = list(...), private = list(...))` (Phase 5) — a
|
|
989
|
+
* plain-list "mixin"/"extension" bundle, NOT wrapped in `R6::R6Class(...)`
|
|
990
|
+
* at all: a real, deliberate convention found dogfooding against a real
|
|
991
|
+
* R6-heavy corpus (25 files, 11 of them entirely invisible to the graph
|
|
992
|
+
* without this) for sharing method bundles across classes via splicing
|
|
993
|
+
* (`public = c(Foo$public, list(...))`) rather than `inherit =`. Only
|
|
994
|
+
* recognized when the list actually has a `public =`/`private =` entry
|
|
995
|
+
* (see `rIsMixinContainer`) — an ordinary data/config list never matches.
|
|
996
|
+
* Reuses kind "class" (nothing better-fitting exists, and everything
|
|
997
|
+
* downstream — the method-list walking, visibility — only cares that
|
|
998
|
+
* ctx.enclosingKind is "class", not how the container was spelled); no
|
|
999
|
+
* heritage edge, since splicing isn't `inherit =`-based inheritance.
|
|
1000
|
+
* - `setClass("Foo", ...)` / `setMethod("generic", "Foo", function() {})`
|
|
1001
|
+
* — S4 class/method calls, recognized as bare top-level `call` nodes
|
|
1002
|
+
* (setClass/setMethod have side effects registering with the S4 system;
|
|
1003
|
+
* they're essentially never assigned to a variable). `setGeneric()` is
|
|
1004
|
+
* NOT specially extracted — it doesn't naturally map to a class or method
|
|
1005
|
+
* kind, and the plan flags it as a case not worth the design risk.
|
|
1006
|
+
*/
|
|
1007
|
+
function describeR(node, ctx) {
|
|
1008
|
+
if (node.type === "binary_operator") {
|
|
1009
|
+
const op = node.childForFieldName("operator")?.text;
|
|
1010
|
+
if (!op || !R_ASSIGN_OPS.has(op))
|
|
1011
|
+
return null;
|
|
1012
|
+
const lhs = node.childForFieldName("lhs");
|
|
1013
|
+
const rhs = node.childForFieldName("rhs");
|
|
1014
|
+
if (lhs?.type !== "identifier")
|
|
1015
|
+
return null;
|
|
1016
|
+
if (rhs?.type === "function_definition") {
|
|
1017
|
+
return rFunctionDescriptor(lhs.text, rhs, rhs.childForFieldName("body"), ctx);
|
|
1018
|
+
}
|
|
1019
|
+
if (rhs?.type === "call" && rCalleeName(rhs) === "R6Class") {
|
|
1020
|
+
// The class node itself; its public=/private=/active= method lists are
|
|
1021
|
+
// handled by walk()'s own `argument`-node interception, not here.
|
|
1022
|
+
return { name: lhs.text, kind: "class", headerEnd: rhs.endIndex, hashNode: rhs };
|
|
1023
|
+
}
|
|
1024
|
+
if (rhs?.type === "call" && rCalleeName(rhs) === "list" && rIsMixinContainer(rhs)) {
|
|
1025
|
+
// Phase 5: a plain-list mixin/extension bundle — same treatment as R6Class.
|
|
1026
|
+
return { name: lhs.text, kind: "class", headerEnd: rhs.endIndex, hashNode: rhs };
|
|
1027
|
+
}
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
if (node.type === "function_definition") {
|
|
1031
|
+
// Right-assign (`function() {} -> foo`): see this function's own doc
|
|
1032
|
+
// comment for why this doesn't mirror the binary_operator branch above.
|
|
1033
|
+
const body = node.childForFieldName("body");
|
|
1034
|
+
if (body?.type !== "binary_operator")
|
|
1035
|
+
return null;
|
|
1036
|
+
const op = body.childForFieldName("operator")?.text;
|
|
1037
|
+
if (!op || !R_RIGHT_ASSIGN_OPS.has(op))
|
|
1038
|
+
return null;
|
|
1039
|
+
const rhs = body.childForFieldName("rhs");
|
|
1040
|
+
if (rhs?.type !== "identifier")
|
|
1041
|
+
return null;
|
|
1042
|
+
return rFunctionDescriptor(rhs.text, node, body.childForFieldName("lhs"), ctx);
|
|
1043
|
+
}
|
|
1044
|
+
if (node.type === "call") {
|
|
1045
|
+
return describeRTopLevelCall(node);
|
|
1046
|
+
}
|
|
1047
|
+
// R6 (Phase 2): reached via walk()'s own `argument`-node interception for a
|
|
1048
|
+
// `public =`/`private =`/`active =` list entry — see the special case there
|
|
1049
|
+
// for why this can't just be a flat kind-table/node-type check like every
|
|
1050
|
+
// other definition shape.
|
|
1051
|
+
if (node.type === "argument" && ctx.rR6Access !== null) {
|
|
1052
|
+
const argName = node.childForFieldName("name");
|
|
1053
|
+
const value = node.childForFieldName("value");
|
|
1054
|
+
if (argName?.type !== "identifier" || value?.type !== "function_definition")
|
|
1055
|
+
return null;
|
|
1056
|
+
const body = value.childForFieldName("body");
|
|
1057
|
+
return {
|
|
1058
|
+
name: argName.text,
|
|
1059
|
+
kind: "method",
|
|
1060
|
+
headerEnd: body ? body.startIndex : value.endIndex,
|
|
1061
|
+
hashNode: value,
|
|
1062
|
+
// owner deliberately unset — R6 methods DO lexically nest inside the
|
|
1063
|
+
// class-defining call, so ctx.enclosingClass already has it.
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
1068
|
+
const R_ASSIGN_OPS = new Set(["<-", "<<-", "="]);
|
|
1069
|
+
const R_RIGHT_ASSIGN_OPS = new Set(["->", "->>"]);
|
|
1070
|
+
/** A plain function assignment (left- or right-assign), OR — if `name` matches
|
|
1071
|
+
* a known S3 generic's `generic.Class` pattern — an S3 method instead. `body`
|
|
1072
|
+
* is the function's REAL content node (already resolved by the caller for
|
|
1073
|
+
* either assignment direction), used only for `headerEnd`; `hashNode` is
|
|
1074
|
+
* always the `function_definition` itself. */
|
|
1075
|
+
function rFunctionDescriptor(name, hashNode, body, ctx) {
|
|
1076
|
+
const headerEnd = body ? body.startIndex : hashNode.endIndex;
|
|
1077
|
+
const s3 = rS3Split(name, ctx.rGenerics);
|
|
1078
|
+
if (s3) {
|
|
1079
|
+
return {
|
|
1080
|
+
name: s3.generic,
|
|
1081
|
+
idName: `${s3.className}.${s3.generic}`,
|
|
1082
|
+
kind: "method",
|
|
1083
|
+
headerEnd,
|
|
1084
|
+
hashNode,
|
|
1085
|
+
owner: s3.className,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
return { name, kind: "function", headerEnd, hashNode };
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* S3 dispatch detection: does `name` split as `generic.Class` for some KNOWN
|
|
1092
|
+
* generic? Tries the longest possible generic prefix first (so a dotted
|
|
1093
|
+
* generic itself, like `as.character`, is found before a shorter false match)
|
|
1094
|
+
* and only ever matches a generic that's either registered locally via
|
|
1095
|
+
* `UseMethod()` in this file (see `collectRGenerics`) or in the small curated
|
|
1096
|
+
* `R_BASE_GENERICS` set below.
|
|
1097
|
+
*
|
|
1098
|
+
* This is the genuinely ambiguous part of R support the plan calls out:
|
|
1099
|
+
* `read.csv`, `data.frame`, and `as.character` used as an ordinary helper
|
|
1100
|
+
* name are NOT S3 dispatch, and nothing in the grammar distinguishes them
|
|
1101
|
+
* from `print.MyClass`. Erring toward the curated set staying small — a
|
|
1102
|
+
* missed S3 method (false negative, falls back to an ordinary `function`
|
|
1103
|
+
* node) is a much smaller problem than a false positive misfiling an
|
|
1104
|
+
* unrelated dotted-name function as some other class's method.
|
|
1105
|
+
*/
|
|
1106
|
+
function rS3Split(name, generics) {
|
|
1107
|
+
const parts = name.split(".");
|
|
1108
|
+
if (parts.length < 2)
|
|
1109
|
+
return null;
|
|
1110
|
+
for (let i = parts.length - 1; i >= 1; i--) {
|
|
1111
|
+
const generic = parts.slice(0, i).join(".");
|
|
1112
|
+
if (generics.has(generic) || R_BASE_GENERICS.has(generic)) {
|
|
1113
|
+
return { generic, className: parts.slice(i).join(".") };
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
/** Common base-R S3 generics worth assuming even without local evidence —
|
|
1119
|
+
* print.Foo/format.Foo etc. are the single most common real-world S3
|
|
1120
|
+
* pattern, and a local `UseMethod()` call will never exist for them (they
|
|
1121
|
+
* ship in base/methods/stats, not the user's own repo). Deliberately small
|
|
1122
|
+
* and unsurprising rather than exhaustive — see `rS3Split`'s doc comment. */
|
|
1123
|
+
const R_BASE_GENERICS = new Set([
|
|
1124
|
+
"print",
|
|
1125
|
+
"format",
|
|
1126
|
+
"summary",
|
|
1127
|
+
"plot",
|
|
1128
|
+
"str",
|
|
1129
|
+
"toString",
|
|
1130
|
+
"as.character",
|
|
1131
|
+
"as.list",
|
|
1132
|
+
"as.data.frame",
|
|
1133
|
+
"as.vector",
|
|
1134
|
+
"as.numeric",
|
|
1135
|
+
"as.matrix",
|
|
1136
|
+
"length",
|
|
1137
|
+
"dim",
|
|
1138
|
+
"names",
|
|
1139
|
+
"rev",
|
|
1140
|
+
"sort",
|
|
1141
|
+
"unique",
|
|
1142
|
+
"predict",
|
|
1143
|
+
"coef",
|
|
1144
|
+
"residuals",
|
|
1145
|
+
"fitted",
|
|
1146
|
+
"update",
|
|
1147
|
+
"merge",
|
|
1148
|
+
"all.equal",
|
|
1149
|
+
"anova",
|
|
1150
|
+
"confint",
|
|
1151
|
+
"vcov",
|
|
1152
|
+
"logLik",
|
|
1153
|
+
]);
|
|
1154
|
+
/** Every S3 generic THIS file registers via a local `UseMethod()` call, so
|
|
1155
|
+
* `rS3Split` can recognize `generic.Class` methods for a repo's own generics,
|
|
1156
|
+
* not just the base-R ones. Runs once per file, ahead of the main walk (same
|
|
1157
|
+
* pre-pass shape as `collectImportedSymbols`). Cross-file generics — a
|
|
1158
|
+
* generic defined in one file, dispatched on in another — aren't found this
|
|
1159
|
+
* way; that would need a whole-repo pass extractFile has no visibility into,
|
|
1160
|
+
* the same limitation Go/C++'s per-file bindings already accept. */
|
|
1161
|
+
function collectRGenerics(root) {
|
|
1162
|
+
const generics = new Set();
|
|
1163
|
+
const visit = (node) => {
|
|
1164
|
+
let fnDef = null;
|
|
1165
|
+
let ownName = null;
|
|
1166
|
+
if (node.type === "binary_operator") {
|
|
1167
|
+
const op = node.childForFieldName("operator")?.text;
|
|
1168
|
+
const lhs = node.childForFieldName("lhs");
|
|
1169
|
+
const rhs = node.childForFieldName("rhs");
|
|
1170
|
+
if (op && R_ASSIGN_OPS.has(op) && lhs?.type === "identifier" && rhs?.type === "function_definition") {
|
|
1171
|
+
fnDef = rhs;
|
|
1172
|
+
ownName = lhs.text;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
else if (node.type === "function_definition") {
|
|
1176
|
+
const body = node.childForFieldName("body");
|
|
1177
|
+
if (body?.type === "binary_operator") {
|
|
1178
|
+
const op = body.childForFieldName("operator")?.text;
|
|
1179
|
+
const rhs = body.childForFieldName("rhs");
|
|
1180
|
+
if (op && R_RIGHT_ASSIGN_OPS.has(op) && rhs?.type === "identifier") {
|
|
1181
|
+
fnDef = node;
|
|
1182
|
+
ownName = rhs.text;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (fnDef && ownName) {
|
|
1187
|
+
const arg = findUseMethodArg(fnDef.childForFieldName("body"));
|
|
1188
|
+
if (arg !== undefined)
|
|
1189
|
+
generics.add(arg || ownName); // "" means UseMethod() with no args
|
|
1190
|
+
}
|
|
1191
|
+
for (const child of node.namedChildren)
|
|
1192
|
+
visit(child);
|
|
1193
|
+
};
|
|
1194
|
+
visit(root);
|
|
1195
|
+
return generics;
|
|
1196
|
+
}
|
|
1197
|
+
/** Searches a function body for a `UseMethod(...)` call and returns its
|
|
1198
|
+
* string-literal generic-name argument, `""` if called with no arguments
|
|
1199
|
+
* (defaults to the enclosing function's own name), or `undefined` if no
|
|
1200
|
+
* `UseMethod` call is found at all. */
|
|
1201
|
+
function findUseMethodArg(node) {
|
|
1202
|
+
if (!node)
|
|
1203
|
+
return undefined;
|
|
1204
|
+
if (node.type === "call" && rCalleeName(node) === "UseMethod") {
|
|
1205
|
+
const first = rCallArgs(node)[0]?.childForFieldName("value");
|
|
1206
|
+
return first?.type === "string" ? (rStringContent(first) ?? "") : "";
|
|
1207
|
+
}
|
|
1208
|
+
for (const child of node.namedChildren) {
|
|
1209
|
+
const found = findUseMethodArg(child);
|
|
1210
|
+
if (found !== undefined)
|
|
1211
|
+
return found;
|
|
1212
|
+
}
|
|
1213
|
+
return undefined;
|
|
1214
|
+
}
|
|
1215
|
+
/** S4: `setClass("Foo", ...)` → a class; `setMethod("generic", "Foo",
|
|
1216
|
+
* function() {})` → a method owned by "Foo". Both are ordinary top-level
|
|
1217
|
+
* `call` nodes — S4's registration functions have side effects and are
|
|
1218
|
+
* essentially never assigned to a variable, unlike R6Class. */
|
|
1219
|
+
function describeRTopLevelCall(node) {
|
|
1220
|
+
const callee = rCalleeName(node);
|
|
1221
|
+
if (callee === "setClass") {
|
|
1222
|
+
const first = rCallArgs(node)[0]?.childForFieldName("value");
|
|
1223
|
+
const name = first?.type === "string" ? rStringContent(first) : null;
|
|
1224
|
+
if (!name)
|
|
1225
|
+
return null;
|
|
1226
|
+
return { name, kind: "class", headerEnd: node.endIndex, hashNode: node };
|
|
1227
|
+
}
|
|
1228
|
+
if (callee === "setMethod") {
|
|
1229
|
+
const args = rCallArgs(node);
|
|
1230
|
+
const generic = args[0] ? rStringContent(args[0].childForFieldName("value") ?? null) : null;
|
|
1231
|
+
const className = args[1] ? rStringContent(args[1].childForFieldName("value") ?? null) : null;
|
|
1232
|
+
const defArg = args.find((a) => a.childForFieldName("name")?.text === "definition") ?? args[2];
|
|
1233
|
+
const fnDef = defArg?.childForFieldName("value");
|
|
1234
|
+
if (!generic || !className || fnDef?.type !== "function_definition")
|
|
1235
|
+
return null;
|
|
1236
|
+
const body = fnDef.childForFieldName("body");
|
|
1237
|
+
return {
|
|
1238
|
+
name: generic,
|
|
1239
|
+
idName: `${className}.${generic}`,
|
|
1240
|
+
kind: "method",
|
|
1241
|
+
headerEnd: body ? body.startIndex : fnDef.endIndex,
|
|
1242
|
+
hashNode: fnDef,
|
|
1243
|
+
owner: className,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
/** A call's callee name, whether bare (`R6Class(...)`) or namespace-qualified
|
|
1249
|
+
* (`R6::R6Class(...)`). Null if the callee isn't a simple name (e.g. itself a
|
|
1250
|
+
* call, or a `$`-based access). */
|
|
1251
|
+
function rCalleeName(node) {
|
|
1252
|
+
const fn = node.childForFieldName("function");
|
|
1253
|
+
if (fn?.type === "identifier")
|
|
1254
|
+
return fn.text;
|
|
1255
|
+
if (fn?.type === "namespace_operator") {
|
|
1256
|
+
const rhs = fn.childForFieldName("rhs");
|
|
1257
|
+
return rhs?.type === "identifier" ? rhs.text : null;
|
|
1258
|
+
}
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
/** A call's positional/named `argument` children (skipping the `,`/`(`/`)`
|
|
1262
|
+
* punctuation tokens that share the `arguments` node's child list). */
|
|
1263
|
+
function rCallArgs(node) {
|
|
1264
|
+
return node.childForFieldName("arguments")?.namedChildren.filter((c) => c.type === "argument") ?? [];
|
|
1265
|
+
}
|
|
1266
|
+
/** An R `string` node's unquoted text, or null if `node` isn't a string. */
|
|
1267
|
+
function rStringContent(node) {
|
|
1268
|
+
if (node?.type !== "string")
|
|
1269
|
+
return null;
|
|
1270
|
+
const content = node.namedChildren.find((c) => c.type === "string_content");
|
|
1271
|
+
return content?.text ?? null;
|
|
1272
|
+
}
|
|
1273
|
+
/** The value of a call's named argument (`setClass("Foo", contains = "Base")`
|
|
1274
|
+
* → the `contains` argument), or null if absent. */
|
|
1275
|
+
function rNamedArg(node, argName) {
|
|
1276
|
+
const arg = rCallArgs(node).find((a) => a.childForFieldName("name")?.text === argName);
|
|
1277
|
+
return arg?.childForFieldName("value") ?? null;
|
|
1278
|
+
}
|
|
1279
|
+
/** An R6 class-defining node's `inherit =` parent class name (a bare
|
|
1280
|
+
* identifier — the parent's own generator variable, not a string), for
|
|
1281
|
+
* `super$` call resolution (Phase 3). `node` is whatever describeR matched: a
|
|
1282
|
+
* binary_operator for R6 (`Foo <- R6::R6Class(...)`) or, for any other kind of
|
|
1283
|
+
* class (S4's setClass, which has no `super`), the call itself — always null
|
|
1284
|
+
* there since `rCalleeName(call) !== "R6Class"`. */
|
|
1285
|
+
function rR6ParentClass(node) {
|
|
1286
|
+
const call = node.type === "binary_operator" ? node.childForFieldName("rhs") : node;
|
|
1287
|
+
if (call?.type !== "call" || rCalleeName(call) !== "R6Class")
|
|
1288
|
+
return null;
|
|
1289
|
+
const value = rNamedArg(call, "inherit");
|
|
1290
|
+
return value?.type === "identifier" ? value.text : null;
|
|
1291
|
+
}
|
|
1292
|
+
/** Does this `list(...)` call look like a Phase-5 mixin/extension bundle —
|
|
1293
|
+
* i.e. does it have a `public =` or `private =` entry whose own value is
|
|
1294
|
+
* itself a `list(...)` call? This is the one check standing between "class-
|
|
1295
|
+
* like container" and an ordinary data/config list (`list(a = 1, b = 2)`,
|
|
1296
|
+
* or even one that happens to have a field named "public" holding something
|
|
1297
|
+
* else) — real code never coincidentally shapes plain data this way, so it's
|
|
1298
|
+
* a safe, precise signal without needing a naming-convention heuristic. */
|
|
1299
|
+
function rIsMixinContainer(node) {
|
|
1300
|
+
return rCallArgs(node).some((a) => {
|
|
1301
|
+
const name = a.childForFieldName("name")?.text;
|
|
1302
|
+
if (name !== "public" && name !== "private")
|
|
1303
|
+
return false;
|
|
1304
|
+
const value = a.childForFieldName("value");
|
|
1305
|
+
return value?.type === "call" && rCalleeName(value) === "list";
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
/** A base-class name list from either a bare string (`contains = "Base"`) or
|
|
1309
|
+
* a `c(...)` call of strings (`contains = c("Base1", "Base2")`) — S4's
|
|
1310
|
+
* multiple-inheritance form. */
|
|
1311
|
+
function rStringOrCVector(value) {
|
|
1312
|
+
if (!value)
|
|
1313
|
+
return [];
|
|
1314
|
+
const single = rStringContent(value);
|
|
1315
|
+
if (single)
|
|
1316
|
+
return [single];
|
|
1317
|
+
if (value.type === "call" && rCalleeName(value) === "c") {
|
|
1318
|
+
return rCallArgs(value)
|
|
1319
|
+
.map((a) => rStringContent(a.childForFieldName("value")))
|
|
1320
|
+
.filter((s) => !!s);
|
|
1321
|
+
}
|
|
1322
|
+
return [];
|
|
1323
|
+
}
|
|
1324
|
+
/** R6 visibility follows the `public =`/`private =`/`active =` section a
|
|
1325
|
+
* method was declared in (see walk()'s `argument`-node interception) —
|
|
1326
|
+
* `ctx.rR6Access` is only set for that direct span, so a plain function or an
|
|
1327
|
+
* S3/S4 method (neither of which has a real visibility concept) checks for a
|
|
1328
|
+
* roxygen `@export` tag next (Phase 3), falling back to the leading-dot naming
|
|
1329
|
+
* convention only when there's no roxygen evidence to go on at all. `node` is
|
|
1330
|
+
* the exact node describeR matched — see `rRoxygenExported`'s doc comment for
|
|
1331
|
+
* why that's always the right one to check for a preceding comment block. */
|
|
1332
|
+
function rExported(name, ctx, node) {
|
|
1333
|
+
if (ctx.rR6Access !== null)
|
|
1334
|
+
return ctx.rR6Access !== "private";
|
|
1335
|
+
const roxygen = rRoxygenExported(node);
|
|
1336
|
+
if (roxygen !== null)
|
|
1337
|
+
return roxygen;
|
|
1338
|
+
return !name.startsWith(".");
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Roxygen `@export` detection (Phase 3): does `node` — a top-level definition
|
|
1342
|
+
* statement (a `binary_operator` assignment, a right-assigned
|
|
1343
|
+
* `function_definition`, or an S4 `setClass`/`setMethod` call) — have a
|
|
1344
|
+
* roxygen doc block immediately preceding it, and if so, is it tagged
|
|
1345
|
+
* `@export`?
|
|
1346
|
+
*
|
|
1347
|
+
* `comment` is a grammar EXTRA in this grammar (floats loosely between
|
|
1348
|
+
* sibling nodes rather than attaching to "the next statement" via a field),
|
|
1349
|
+
* so this walks backward through `previousNamedSibling` collecting a
|
|
1350
|
+
* contiguous run of `comment` nodes — the run ends at the first non-comment
|
|
1351
|
+
* sibling, or at the first comment that isn't itself a roxygen (`#'`) line,
|
|
1352
|
+
* either of which is roxygen's own "this block documents the next statement"
|
|
1353
|
+
* boundary.
|
|
1354
|
+
*
|
|
1355
|
+
* Returns:
|
|
1356
|
+
* - `true` — a roxygen block was found and it contains `@export`.
|
|
1357
|
+
* - `false` — a roxygen block was found but it does NOT contain `@export`.
|
|
1358
|
+
* This is deliberately a confident "not exported," not "unknown": roxygen
|
|
1359
|
+
* generates a package's NAMESPACE from exactly its `@export`-tagged items,
|
|
1360
|
+
* so a documented-but-untagged function is an explicit "internal, for
|
|
1361
|
+
* maintainers only" signal, not an absence of evidence.
|
|
1362
|
+
* - `null` — no roxygen block at all, so the caller should fall back to the
|
|
1363
|
+
* leading-dot naming convention instead of guessing.
|
|
1364
|
+
*/
|
|
1365
|
+
function rRoxygenExported(node) {
|
|
1366
|
+
let sib = node.previousNamedSibling;
|
|
1367
|
+
let sawRoxygen = false;
|
|
1368
|
+
let exported = false;
|
|
1369
|
+
while (sib?.type === "comment") {
|
|
1370
|
+
const text = sib.text.trim();
|
|
1371
|
+
if (!text.startsWith("#'"))
|
|
1372
|
+
break; // an ordinary # comment ends the roxygen block
|
|
1373
|
+
sawRoxygen = true;
|
|
1374
|
+
if (/^#'\s*@export\b/.test(text))
|
|
1375
|
+
exported = true;
|
|
1376
|
+
sib = sib.previousNamedSibling;
|
|
1377
|
+
}
|
|
1378
|
+
return sawRoxygen ? exported : null;
|
|
1379
|
+
}
|
|
594
1380
|
/** Java definition shapes. Uniform in a way Go's are not: every declaration carries
|
|
595
1381
|
* a `name` field and (for types and most members) a `body`, so one mapped lookup
|
|
596
1382
|
* covers classes, interfaces, enums, records, methods, and constructors. Methods are
|
|
@@ -622,6 +1408,87 @@ function describeJava(node, ctx) {
|
|
|
622
1408
|
}
|
|
623
1409
|
return desc;
|
|
624
1410
|
}
|
|
1411
|
+
/** Kotlin definition shapes. Unlike Java's, tree-sitter-kotlin exposes no `name`
|
|
1412
|
+
* or `body` fields: a definition's name is an unnamed `simple_identifier` (functions)
|
|
1413
|
+
* or `type_identifier` (types) child, and its body is a `class_body` / `function_body`
|
|
1414
|
+
* / `statements` child. `class_declaration` also folds classes, interfaces, and enum
|
|
1415
|
+
* classes into one node type — the kind is read off the declaration's own keywords. */
|
|
1416
|
+
function describeKotlin(node, ctx) {
|
|
1417
|
+
// The first direct `type_identifier` is the declared name (type parameters, primary
|
|
1418
|
+
// constructor parameters and delegation specifiers are all nested beneath it).
|
|
1419
|
+
const typeName = () => node.namedChildren.find((c) => c.type === "type_identifier")?.text ?? null;
|
|
1420
|
+
// The first direct `simple_identifier` is the function name (receiver type, params
|
|
1421
|
+
// and type parameters are all nested beneath other child nodes).
|
|
1422
|
+
const funcName = () => node.namedChildren.find((c) => c.type === "simple_identifier")?.text ?? null;
|
|
1423
|
+
// `class X : A, B()` heritage lives in `delegation_specifier` children; a nested
|
|
1424
|
+
// type parameter's identifier is one of the same node type, so only direct children
|
|
1425
|
+
// count as the declared name.
|
|
1426
|
+
const headEnd = (type) => {
|
|
1427
|
+
const body = node.namedChildren.find((c) => c.type === type);
|
|
1428
|
+
return body ? body.startIndex : node.endIndex;
|
|
1429
|
+
};
|
|
1430
|
+
if (node.type === "class_declaration") {
|
|
1431
|
+
const name = typeName();
|
|
1432
|
+
if (!name)
|
|
1433
|
+
return null;
|
|
1434
|
+
let kind = "class";
|
|
1435
|
+
if (node.namedChildren.some((c) => c.type === "enum_class_body"))
|
|
1436
|
+
kind = "enum";
|
|
1437
|
+
else if (node.children.some((c) => c.type === "interface"))
|
|
1438
|
+
kind = "interface";
|
|
1439
|
+
else {
|
|
1440
|
+
const mods = node.namedChildren.find((c) => c.type === "modifiers");
|
|
1441
|
+
// `annotation class` → the interface role Java's annotation_type_declaration plays.
|
|
1442
|
+
if (mods?.namedChildren.some((c) => c.type === "class_modifier" && c.text === "annotation"))
|
|
1443
|
+
kind = "interface";
|
|
1444
|
+
}
|
|
1445
|
+
const body = node.namedChildren.find((c) => c.type === "class_body" || c.type === "enum_class_body");
|
|
1446
|
+
return { name, kind, headerEnd: body ? body.startIndex : node.endIndex, hashNode: node };
|
|
1447
|
+
}
|
|
1448
|
+
if (node.type === "object_declaration") {
|
|
1449
|
+
const name = typeName();
|
|
1450
|
+
if (!name)
|
|
1451
|
+
return null;
|
|
1452
|
+
return { name, kind: "class", headerEnd: headEnd("class_body"), hashNode: node };
|
|
1453
|
+
}
|
|
1454
|
+
if (node.type === "function_declaration") {
|
|
1455
|
+
const name = funcName();
|
|
1456
|
+
if (!name)
|
|
1457
|
+
return null;
|
|
1458
|
+
const kind = KOTLIN_TYPE_KINDS.has(ctx.enclosingKind ?? "file") ? "method" : "function";
|
|
1459
|
+
return { name, kind, headerEnd: headEnd("function_body"), hashNode: node };
|
|
1460
|
+
}
|
|
1461
|
+
if (node.type === "secondary_constructor") {
|
|
1462
|
+
// Constructors carry no name of their own — they are the class's own, so scope the
|
|
1463
|
+
// node under the enclosing class the same way Java's constructor_declaration does.
|
|
1464
|
+
if (!ctx.enclosingClass)
|
|
1465
|
+
return null;
|
|
1466
|
+
return {
|
|
1467
|
+
name: ctx.enclosingClass,
|
|
1468
|
+
kind: "method",
|
|
1469
|
+
headerEnd: headEnd("statements"),
|
|
1470
|
+
hashNode: node,
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
if (node.type === "type_alias") {
|
|
1474
|
+
const name = typeName();
|
|
1475
|
+
if (!name)
|
|
1476
|
+
return null;
|
|
1477
|
+
return { name, kind: "type", headerEnd: node.endIndex, hashNode: node };
|
|
1478
|
+
}
|
|
1479
|
+
if (node.type === "property_declaration") {
|
|
1480
|
+
// Top-level `val`/`var` only — a class property is a field, not a definition node
|
|
1481
|
+
// (no depth tier emits fields), so it must not become one.
|
|
1482
|
+
if (ctx.enclosingKind !== null)
|
|
1483
|
+
return null;
|
|
1484
|
+
const decl = node.namedChildren.find((c) => c.type === "variable_declaration");
|
|
1485
|
+
const name = decl?.namedChildren.find((c) => c.type === "simple_identifier")?.text;
|
|
1486
|
+
if (!name)
|
|
1487
|
+
return null;
|
|
1488
|
+
return { name, kind: "variable", headerEnd: node.endIndex, hashNode: node };
|
|
1489
|
+
}
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
625
1492
|
/** Java visibility: `public` (or `protected`) on the declaration's own modifier list.
|
|
626
1493
|
* A package-private or private member is not part of the API surface. Read off the
|
|
627
1494
|
* `modifiers` child's tokens, ignoring annotations, which live in the same node. */
|
|
@@ -631,6 +1498,15 @@ function javaExported(node) {
|
|
|
631
1498
|
return false;
|
|
632
1499
|
return mods.children.some((c) => c.type === "public" || c.type === "protected");
|
|
633
1500
|
}
|
|
1501
|
+
/** Kotlin visibility: exported by default (`public` is implicit); only an explicit
|
|
1502
|
+
* `internal` / `private` / `protected` visibility modifier hides a definition. */
|
|
1503
|
+
function kotlinExported(node) {
|
|
1504
|
+
const mods = node.namedChildren.find((c) => c.type === "modifiers");
|
|
1505
|
+
if (!mods)
|
|
1506
|
+
return true;
|
|
1507
|
+
const vis = mods.namedChildren.find((c) => c.type === "visibility_modifier");
|
|
1508
|
+
return !vis || vis.text === "public";
|
|
1509
|
+
}
|
|
634
1510
|
/** The receiver's base type name for a Go method, unwrapping a pointer receiver
|
|
635
1511
|
* (`func (u *User) …` → `User`). Null if it can't be read. */
|
|
636
1512
|
function goReceiverType(node) {
|
|
@@ -679,6 +1555,7 @@ function heritageEdges(node, classId, ctx) {
|
|
|
679
1555
|
// `superclass` holds `extends X`; `super_interfaces` holds `implements A, B`
|
|
680
1556
|
// (and, on an interface declaration, `extends A, B` — which tree-sitter-java
|
|
681
1557
|
// still spells `extends_interfaces`).
|
|
1558
|
+
const typeParams = javaTypeParameterNames(node);
|
|
682
1559
|
for (const child of node.namedChildren) {
|
|
683
1560
|
const relation = child.type === "superclass"
|
|
684
1561
|
? "extends"
|
|
@@ -687,12 +1564,35 @@ function heritageEdges(node, classId, ctx) {
|
|
|
687
1564
|
: null;
|
|
688
1565
|
if (!relation)
|
|
689
1566
|
continue;
|
|
690
|
-
for (const
|
|
691
|
-
|
|
1567
|
+
for (const entry of javaSupertypeEntries(child)) {
|
|
1568
|
+
const name = javaSupertypeName(entry);
|
|
1569
|
+
// Belt-and-braces. A type VARIABLE is never a supertype, and erasing the
|
|
1570
|
+
// arguments already removes every case measured on gson and spring-petclinic
|
|
1571
|
+
// (identical output with this filter removed) — Java cannot extend or implement
|
|
1572
|
+
// a type variable, so a surviving `T` would have to come from a shape neither
|
|
1573
|
+
// repo contains. Kept because a wrong supertype is not a cosmetic edge: it
|
|
1574
|
+
// feeds `classParents` and from there call resolution.
|
|
1575
|
+
if (!name || typeParams.has(name))
|
|
1576
|
+
continue;
|
|
1577
|
+
edges.push({ source: classId, relation, name, file: ctx.rel });
|
|
692
1578
|
}
|
|
693
1579
|
}
|
|
694
1580
|
return edges;
|
|
695
1581
|
}
|
|
1582
|
+
if (ctx.lang === "kotlin") {
|
|
1583
|
+
// The `:` clause is a list of `delegation_specifier`s — a superclass construction
|
|
1584
|
+
// (`class A : B()`), an interface, or `by` delegation. The first `type_identifier`
|
|
1585
|
+
// under each names the type; everything else (type args, delegation target) is not
|
|
1586
|
+
// the heritage target, so only the head type counts.
|
|
1587
|
+
for (const child of node.namedChildren) {
|
|
1588
|
+
if (child.type !== "delegation_specifier")
|
|
1589
|
+
continue;
|
|
1590
|
+
const t = child.namedChildren.find((c) => c.type === "user_type")?.namedChildren.find((c) => c.type === "type_identifier");
|
|
1591
|
+
if (t)
|
|
1592
|
+
edges.push({ source: classId, relation: "extends", name: t.text, file: ctx.rel });
|
|
1593
|
+
}
|
|
1594
|
+
return edges;
|
|
1595
|
+
}
|
|
696
1596
|
if (ctx.lang === "python") {
|
|
697
1597
|
const supers = node.childForFieldName("superclasses"); // argument_list
|
|
698
1598
|
for (const c of supers?.namedChildren ?? []) {
|
|
@@ -702,6 +1602,31 @@ function heritageEdges(node, classId, ctx) {
|
|
|
702
1602
|
}
|
|
703
1603
|
return edges;
|
|
704
1604
|
}
|
|
1605
|
+
if (ctx.lang === "r") {
|
|
1606
|
+
// `node` is whatever describeR matched: a binary_operator for R6
|
|
1607
|
+
// (`Foo <- R6::R6Class(...)`) or the call itself for S4 (`setClass(...)`
|
|
1608
|
+
// is a bare top-level statement, essentially never assigned).
|
|
1609
|
+
const call = node.type === "binary_operator" ? node.childForFieldName("rhs") : node;
|
|
1610
|
+
if (call?.type !== "call")
|
|
1611
|
+
return edges;
|
|
1612
|
+
const callee = rCalleeName(call);
|
|
1613
|
+
if (callee === "R6Class") {
|
|
1614
|
+
// `inherit = ParentClass` — a bare identifier (the parent's own
|
|
1615
|
+
// generator variable), not a string; R6 supports single inheritance only.
|
|
1616
|
+
const value = rNamedArg(call, "inherit");
|
|
1617
|
+
if (value?.type === "identifier") {
|
|
1618
|
+
edges.push({ source: classId, relation: "extends", name: value.text, file: ctx.rel });
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
else if (callee === "setClass") {
|
|
1622
|
+
// `contains = "Base"` or `contains = c("Base1", "Base2")` — S4 supports
|
|
1623
|
+
// multiple inheritance.
|
|
1624
|
+
for (const name of rStringOrCVector(rNamedArg(call, "contains"))) {
|
|
1625
|
+
edges.push({ source: classId, relation: "extends", name, file: ctx.rel });
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return edges;
|
|
1629
|
+
}
|
|
705
1630
|
if (ctx.lang === "php") {
|
|
706
1631
|
// `class C extends B implements I, J` → base_clause (extends) +
|
|
707
1632
|
// class_interface_clause (implements); names may be namespace-qualified.
|
|
@@ -734,17 +1659,58 @@ function heritageEdges(node, classId, ctx) {
|
|
|
734
1659
|
}
|
|
735
1660
|
return edges;
|
|
736
1661
|
}
|
|
737
|
-
/**
|
|
738
|
-
*
|
|
739
|
-
|
|
740
|
-
|
|
1662
|
+
/**
|
|
1663
|
+
* The supertypes a heritage clause names, one node each — NOT every `type_identifier`
|
|
1664
|
+
* beneath it.
|
|
1665
|
+
*
|
|
1666
|
+
* `superclass` wraps a single type; `super_interfaces`/`extends_interfaces` wrap a
|
|
1667
|
+
* `type_list` of them. Descending blindly instead walked into `type_arguments`, so
|
|
1668
|
+
* `implements Comparable<Item>` reported `Item` as a supertype too.
|
|
1669
|
+
*/
|
|
1670
|
+
function javaSupertypeEntries(clause) {
|
|
1671
|
+
const list = clause.namedChildren.find((c) => c.type === "type_list");
|
|
1672
|
+
return list ? [...list.namedChildren] : [...clause.namedChildren];
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* What a supertype entry is CALLED, or null when this pass cannot say.
|
|
1676
|
+
*
|
|
1677
|
+
* Type arguments are erased, because they are not part of the supertype's identity:
|
|
1678
|
+
*
|
|
1679
|
+
* Base | Base<Item> -> Base
|
|
1680
|
+
*
|
|
1681
|
+
* A qualified name is kept WHOLE rather than reduced to its final segment:
|
|
1682
|
+
*
|
|
1683
|
+
* Outer.Inner | Outer.Inner<K> -> Outer.Inner
|
|
1684
|
+
*
|
|
1685
|
+
* Heritage keeps an unresolved base as the edge target by design ("usually an
|
|
1686
|
+
* external/imported type — keep the name"), so the full string is both truthful and
|
|
1687
|
+
* unable to false-match a node id, where a bare `Inner` could collide with an
|
|
1688
|
+
* unrelated in-repo type. That differs from construction (#103), which drops a
|
|
1689
|
+
* qualified name instead — construction has no keep-the-name contract to fall back on.
|
|
1690
|
+
*/
|
|
1691
|
+
function javaSupertypeName(node) {
|
|
1692
|
+
if (!node)
|
|
1693
|
+
return null;
|
|
1694
|
+
if (node.type === "generic_type")
|
|
1695
|
+
return javaSupertypeName(node.namedChildren[0]);
|
|
1696
|
+
if (node.type === "scoped_type_identifier")
|
|
1697
|
+
return node.text;
|
|
1698
|
+
return node.type === "type_identifier" ? node.text : null;
|
|
1699
|
+
}
|
|
1700
|
+
/** The names a declaration binds as its own type parameters (`class C<T, U>` → T, U),
|
|
1701
|
+
* so they can never be mistaken for supertypes. */
|
|
1702
|
+
function javaTypeParameterNames(decl) {
|
|
1703
|
+
const params = decl.childForFieldName("type_parameters");
|
|
1704
|
+
if (!params)
|
|
1705
|
+
return new Set();
|
|
1706
|
+
const out = new Set();
|
|
741
1707
|
const visit = (n) => {
|
|
742
1708
|
if (n.type === "type_identifier")
|
|
743
|
-
out.
|
|
1709
|
+
out.add(n.text);
|
|
744
1710
|
for (const c of n.namedChildren)
|
|
745
1711
|
visit(c);
|
|
746
1712
|
};
|
|
747
|
-
visit(
|
|
1713
|
+
visit(params);
|
|
748
1714
|
return out;
|
|
749
1715
|
}
|
|
750
1716
|
function calleeName(node, lang) {
|
|
@@ -772,6 +1738,26 @@ function calleeName(node, lang) {
|
|
|
772
1738
|
return { name: nameNode.text, viaMember: true, receiver: "this" };
|
|
773
1739
|
return { name: nameNode.text, viaMember: true, receiver: javaReceiver(obj) };
|
|
774
1740
|
}
|
|
1741
|
+
if (lang === "kotlin") {
|
|
1742
|
+
// `call_expression` = callee expression + `call_suffix`. A bare `foo()` names a
|
|
1743
|
+
// plain call; `obj.foo()` is a `navigation_expression` whose trailing
|
|
1744
|
+
// `navigation_suffix` holds the method name and whose object is the receiver.
|
|
1745
|
+
const target = node.namedChildren[0];
|
|
1746
|
+
if (target?.type === "simple_identifier")
|
|
1747
|
+
return { name: target.text, viaMember: false };
|
|
1748
|
+
if (target?.type === "navigation_expression") {
|
|
1749
|
+
const suffix = target.namedChildren.find((c) => c.type === "navigation_suffix");
|
|
1750
|
+
const name = suffix?.namedChildren.find((c) => c.type === "simple_identifier");
|
|
1751
|
+
const receiver = target.namedChildren[0];
|
|
1752
|
+
if (!name)
|
|
1753
|
+
return null;
|
|
1754
|
+
if (receiver?.type === "simple_identifier")
|
|
1755
|
+
return { name: name.text, viaMember: true, receiver: receiver.text };
|
|
1756
|
+
if (receiver?.type === "this_expression" || receiver?.type === "super_expression")
|
|
1757
|
+
return { name: name.text, viaMember: true, receiver: receiver.type === "this_expression" ? "this" : "super" };
|
|
1758
|
+
}
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
775
1761
|
if (lang === "php")
|
|
776
1762
|
return phpCallee(node);
|
|
777
1763
|
const fn = node.childForFieldName("function");
|
|
@@ -794,6 +1780,48 @@ function calleeName(node, lang) {
|
|
|
794
1780
|
const p = fn.childForFieldName("property") ?? fn.namedChildren.at(-1);
|
|
795
1781
|
return p ? { name: p.text, viaMember: true, receiver: tsReceiver(fn) } : null;
|
|
796
1782
|
}
|
|
1783
|
+
if (lang === "r" && (fn.type === "extract_operator" || fn.type === "namespace_operator")) {
|
|
1784
|
+
const rhs = fn.childForFieldName("rhs");
|
|
1785
|
+
if (rhs?.type !== "identifier")
|
|
1786
|
+
return null;
|
|
1787
|
+
if (fn.type === "extract_operator") {
|
|
1788
|
+
const lhs = fn.childForFieldName("lhs");
|
|
1789
|
+
if (lhs?.type === "identifier" && (lhs.text === "self" || lhs.text === "private")) {
|
|
1790
|
+
// R6 (Phase 2): `self$method()` / `private$method()` — resolves directly to
|
|
1791
|
+
// the enclosing class via ctx.enclosingClass, same mechanism (and same
|
|
1792
|
+
// magic receiver string) as Python/TS's self/cls/this — see
|
|
1793
|
+
// resolveRecvType, which already special-cases "self" generically.
|
|
1794
|
+
return { name: rhs.text, viaMember: true, receiver: "self" };
|
|
1795
|
+
}
|
|
1796
|
+
if (lhs?.type === "identifier" && lhs.text === "super") {
|
|
1797
|
+
// R6 (Phase 3): `super$method()` — R6's inheritance-dispatch keyword,
|
|
1798
|
+
// resolves directly to the PARENT class via ctx.rSuperClass (NOT
|
|
1799
|
+
// ctx.enclosingClass — that would wrongly find the current class's own
|
|
1800
|
+
// same-named override instead of climbing to the parent).
|
|
1801
|
+
return { name: rhs.text, viaMember: true, receiver: "super" };
|
|
1802
|
+
}
|
|
1803
|
+
// Any other `obj$method()` (Phase 4): still a PLAIN name match, not a
|
|
1804
|
+
// typed member call — there's no general field-type-binding table for
|
|
1805
|
+
// R6 composition (`private$other_obj$method()`), and a real codebase's
|
|
1806
|
+
// dominant field-assignment shape (constructor-parameter pass-through,
|
|
1807
|
+
// `do.call(class_var$new, ...)` dynamic dispatch) turned out to defeat
|
|
1808
|
+
// the simple "field <- SomeClass$new()" pattern every other language's
|
|
1809
|
+
// binding table relies on anyway — see plan_r_language_support.md's
|
|
1810
|
+
// Phase 2 "known gaps" and the follow-up investigation against a real
|
|
1811
|
+
// R6-heavy corpus. What DOES help: bare-name resolution must be allowed
|
|
1812
|
+
// to match a "method" node here, not just "function" — R6 methods are
|
|
1813
|
+
// always kind "method", so without `kinds` below, EVERY untyped `$`
|
|
1814
|
+
// call would be unconditionally unresolvable rather than just
|
|
1815
|
+
// occasionally ambiguous (resolve.ts already drops a genuinely
|
|
1816
|
+
// ambiguous bare-name match rather than guessing, so this only adds
|
|
1817
|
+
// resolutions for uniquely-named methods, never a wrong-class guess).
|
|
1818
|
+
return { name: rhs.text, viaMember: false, kinds: ["function", "method"] };
|
|
1819
|
+
}
|
|
1820
|
+
// `pkg::fun()` (qualified call) — always a real function/exported symbol,
|
|
1821
|
+
// never an R6 method (those are only ever reached via `$` on an instance),
|
|
1822
|
+
// so no need to widen the match kinds here.
|
|
1823
|
+
return { name: rhs.text, viaMember: false };
|
|
1824
|
+
}
|
|
797
1825
|
return null;
|
|
798
1826
|
}
|
|
799
1827
|
/** The number of arguments at a Java call site (`method_invocation` or
|
|
@@ -938,13 +1966,27 @@ function tsReceiver(fn) {
|
|
|
938
1966
|
}
|
|
939
1967
|
return undefined;
|
|
940
1968
|
}
|
|
1969
|
+
/** R has no import statement at the grammar level — `library(x)`, `require(x)`,
|
|
1970
|
+
* and `source("f.R")` are ordinary `call` nodes, indistinguishable from any other
|
|
1971
|
+
* call except by their callee name. This is call-SITE pattern matching, a first
|
|
1972
|
+
* for this function's normal node-type switch — every other language's import
|
|
1973
|
+
* shape is a dedicated grammar construct. */
|
|
1974
|
+
const R_IMPORT_CALLS = new Set(["library", "require", "source"]);
|
|
941
1975
|
function isImport(node, lang) {
|
|
942
1976
|
// Go: match the per-import leaf, so single (`import "fmt"`) and grouped
|
|
943
1977
|
// (`import ( … )`) forms each yield one edge as the walk recurses into the list.
|
|
944
1978
|
if (lang === "go")
|
|
945
1979
|
return node.type === "import_spec";
|
|
1980
|
+
if (lang === "r") {
|
|
1981
|
+
if (node.type !== "call")
|
|
1982
|
+
return false;
|
|
1983
|
+
const fn = node.childForFieldName("function");
|
|
1984
|
+
return fn?.type === "identifier" && R_IMPORT_CALLS.has(fn.text);
|
|
1985
|
+
}
|
|
946
1986
|
if (lang === "java")
|
|
947
1987
|
return node.type === "import_declaration";
|
|
1988
|
+
if (lang === "kotlin")
|
|
1989
|
+
return node.type === "import_header";
|
|
948
1990
|
// PHP: one edge per imported symbol — the clause leaf inside a (possibly
|
|
949
1991
|
// grouped) `use A\B, C\D;` / `use A\{B, C};` declaration.
|
|
950
1992
|
if (lang === "php")
|
|
@@ -967,6 +2009,14 @@ function importSpecifier(node, lang) {
|
|
|
967
2009
|
const path = node.childForFieldName("path") ?? node.namedChildren.at(-1);
|
|
968
2010
|
return path ? path.text.replace(/^["`]|["`]$/g, "") : null;
|
|
969
2011
|
}
|
|
2012
|
+
if (lang === "r") {
|
|
2013
|
+
// library(pkg) / library("pkg") / require(pkg) / source("f.R") — the target is
|
|
2014
|
+
// always the first (and normally only) positional argument, bare symbol or string.
|
|
2015
|
+
const value = rCallArgs(node)[0]?.childForFieldName("value") ?? null;
|
|
2016
|
+
if (value?.type === "identifier")
|
|
2017
|
+
return value.text;
|
|
2018
|
+
return rStringContent(value);
|
|
2019
|
+
}
|
|
970
2020
|
if (lang === "java") {
|
|
971
2021
|
// `import a.b.C;` / `import static a.b.C.d;` / `import a.b.*;` — the fully
|
|
972
2022
|
// qualified name is the scoped_identifier; a wildcard `*` is a separate token
|
|
@@ -974,6 +2024,12 @@ function importSpecifier(node, lang) {
|
|
|
974
2024
|
const id = node.namedChildren.find((c) => c.type === "scoped_identifier" || c.type === "identifier");
|
|
975
2025
|
return id?.text ?? null;
|
|
976
2026
|
}
|
|
2027
|
+
if (lang === "kotlin") {
|
|
2028
|
+
// `import com.example.Foo` — the dotted path is the `identifier` child. A
|
|
2029
|
+
// wildcard (`import a.b.*`) and an `as` alias are separate children, so the
|
|
2030
|
+
// identifier text is already the module path (wildcards dropped, like Java).
|
|
2031
|
+
return node.namedChildren.find((c) => c.type === "identifier")?.text ?? null;
|
|
2032
|
+
}
|
|
977
2033
|
const str = node.namedChildren.find((c) => c.type === "string");
|
|
978
2034
|
if (!str)
|
|
979
2035
|
return null;
|