Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion strategies/extractors.go
Original file line number Diff line number Diff line change
Expand Up @@ -1908,11 +1908,23 @@ func javaVisit(node *sitter.Node, filePath, blobSHA string, src []byte, imports
javaMethodDecl(n, astkit.KindMethod, filePath, blobSHA, src, imports, parentClass, out)
case "enum_body_declarations":
javaVisit(n, filePath, blobSHA, src, imports, parentClass, out)
case "static_initializer", "block":
// `static { ... }` and instance initializer blocks: anonymous
// classes and lambdas declared inside them belong to the class.
if parentClass != "" {
javaNestedExecutables(n, filePath, blobSHA, src, imports, parentClass, out)
}
case "enum_constant":
if parentClass == "" {
continue
}
javaEnumConstantDecl(n, src, parentClass, out)
// `TWEETS(new TypeToken<List<Tweet>>() {})`: anonymous classes
// and lambdas passed as constructor arguments are declarations of
// the enum (gson ParseBenchmark, 2026-10-12).
if args := n.ChildByFieldName("arguments"); args != nil {
javaNestedExecutables(args, filePath, blobSHA, src, imports, parentClass, out)
}
if body := n.ChildByFieldName("body"); body != nil {
javaVisit(body, filePath, blobSHA, src, imports, parentClass, out)
}
Expand Down Expand Up @@ -2245,7 +2257,11 @@ func javaLambdaDecl(n *sitter.Node, filePath, blobSHA string, src []byte, import
Span: internalast.NodeSpan(n), Body: n.Content(src), Modifiers: []string{"lambda"},
CallSites: javaCallSites(body, src),
})
javaNestedExecutables(body, filePath, blobSHA, src, imports, parentClass, out)
// Walk the lambda node, not its body: an expression body can itself be
// the anonymous class or lambda (`url -> new Monitor() {...}`, dubbo;
// `() -> new SubTypeToken<Integer>() {}`, gson), and the walker only
// inspects children (2026-10-12).
javaNestedExecutables(n, filePath, blobSHA, src, imports, parentClass, out)
}

func javaFieldDecl(n *sitter.Node, filePath, blobSHA string, src []byte, imports []string, parentClass string, out *[]astkit.Symbol) {
Expand Down Expand Up @@ -2277,6 +2293,13 @@ func javaFieldDecl(n *sitter.Node, filePath, blobSHA string, src []byte, imports
Modifiers: modifiers,
Annotations: javaAnnotations(n, src),
})
// An initializer declares code too: `CLASS = new TypeAdapter<>() {
// write(...) {...} }` (gson TypeAdapters has 25 such adapters; none
// was indexed, so change_impact on TypeAdapter.write reported the
// family closed at 6 of 31 overrides, 2026-10-12). Lambdas likewise.
// Walk the declarator, not its value: the value node is often the
// anonymous class or lambda itself, and the walker inspects children.
javaNestedExecutables(decl, filePath, blobSHA, src, imports, parentClass, out)
}
}

Expand Down
67 changes: 67 additions & 0 deletions strategies/metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1412,3 +1412,70 @@ public class Outer2 {
}
}
}

// gson TypeAdapters.java (2026-10-12): 25 of its 31 TypeAdapter.write
// overrides live in anonymous classes assigned to static fields, and none
// was indexed. Anonymous classes and lambdas in field initializers and
// initializer blocks are declarations of the enclosing class.
func TestJava_AnonymousClassesInFieldInitializersAndBlocks(t *testing.T) {
src := `class Adapters {
static final Adapter CLASS = new Adapter() {
@Override public void write(Out out, Object v) { out.nullValue(); }
};
final Runnable r = () -> { run(); };
static {
REGISTRY.add(new Adapter() { public void write(Out out, Object v) {} });
}
{ helper = new Adapter() { public void write(Out out, Object v) {} }; }
abstract static class Adapter { public abstract void write(Out out, Object v); }
}`
syms, _ := extract(t, astkit.LangJava, src)
var anon, writes, lambdas int
for _, s := range syms {
switch {
case s.Kind == astkit.KindClass && strings.HasPrefix(s.Name, "<anonymous@"):
anon++
if s.ParentName != "Adapters" {
t.Errorf("anonymous class %s parent=%q want Adapters", s.Name, s.ParentName)
}
case s.Kind == astkit.KindMethod && s.Name == "write" && strings.Contains(s.ParentName, "<anonymous@"):
writes++
case s.Kind == astkit.KindFunction && strings.HasPrefix(s.Name, "<lambda@"):
lambdas++
}
}
if anon != 3 || writes != 3 || lambdas != 1 {
t.Errorf("anonymous=%d write overrides=%d lambdas=%d; want 3, 3, 1", anon, writes, lambdas)
}
}

// dubbo MonitorFilterTest / gson TypeTokenTest / gson ParseBenchmark
// (2026-10-12): an anonymous class that IS a lambda's expression body, or an
// enum constant's constructor argument, was never declared; javac found them.
func TestJava_AnonymousClassesAsLambdaBodiesAndEnumArguments(t *testing.T) {
src := `class Holder {
Factory f = url -> new Monitor() {
public String getUrl() { return url; }
};
void m() { run(() -> new Token<Integer>() {}); }
enum Api {
TWEETS(new Token<String>() {}, x -> x);
Api(Token<?> t, Fn f) {}
}
}`
syms, _ := extract(t, astkit.LangJava, src)
var anon, getURL, lambdas int
for _, s := range syms {
switch {
case s.Kind == astkit.KindClass && strings.HasPrefix(s.Name, "<anonymous@"):
anon++
case s.Kind == astkit.KindMethod && s.Name == "getUrl" && strings.HasPrefix(s.ParentName, "<anonymous@"):
getURL++
case s.Kind == astkit.KindFunction && strings.HasPrefix(s.Name, "<lambda@"):
lambdas++
}
}
if anon != 3 || getURL != 1 || lambdas != 3 {
t.Errorf("anonymous=%d getUrl=%d lambdas=%d; want 3, 1, 3", anon, getURL, lambdas)
}
}
Loading