Skip to content

Commit ec2fdcd

Browse files
authored
fix(java): recognize interface TypeKind and preserve Implements relation (#192)
Two pieces wired through the Java IPC pipeline: 1. **Interface kind detection.** java_parser used to hardcode ClassInfo.classType = "class" for every TypeDeclaration. Now the parser classifies declarations as class / interface / enum / annotation / record (RepoAnalyzer/DependencyResolver) and the StreamingResultSender forwards the value verbatim. abcoder's collect.classKind already mapped CLASS_TYPE_INTERFACE/ENUM to the right SymbolKind, so the universal AST's Type.TypeKind now correctly says "interface"/"enum" instead of always "struct". 2. **Implements relation.** java_parser already emitted ImplementsTypes/ImplementsDetails per class, and abcoder's collector recorded them in c.deps, but export.go blended them into the generic SubStruct dependency list and Type.Implements stayed empty. Add an implementsRel map on Collector (kept parallel to c.deps), populate it from both the javaIPC scanner path and the tree-sitter class_declaration path, and have exportSymbol emit Type.Implements while de-duping the same target from SubStruct.
1 parent 9982ee1 commit ec2fdcd

12 files changed

Lines changed: 338 additions & 1 deletion

File tree

lang/collect/collect.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ type Collector struct {
6767
// symbol => [deps]
6868
deps map[*DocumentSymbol][]dependency
6969

70+
// type symbol => list of interfaces it implements.
71+
// Populated alongside c.deps but kept separately so Export can emit
72+
// Type.Implements distinct from the generic SubStruct dependency list.
73+
implementsRel map[*DocumentSymbol][]dependency
74+
7075
// variable (or const) => type
7176
vars map[*DocumentSymbol]dependency
7277

@@ -101,6 +106,19 @@ func (c *Collector) UseJavaIPC(conv *javaipc.Converter) {
101106
c.javaIPC = conv
102107
}
103108

109+
// addImplementsRel records that `from` implements `iface`. Idempotent on (from, iface).
110+
func (c *Collector) addImplementsRel(from *DocumentSymbol, iface *DocumentSymbol, tokenLoc Location) {
111+
if from == nil || iface == nil {
112+
return
113+
}
114+
for _, existing := range c.implementsRel[from] {
115+
if existing.Symbol == iface {
116+
return
117+
}
118+
}
119+
c.implementsRel[from] = append(c.implementsRel[from], dependency{Location: tokenLoc, Symbol: iface})
120+
}
121+
104122
type methodInfo struct {
105123
Receiver dependency `json:"receiver"`
106124
Interface *dependency `json:"implement,omitempty"` // which interface it implements
@@ -143,6 +161,7 @@ func NewCollector(repo string, cli *LSPClient) *Collector {
143161
syms: map[Location]*DocumentSymbol{},
144162
funcs: map[*DocumentSymbol]functionInfo{},
145163
deps: map[*DocumentSymbol][]dependency{},
164+
implementsRel: map[*DocumentSymbol][]dependency{},
146165
vars: map[*DocumentSymbol]dependency{},
147166
files: map[string]*uniast.File{},
148167
fileContentCache: make(map[string]string),
@@ -760,6 +779,7 @@ func (c *Collector) ScannerByJavaIPC(ctx context.Context) ([]*DocumentSymbol, er
760779
}
761780
tokLoc := locFromPos(fileAbs, impl.StartLine, impl.StartColumn, impl.EndLine, impl.EndColumn)
762781
addDep(classSym, depSym, tokLoc)
782+
c.addImplementsRel(classSym, depSym, tokLoc)
763783
}
764784
} else {
765785
for _, impl := range ci.ImplementsTypes {
@@ -777,6 +797,7 @@ func (c *Collector) ScannerByJavaIPC(ctx context.Context) ([]*DocumentSymbol, er
777797
depSym.Kind = SKInterface
778798
}
779799
addDep(classSym, depSym, classSym.Location)
800+
c.addImplementsRel(classSym, depSym, classSym.Location)
780801
}
781802
}
782803

@@ -1587,6 +1608,7 @@ func (c *Collector) walk(node *sitter.Node, uri DocumentURI, content []byte, fil
15871608
impl.Kind = SKInterface
15881609
impl.Role = REFERENCE
15891610
c.addReferenceDeps(sym, impl)
1611+
c.addImplementsRel(sym, impl, impl.Location)
15901612
}
15911613
}
15921614
}

lang/collect/export.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,10 +573,29 @@ func (c *Collector) exportSymbol(repo *uniast.Repository, symbol *DocumentSymbol
573573
TypeKind: mapKind(k),
574574
Exported: public,
575575
}
576-
// collect deps
576+
// Implements relationship is preserved as a first-class field rather
577+
// than blended into the generic SubStruct dependency list.
578+
implSyms := map[*DocumentSymbol]bool{}
579+
if rels := c.implementsRel[symbol]; rels != nil {
580+
for _, rel := range rels {
581+
tok := ""
582+
if c.cli != nil {
583+
tok, _ = c.cli.Locate(rel.Location)
584+
}
585+
iid, err := c.exportSymbol(repo, rel.Symbol, tok, visited)
586+
if err != nil {
587+
continue
588+
}
589+
obj.Implements = append(obj.Implements, *iid)
590+
implSyms[rel.Symbol] = true
591+
}
592+
}
577593
// collect deps
578594
if deps := c.deps[symbol]; deps != nil {
579595
for _, dep := range deps {
596+
if implSyms[dep.Symbol] {
597+
continue
598+
}
580599
tok := ""
581600
if c.cli != nil {
582601
tok, _ = c.cli.Locate(dep.Location)
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright 2025 CloudWeGo Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package collect
16+
17+
import (
18+
"context"
19+
"path/filepath"
20+
"runtime"
21+
"testing"
22+
23+
javaipc "github.com/cloudwego/abcoder/lang/java/ipc"
24+
javapb "github.com/cloudwego/abcoder/lang/java/pb"
25+
"github.com/cloudwego/abcoder/lang/lsp"
26+
"github.com/cloudwego/abcoder/lang/uniast"
27+
)
28+
29+
// TestJavaIPC_InterfaceKindAndImplements drives the Java parser → universal AST
30+
// pipeline for the two regressions:
31+
// 1. Interface declarations must be exported with TypeKind == "interface".
32+
// 2. A class that "implements I" must have I in Type.Implements (and not be
33+
// a plain SubStruct dependency).
34+
//
35+
// We hand-build the javaipc.Converter so the test does not require the real
36+
// java parser binary; it still exercises ScannerByJavaIPC + Export end-to-end
37+
// against the real fixture source files under testdata/java/5_interface_impl.
38+
func TestJavaIPC_InterfaceKindAndImplements(t *testing.T) {
39+
repo := fixtureRepo(t)
40+
conv := buildInterfaceFixtureConverter(repo)
41+
42+
cli := &lsp.LSPClient{ClientOptions: lsp.ClientOptions{Language: uniast.Java}}
43+
c := NewCollector(repo, cli)
44+
c.Language = uniast.Java
45+
c.NeedStdSymbol = true
46+
c.UseJavaIPC(conv)
47+
48+
if _, err := c.ScannerByJavaIPC(context.Background()); err != nil {
49+
t.Fatalf("ScannerByJavaIPC failed: %v", err)
50+
}
51+
rep, err := c.Export(context.Background())
52+
if err != nil {
53+
t.Fatalf("Export failed: %v", err)
54+
}
55+
56+
types := collectExportedTypes(rep)
57+
58+
animal, ok := types["Animal"]
59+
if !ok {
60+
t.Fatalf("Animal type not exported; got types: %v", typeNames(types))
61+
}
62+
if animal.TypeKind != uniast.TypeKindInterface {
63+
t.Errorf("Animal.TypeKind = %q, want %q", animal.TypeKind, uniast.TypeKindInterface)
64+
}
65+
66+
swimmer, ok := types["Swimmer"]
67+
if !ok {
68+
t.Fatalf("Swimmer type not exported; got types: %v", typeNames(types))
69+
}
70+
if swimmer.TypeKind != uniast.TypeKindInterface {
71+
t.Errorf("Swimmer.TypeKind = %q, want %q", swimmer.TypeKind, uniast.TypeKindInterface)
72+
}
73+
74+
dog, ok := types["Dog"]
75+
if !ok {
76+
t.Fatalf("Dog type not exported; got types: %v", typeNames(types))
77+
}
78+
if dog.TypeKind != uniast.TypeKindStruct {
79+
t.Errorf("Dog.TypeKind = %q, want %q", dog.TypeKind, uniast.TypeKindStruct)
80+
}
81+
if !containsIdentityName(dog.Implements, "Animal") {
82+
t.Errorf("Dog.Implements does not contain Animal; got %v", identityNames(dog.Implements))
83+
}
84+
if containsDependencyName(dog.SubStruct, "Animal") {
85+
t.Errorf("Dog.SubStruct should not duplicate the Animal implements relation; got %v",
86+
dependencyNames(dog.SubStruct))
87+
}
88+
89+
fish, ok := types["Fish"]
90+
if !ok {
91+
t.Fatalf("Fish type not exported; got types: %v", typeNames(types))
92+
}
93+
if !containsIdentityName(fish.Implements, "Animal") {
94+
t.Errorf("Fish.Implements missing Animal; got %v", identityNames(fish.Implements))
95+
}
96+
if !containsIdentityName(fish.Implements, "Swimmer") {
97+
t.Errorf("Fish.Implements missing Swimmer; got %v", identityNames(fish.Implements))
98+
}
99+
}
100+
101+
// fixtureRepo returns the absolute path to testdata/java/5_interface_impl.
102+
func fixtureRepo(t *testing.T) string {
103+
t.Helper()
104+
_, thisFile, _, ok := runtime.Caller(0)
105+
if !ok {
106+
t.Fatalf("runtime.Caller failed")
107+
}
108+
return filepath.Join(filepath.Dir(thisFile), "..", "..", "testdata", "java", "5_interface_impl")
109+
}
110+
111+
func buildInterfaceFixtureConverter(repo string) *javaipc.Converter {
112+
conv := javaipc.NewConverter(repo, "test-mod")
113+
114+
srcDir := filepath.Join(repo, "src", "main", "java", "org", "example")
115+
mk := func(fqcn, fname string, kind javapb.ClassType, startLine, endLine int32, implements []string) *javapb.ClassInfo {
116+
return &javapb.ClassInfo{
117+
ClassName: fqcn,
118+
PackageName: "org.example",
119+
FilePath: filepath.Join(srcDir, fname),
120+
ClassType: kind,
121+
ImplementsTypes: implements,
122+
StartLine: startLine,
123+
StartColumn: 1,
124+
EndLine: endLine,
125+
EndColumn: 2,
126+
Source: &javapb.SourceInfo{Type: javapb.SourceType_SOURCE_TYPE_LOCAL},
127+
}
128+
}
129+
conv.LocalClassCache["org.example.Animal"] =
130+
mk("org.example.Animal", "Animal.java", javapb.ClassType_CLASS_TYPE_INTERFACE, 3, 6, nil)
131+
conv.LocalClassCache["org.example.Swimmer"] =
132+
mk("org.example.Swimmer", "Swimmer.java", javapb.ClassType_CLASS_TYPE_INTERFACE, 3, 5, nil)
133+
conv.LocalClassCache["org.example.Dog"] =
134+
mk("org.example.Dog", "Dog.java", javapb.ClassType_CLASS_TYPE_CLASS, 3, 19,
135+
[]string{"org.example.Animal"})
136+
conv.LocalClassCache["org.example.Fish"] =
137+
mk("org.example.Fish", "Fish.java", javapb.ClassType_CLASS_TYPE_CLASS, 3, 23,
138+
[]string{"org.example.Animal", "org.example.Swimmer"})
139+
return conv
140+
}
141+
142+
func collectExportedTypes(rep *uniast.Repository) map[string]*uniast.Type {
143+
out := map[string]*uniast.Type{}
144+
for _, mod := range rep.Modules {
145+
for _, pkg := range mod.Packages {
146+
for _, ty := range pkg.Types {
147+
out[ty.Identity.Name] = ty
148+
}
149+
}
150+
}
151+
return out
152+
}
153+
154+
func typeNames(m map[string]*uniast.Type) []string {
155+
out := make([]string, 0, len(m))
156+
for k := range m {
157+
out = append(out, k)
158+
}
159+
return out
160+
}
161+
162+
func containsIdentityName(ids []uniast.Identity, name string) bool {
163+
for _, id := range ids {
164+
if id.Name == name {
165+
return true
166+
}
167+
}
168+
return false
169+
}
170+
171+
func identityNames(ids []uniast.Identity) []string {
172+
out := make([]string, 0, len(ids))
173+
for _, id := range ids {
174+
out = append(out, id.Name)
175+
}
176+
return out
177+
}
178+
179+
func containsDependencyName(deps []uniast.Dependency, name string) bool {
180+
for _, d := range deps {
181+
if d.Identity.Name == name {
182+
return true
183+
}
184+
}
185+
return false
186+
}
187+
188+
func dependencyNames(deps []uniast.Dependency) []string {
189+
out := make([]string, 0, len(deps))
190+
for _, d := range deps {
191+
out = append(out, d.Identity.Name)
192+
}
193+
return out
194+
}
2.86 MB
Binary file not shown.

testdata/java/0_simple/pom.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>simple</groupId>
8+
<artifactId>simple</artifactId>
9+
<version>1.0.0</version>
10+
<packaging>jar</packaging>
11+
12+
<build>
13+
<sourceDirectory>.</sourceDirectory>
14+
</build>
15+
</project>

testdata/java/1_advanced/pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>org.example</groupId>
8+
<artifactId>advanced</artifactId>
9+
<version>1.0.0</version>
10+
<packaging>jar</packaging>
11+
</project>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>org.example</groupId>
8+
<artifactId>inheritance</artifactId>
9+
<version>1.0.0</version>
10+
<packaging>jar</packaging>
11+
</project>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>org.example</groupId>
8+
<artifactId>interface-impl</artifactId>
9+
<version>1.0.0</version>
10+
<packaging>jar</packaging>
11+
</project>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.example;
2+
3+
public interface Animal {
4+
void eat();
5+
String name();
6+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.example;
2+
3+
public class Dog implements Animal {
4+
private final String n;
5+
6+
public Dog(String n) {
7+
this.n = n;
8+
}
9+
10+
@Override
11+
public void eat() {
12+
System.out.println(n + " eats.");
13+
}
14+
15+
@Override
16+
public String name() {
17+
return n;
18+
}
19+
}

0 commit comments

Comments
 (0)