Merge pull request #40 from a5345534/fix/jvm-symbol-extraction

fix: extract JVM symbol names from declarations
This commit is contained in:
Giancarlo Erra
2026-05-04 10:43:50 +01:00
committed by GitHub
2 changed files with 92 additions and 8 deletions
+36 -8
View File
@@ -470,10 +470,8 @@ function extractFromJvm(
: ["class_declaration", "interface_declaration", "enum_declaration", "object_declaration"];
for (const k of classKinds) {
for (const cls of safeFindAll(root, k)) {
const nameNode = cls.find({ rule: { kind: "type_identifier" } })
?? cls.find({ rule: { kind: "identifier" } });
if (!nameNode) continue;
const name = nameNode.text();
const name = extractJvmTypeName(cls.text(), langKey);
if (!name) continue;
const r = cls.range();
const startLine = r.start.line + 1;
const endLine = r.end.line + 1;
@@ -496,10 +494,8 @@ function extractFromJvm(
: ["method_declaration", "constructor_declaration"];
for (const k of methodKinds) {
for (const m of safeFindAll(root, k)) {
const nameNode = m.find({ rule: { kind: "identifier" } })
?? m.find({ rule: { kind: "simple_identifier" } });
if (!nameNode) continue;
const name = nameNode.text();
const name = extractJvmCallableName(m.text());
if (!name) continue;
const r = m.range();
const startLine = r.start.line + 1;
const endLine = r.end.line + 1;
@@ -533,6 +529,38 @@ function extractFromJvm(
return { symbols, rawCalls };
}
function stripJvmAnnotations(text: string): string {
return text
.split("\n")
.map((line) =>
line.replace(/^\s*(?:@(?:[\w$]+:)?[\w$.]+(?:\([^)]*\))?\s*)+/, "")
)
.join("\n");
}
function extractJvmTypeName(text: string, langKey: string): string | null {
const withoutAnnotations = stripJvmAnnotations(text);
const header = withoutAnnotations.split("{", 1)[0] ?? withoutAnnotations;
const pattern = langKey === "scala"
? /\b(?:class|object|trait)\s+([A-Za-z_$][\w$]*)\b/
: /\b(?:class|interface|enum|object)\s+([A-Za-z_$][\w$]*)\b/;
return header.match(pattern)?.[1] ?? null;
}
function extractJvmCallableName(text: string): string | null {
const withoutAnnotations = stripJvmAnnotations(text);
const signature = withoutAnnotations
.split("{", 1)[0]
.split("=", 1)[0]
.trim();
const scalaDefMatches = Array.from(signature.matchAll(/\bdef\s+([A-Za-z_$][\w$]*)\b/g));
if (scalaDefMatches.length > 0) {
return scalaDefMatches[scalaDefMatches.length - 1][1];
}
const matches = Array.from(signature.matchAll(/([A-Za-z_$][\w$]*)\s*\(/g));
return matches.length > 0 ? matches[matches.length - 1][1] : null;
}
// ── C# ──────────────────────────────────────────────────────────────────
function extractFromCSharp(
+56
View File
@@ -149,6 +149,58 @@ public class Foo {
expect(names).toContain("baz");
});
it("prefers the declared Java class name over parameter types in Spring Boot entrypoints", () => {
const src = `
@SpringBootApplication
public class WorkflowFlowableApplication {
public static void main(String[] args) {
SpringApplication.run(WorkflowFlowableApplication.class, args);
}
}
`;
const out = extractSymbolsAndCalls(src, "java" as unknown as Lang, ".java", "WorkflowFlowableApplication.java");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("WorkflowFlowableApplication");
expect(names).not.toContain("String");
expect(names).toContain("main");
});
it("does not treat Java test annotations as method names", () => {
const src = `
class SecurityAuthClientRequireSubjectTest {
@AfterEach
void cleanup() {}
@Test
void requireSubjectThrows() {}
@Test(timeout = 1000)
void fastTest() {}
}
`;
const out = extractSymbolsAndCalls(src, "java" as unknown as Lang, ".java", "SecurityAuthClientRequireSubjectTest.java");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("SecurityAuthClientRequireSubjectTest");
expect(names).toContain("cleanup");
expect(names).toContain("requireSubjectThrows");
expect(names).toContain("fastTest");
expect(names).not.toContain("AfterEach");
expect(names).not.toContain("Test");
});
it("preserves Java declarations when annotations share the same line", () => {
const src = `
class InlineAnnotationTest {
@Test void cleanup() {}
}
`;
const out = extractSymbolsAndCalls(src, "java" as unknown as Lang, ".java", "InlineAnnotationTest.java");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("InlineAnnotationTest");
expect(names).toContain("cleanup");
expect(names).not.toContain("Test");
});
it("extracts Kotlin top-level fun and class methods", () => {
const src = `
fun greet(name: String): String = "Hi"
@@ -168,6 +220,8 @@ class Bar {
const src = `
class Foo {
def bar(): Int = 1
def size: Int = 1
def now = Instant.now()
}
object Main {
@@ -177,6 +231,8 @@ object Main {
const out = extractSymbolsAndCalls(src, "scala" as unknown as Lang, ".scala", "Main.scala");
const names = out.symbols.map((s) => s.name);
expect(names).toContain("bar");
expect(names).toContain("size");
expect(names).toContain("now");
expect(names).toContain("main");
});
});