diff --git a/lang/collect/collect.go b/lang/collect/collect.go index 49a4d265..dbd4bcfe 100644 --- a/lang/collect/collect.go +++ b/lang/collect/collect.go @@ -67,6 +67,11 @@ type Collector struct { // symbol => [deps] deps map[*DocumentSymbol][]dependency + // type symbol => list of interfaces it implements. + // Populated alongside c.deps but kept separately so Export can emit + // Type.Implements distinct from the generic SubStruct dependency list. + implementsRel map[*DocumentSymbol][]dependency + // variable (or const) => type vars map[*DocumentSymbol]dependency @@ -101,6 +106,19 @@ func (c *Collector) UseJavaIPC(conv *javaipc.Converter) { c.javaIPC = conv } +// addImplementsRel records that `from` implements `iface`. Idempotent on (from, iface). +func (c *Collector) addImplementsRel(from *DocumentSymbol, iface *DocumentSymbol, tokenLoc Location) { + if from == nil || iface == nil { + return + } + for _, existing := range c.implementsRel[from] { + if existing.Symbol == iface { + return + } + } + c.implementsRel[from] = append(c.implementsRel[from], dependency{Location: tokenLoc, Symbol: iface}) +} + type methodInfo struct { Receiver dependency `json:"receiver"` Interface *dependency `json:"implement,omitempty"` // which interface it implements @@ -143,6 +161,7 @@ func NewCollector(repo string, cli *LSPClient) *Collector { syms: map[Location]*DocumentSymbol{}, funcs: map[*DocumentSymbol]functionInfo{}, deps: map[*DocumentSymbol][]dependency{}, + implementsRel: map[*DocumentSymbol][]dependency{}, vars: map[*DocumentSymbol]dependency{}, files: map[string]*uniast.File{}, fileContentCache: make(map[string]string), @@ -760,6 +779,7 @@ func (c *Collector) ScannerByJavaIPC(ctx context.Context) ([]*DocumentSymbol, er } tokLoc := locFromPos(fileAbs, impl.StartLine, impl.StartColumn, impl.EndLine, impl.EndColumn) addDep(classSym, depSym, tokLoc) + c.addImplementsRel(classSym, depSym, tokLoc) } } else { for _, impl := range ci.ImplementsTypes { @@ -777,6 +797,7 @@ func (c *Collector) ScannerByJavaIPC(ctx context.Context) ([]*DocumentSymbol, er depSym.Kind = SKInterface } addDep(classSym, depSym, classSym.Location) + c.addImplementsRel(classSym, depSym, classSym.Location) } } @@ -1587,6 +1608,7 @@ func (c *Collector) walk(node *sitter.Node, uri DocumentURI, content []byte, fil impl.Kind = SKInterface impl.Role = REFERENCE c.addReferenceDeps(sym, impl) + c.addImplementsRel(sym, impl, impl.Location) } } } diff --git a/lang/collect/export.go b/lang/collect/export.go index 2f63f9af..f7e7d560 100644 --- a/lang/collect/export.go +++ b/lang/collect/export.go @@ -573,10 +573,29 @@ func (c *Collector) exportSymbol(repo *uniast.Repository, symbol *DocumentSymbol TypeKind: mapKind(k), Exported: public, } - // collect deps + // Implements relationship is preserved as a first-class field rather + // than blended into the generic SubStruct dependency list. + implSyms := map[*DocumentSymbol]bool{} + if rels := c.implementsRel[symbol]; rels != nil { + for _, rel := range rels { + tok := "" + if c.cli != nil { + tok, _ = c.cli.Locate(rel.Location) + } + iid, err := c.exportSymbol(repo, rel.Symbol, tok, visited) + if err != nil { + continue + } + obj.Implements = append(obj.Implements, *iid) + implSyms[rel.Symbol] = true + } + } // collect deps if deps := c.deps[symbol]; deps != nil { for _, dep := range deps { + if implSyms[dep.Symbol] { + continue + } tok := "" if c.cli != nil { tok, _ = c.cli.Locate(dep.Location) diff --git a/lang/collect/java_interface_test.go b/lang/collect/java_interface_test.go new file mode 100644 index 00000000..ad63ffbc --- /dev/null +++ b/lang/collect/java_interface_test.go @@ -0,0 +1,194 @@ +// Copyright 2025 CloudWeGo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collect + +import ( + "context" + "path/filepath" + "runtime" + "testing" + + javaipc "github.com/cloudwego/abcoder/lang/java/ipc" + javapb "github.com/cloudwego/abcoder/lang/java/pb" + "github.com/cloudwego/abcoder/lang/lsp" + "github.com/cloudwego/abcoder/lang/uniast" +) + +// TestJavaIPC_InterfaceKindAndImplements drives the Java parser → universal AST +// pipeline for the two regressions: +// 1. Interface declarations must be exported with TypeKind == "interface". +// 2. A class that "implements I" must have I in Type.Implements (and not be +// a plain SubStruct dependency). +// +// We hand-build the javaipc.Converter so the test does not require the real +// java parser binary; it still exercises ScannerByJavaIPC + Export end-to-end +// against the real fixture source files under testdata/java/5_interface_impl. +func TestJavaIPC_InterfaceKindAndImplements(t *testing.T) { + repo := fixtureRepo(t) + conv := buildInterfaceFixtureConverter(repo) + + cli := &lsp.LSPClient{ClientOptions: lsp.ClientOptions{Language: uniast.Java}} + c := NewCollector(repo, cli) + c.Language = uniast.Java + c.NeedStdSymbol = true + c.UseJavaIPC(conv) + + if _, err := c.ScannerByJavaIPC(context.Background()); err != nil { + t.Fatalf("ScannerByJavaIPC failed: %v", err) + } + rep, err := c.Export(context.Background()) + if err != nil { + t.Fatalf("Export failed: %v", err) + } + + types := collectExportedTypes(rep) + + animal, ok := types["Animal"] + if !ok { + t.Fatalf("Animal type not exported; got types: %v", typeNames(types)) + } + if animal.TypeKind != uniast.TypeKindInterface { + t.Errorf("Animal.TypeKind = %q, want %q", animal.TypeKind, uniast.TypeKindInterface) + } + + swimmer, ok := types["Swimmer"] + if !ok { + t.Fatalf("Swimmer type not exported; got types: %v", typeNames(types)) + } + if swimmer.TypeKind != uniast.TypeKindInterface { + t.Errorf("Swimmer.TypeKind = %q, want %q", swimmer.TypeKind, uniast.TypeKindInterface) + } + + dog, ok := types["Dog"] + if !ok { + t.Fatalf("Dog type not exported; got types: %v", typeNames(types)) + } + if dog.TypeKind != uniast.TypeKindStruct { + t.Errorf("Dog.TypeKind = %q, want %q", dog.TypeKind, uniast.TypeKindStruct) + } + if !containsIdentityName(dog.Implements, "Animal") { + t.Errorf("Dog.Implements does not contain Animal; got %v", identityNames(dog.Implements)) + } + if containsDependencyName(dog.SubStruct, "Animal") { + t.Errorf("Dog.SubStruct should not duplicate the Animal implements relation; got %v", + dependencyNames(dog.SubStruct)) + } + + fish, ok := types["Fish"] + if !ok { + t.Fatalf("Fish type not exported; got types: %v", typeNames(types)) + } + if !containsIdentityName(fish.Implements, "Animal") { + t.Errorf("Fish.Implements missing Animal; got %v", identityNames(fish.Implements)) + } + if !containsIdentityName(fish.Implements, "Swimmer") { + t.Errorf("Fish.Implements missing Swimmer; got %v", identityNames(fish.Implements)) + } +} + +// fixtureRepo returns the absolute path to testdata/java/5_interface_impl. +func fixtureRepo(t *testing.T) string { + t.Helper() + _, thisFile, _, ok := runtime.Caller(0) + if !ok { + t.Fatalf("runtime.Caller failed") + } + return filepath.Join(filepath.Dir(thisFile), "..", "..", "testdata", "java", "5_interface_impl") +} + +func buildInterfaceFixtureConverter(repo string) *javaipc.Converter { + conv := javaipc.NewConverter(repo, "test-mod") + + srcDir := filepath.Join(repo, "src", "main", "java", "org", "example") + mk := func(fqcn, fname string, kind javapb.ClassType, startLine, endLine int32, implements []string) *javapb.ClassInfo { + return &javapb.ClassInfo{ + ClassName: fqcn, + PackageName: "org.example", + FilePath: filepath.Join(srcDir, fname), + ClassType: kind, + ImplementsTypes: implements, + StartLine: startLine, + StartColumn: 1, + EndLine: endLine, + EndColumn: 2, + Source: &javapb.SourceInfo{Type: javapb.SourceType_SOURCE_TYPE_LOCAL}, + } + } + conv.LocalClassCache["org.example.Animal"] = + mk("org.example.Animal", "Animal.java", javapb.ClassType_CLASS_TYPE_INTERFACE, 3, 6, nil) + conv.LocalClassCache["org.example.Swimmer"] = + mk("org.example.Swimmer", "Swimmer.java", javapb.ClassType_CLASS_TYPE_INTERFACE, 3, 5, nil) + conv.LocalClassCache["org.example.Dog"] = + mk("org.example.Dog", "Dog.java", javapb.ClassType_CLASS_TYPE_CLASS, 3, 19, + []string{"org.example.Animal"}) + conv.LocalClassCache["org.example.Fish"] = + mk("org.example.Fish", "Fish.java", javapb.ClassType_CLASS_TYPE_CLASS, 3, 23, + []string{"org.example.Animal", "org.example.Swimmer"}) + return conv +} + +func collectExportedTypes(rep *uniast.Repository) map[string]*uniast.Type { + out := map[string]*uniast.Type{} + for _, mod := range rep.Modules { + for _, pkg := range mod.Packages { + for _, ty := range pkg.Types { + out[ty.Identity.Name] = ty + } + } + } + return out +} + +func typeNames(m map[string]*uniast.Type) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + return out +} + +func containsIdentityName(ids []uniast.Identity, name string) bool { + for _, id := range ids { + if id.Name == name { + return true + } + } + return false +} + +func identityNames(ids []uniast.Identity) []string { + out := make([]string, 0, len(ids)) + for _, id := range ids { + out = append(out, id.Name) + } + return out +} + +func containsDependencyName(deps []uniast.Dependency, name string) bool { + for _, d := range deps { + if d.Identity.Name == name { + return true + } + } + return false +} + +func dependencyNames(deps []uniast.Dependency) []string { + out := make([]string, 0, len(deps)) + for _, d := range deps { + out = append(out, d.Identity.Name) + } + return out +} diff --git a/lang/java/ipc/abcoder-java-analyzer-1.0-SNAPSHOT.jar b/lang/java/ipc/abcoder-java-analyzer-1.0-SNAPSHOT.jar index 49978268..dca3520c 100644 Binary files a/lang/java/ipc/abcoder-java-analyzer-1.0-SNAPSHOT.jar and b/lang/java/ipc/abcoder-java-analyzer-1.0-SNAPSHOT.jar differ diff --git a/testdata/java/0_simple/pom.xml b/testdata/java/0_simple/pom.xml new file mode 100644 index 00000000..fc66a296 --- /dev/null +++ b/testdata/java/0_simple/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + simple + simple + 1.0.0 + jar + + + . + + diff --git a/testdata/java/1_advanced/pom.xml b/testdata/java/1_advanced/pom.xml new file mode 100644 index 00000000..42115658 --- /dev/null +++ b/testdata/java/1_advanced/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + + org.example + advanced + 1.0.0 + jar + diff --git a/testdata/java/2_inheritance/pom.xml b/testdata/java/2_inheritance/pom.xml new file mode 100644 index 00000000..26fa691f --- /dev/null +++ b/testdata/java/2_inheritance/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + + org.example + inheritance + 1.0.0 + jar + diff --git a/testdata/java/5_interface_impl/pom.xml b/testdata/java/5_interface_impl/pom.xml new file mode 100644 index 00000000..496e8596 --- /dev/null +++ b/testdata/java/5_interface_impl/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + + org.example + interface-impl + 1.0.0 + jar + diff --git a/testdata/java/5_interface_impl/src/main/java/org/example/Animal.java b/testdata/java/5_interface_impl/src/main/java/org/example/Animal.java new file mode 100644 index 00000000..4c745a94 --- /dev/null +++ b/testdata/java/5_interface_impl/src/main/java/org/example/Animal.java @@ -0,0 +1,6 @@ +package org.example; + +public interface Animal { + void eat(); + String name(); +} diff --git a/testdata/java/5_interface_impl/src/main/java/org/example/Dog.java b/testdata/java/5_interface_impl/src/main/java/org/example/Dog.java new file mode 100644 index 00000000..88a7861b --- /dev/null +++ b/testdata/java/5_interface_impl/src/main/java/org/example/Dog.java @@ -0,0 +1,19 @@ +package org.example; + +public class Dog implements Animal { + private final String n; + + public Dog(String n) { + this.n = n; + } + + @Override + public void eat() { + System.out.println(n + " eats."); + } + + @Override + public String name() { + return n; + } +} diff --git a/testdata/java/5_interface_impl/src/main/java/org/example/Fish.java b/testdata/java/5_interface_impl/src/main/java/org/example/Fish.java new file mode 100644 index 00000000..b9939a6a --- /dev/null +++ b/testdata/java/5_interface_impl/src/main/java/org/example/Fish.java @@ -0,0 +1,24 @@ +package org.example; + +public class Fish implements Animal, Swimmer { + private final String n; + + public Fish(String n) { + this.n = n; + } + + @Override + public void eat() { + System.out.println(n + " eats."); + } + + @Override + public void swim() { + System.out.println(n + " swims."); + } + + @Override + public String name() { + return n; + } +} diff --git a/testdata/java/5_interface_impl/src/main/java/org/example/Swimmer.java b/testdata/java/5_interface_impl/src/main/java/org/example/Swimmer.java new file mode 100644 index 00000000..7978e1cd --- /dev/null +++ b/testdata/java/5_interface_impl/src/main/java/org/example/Swimmer.java @@ -0,0 +1,5 @@ +package org.example; + +public interface Swimmer { + void swim(); +}