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
45 changes: 35 additions & 10 deletions lang/uniast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,17 +346,35 @@ func NewIdentity(mod, pkg, name string) Identity {
}

func NewIdentityFromString(str string) (ret Identity) {
sp := strings.Split(str, "?")
if len(sp) == 2 {
ret.ModPath = sp[0]
str = sp[1]
// Identity format: ModPath?PkgPath#Name
//
// We parse LAST '#' AND FIRST '?' to isolate ModPath, PkgPath, and Name.
// 1. Locate the LAST '#' to isolate the Name. This is crucial for Java where the Name
// itself may contain '?' (e.g., generic wildcards like List<?>).
// 2. Locate the FIRST '?' in the remaining part to separate ModPath and PkgPath.
// Using the first '?' is more robust if PkgPath is a URL containing query parameters.

// Step 1: Separate PkgPath and Name using the last '#'
hashIdx := strings.LastIndex(str, "#")
if hashIdx != -1 {
ret.Name = str[hashIdx+1:]
str = str[:hashIdx]
} else {
// If no '#', the entire string is treated as the Name
ret.Name = str
return ret
}
sp = strings.Split(str, "#")
if len(sp) == 2 {
ret.PkgPath = sp[0]
str = sp[1]

// Step 2: Separate ModPath and PkgPath using the first '?'
questionIdx := strings.Index(str, "?")
if questionIdx != -1 {
ret.ModPath = str[:questionIdx]
ret.PkgPath = str[questionIdx+1:]
} else {
// If no '?', the remaining part is the PkgPath
ret.PkgPath = str
}
ret.Name = str

return ret
}

Expand All @@ -374,7 +392,14 @@ func (i Identity) CallName() string {
}

func (i Identity) Full() string {
return i.ModPath + "?" + i.PkgPath + "#" + i.Name
builder := strings.Builder{}
builder.Grow(len(i.ModPath) + len(i.PkgPath) + len(i.Name) + 2)
builder.WriteString(i.ModPath)
builder.WriteString("?")
builder.WriteString(i.PkgPath)
builder.WriteString("#")
builder.WriteString(i.Name)
return builder.String()
}

// GetFunction the function identified by id.
Expand Down
166 changes: 166 additions & 0 deletions lang/uniast/identity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/**
* Copyright 2025 ByteDance Inc.
*
* 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 uniast

import (
"testing"
)

func TestNewIdentityFromString(t *testing.T) {
tests := []struct {
name string
input string
expected Identity
}{
{
name: "standard format",
input: "mod?pkg#name",
expected: Identity{ModPath: "mod", PkgPath: "pkg", Name: "name"},
},
{
name: "name with question mark - Java wildcard",
input: "mod?pkg#name<?>",
expected: Identity{ModPath: "mod", PkgPath: "pkg", Name: "name<?>"},
},
{
name: "name with multiple question marks",
input: "mod?pkg#Map<?,?>",
expected: Identity{ModPath: "mod", PkgPath: "pkg", Name: "Map<?,?>"},
},
{
name: "no ModPath",
input: "pkg#name",
expected: Identity{ModPath: "", PkgPath: "pkg", Name: "name"},
},
{
name: "no PkgPath and ModPath",
input: "name",
expected: Identity{ModPath: "", PkgPath: "", Name: "name"},
},
{
name: "complex Java generic",
input: "mod?pkg#Function<? super T, ? extends R>",
expected: Identity{ModPath: "mod", PkgPath: "pkg", Name: "Function<? super T, ? extends R>"},
},
{
name: "with version number",
input: "mod@v1.0?pkg#name",
expected: Identity{ModPath: "mod@v1.0", PkgPath: "pkg", Name: "name"},
},
{
name: "Java method with generic parameters",
input: "com.example@1.0?com.example.utils#process<?>",
expected: Identity{ModPath: "com.example@1.0", PkgPath: "com.example.utils", Name: "process<?>"},
},
{
name: "nested generics",
input: "mod?pkg#List<Map<String, ?>>",
expected: Identity{ModPath: "mod", PkgPath: "pkg", Name: "List<Map<String, ?>>"},
},
{
name: "capture wildcard",
input: "mod?pkg#capture of ?",
expected: Identity{ModPath: "mod", PkgPath: "pkg", Name: "capture of ?"},
},
{
name: "ModPath with empty PkgPath and Name",
input: "mod?#",
expected: Identity{ModPath: "mod", PkgPath: "", Name: ""},
},
{
name: "only PkgPath separator",
input: "pkg#",
expected: Identity{ModPath: "", PkgPath: "pkg", Name: ""},
},
{
name: "both separators but empty parts",
input: "?#",
expected: Identity{ModPath: "", PkgPath: "", Name: ""},
},
{
name: "ModPath with version and question mark in name",
input: "mod@v1.0?pkg#method<?>",
expected: Identity{ModPath: "mod@v1.0", PkgPath: "pkg", Name: "method<?>"},
},
{
name: "empty string",
input: "",
expected: Identity{ModPath: "", PkgPath: "", Name: ""},
},
{
"java example 1",
`com.bytedance.ea.travel:travel-web:1.0.0-SNAPSHOT?com.bytedance.ea.travel.web.controller#CommonInfoController.public Result<?> allCountries(@RequestParam(name = "language", required = false) String language)`,
Identity{ModPath: "com.bytedance.ea.travel:travel-web:1.0.0-SNAPSHOT", PkgPath: "com.bytedance.ea.travel.web.controller", Name: "CommonInfoController.public Result<?> allCountries(@RequestParam(name = \"language\", required = false) String language)"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NewIdentityFromString(tt.input)
if result != tt.expected {
t.Errorf("NewIdentityFromString(%q) = %+v, want %+v", tt.input, result, tt.expected)
}
})
}
}

func TestIdentity_Full(t *testing.T) {
tests := []struct {
name string
identity Identity
expected string
}{
{
name: "all parts present",
identity: Identity{ModPath: "mod", PkgPath: "pkg", Name: "name"},
expected: "mod?pkg#name",
},
{
name: "no ModPath",
identity: Identity{ModPath: "", PkgPath: "pkg", Name: "name"},
expected: "?pkg#name",
},
{
name: "no PkgPath",
identity: Identity{ModPath: "mod", PkgPath: "", Name: "name"},
expected: "mod?#name",
},
{
name: "only Name",
identity: Identity{ModPath: "", PkgPath: "", Name: "name"},
expected: "?#name",
},
{
name: "all empty",
identity: Identity{ModPath: "", PkgPath: "", Name: ""},
expected: "?#",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.identity.Full(); got != tt.expected {
t.Errorf("Identity.Full() = %v, want %v", got, tt.expected)
}
// Round-trip test
parsed := NewIdentityFromString(tt.expected)
if parsed != tt.identity {
t.Errorf("NewIdentityFromString(Full()) = %+v, want %+v", parsed, tt.identity)
}
})
}
}
Loading