|
| 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 | +} |
0 commit comments