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
22 changes: 22 additions & 0 deletions lang/collect/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
}
}
Expand Down
21 changes: 20 additions & 1 deletion lang/collect/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
194 changes: 194 additions & 0 deletions lang/collect/java_interface_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Binary file modified lang/java/ipc/abcoder-java-analyzer-1.0-SNAPSHOT.jar
Binary file not shown.
15 changes: 15 additions & 0 deletions testdata/java/0_simple/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>simple</groupId>
<artifactId>simple</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<build>
<sourceDirectory>.</sourceDirectory>
</build>
</project>
11 changes: 11 additions & 0 deletions testdata/java/1_advanced/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>advanced</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
</project>
11 changes: 11 additions & 0 deletions testdata/java/2_inheritance/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>inheritance</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
</project>
11 changes: 11 additions & 0 deletions testdata/java/5_interface_impl/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>interface-impl</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.example;

public interface Animal {
void eat();
String name();
}
19 changes: 19 additions & 0 deletions testdata/java/5_interface_impl/src/main/java/org/example/Dog.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
24 changes: 24 additions & 0 deletions testdata/java/5_interface_impl/src/main/java/org/example/Fish.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.example;

public interface Swimmer {
void swim();
}
Loading