Skip to content
Open
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
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ require (
github.com/gobwas/glob v0.2.3
github.com/samber/lo v1.38.1
github.com/segmentio/ksuid v1.0.4
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.14.4
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/sys v0.30.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
Expand All @@ -16,3 +22,7 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
26 changes: 26 additions & 0 deletions javajive_tests/decompiler/core/values/expression_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package values_test

import (
"testing"

"github.com/stretchr/testify/require"
"github.com/yaklang/javajive/classparser/decompiler/core/class_context"
"github.com/yaklang/javajive/classparser/decompiler/core/values/types"
values "github.com/yaklang/javajive/classparser/decompiler/core/values"
)

func TestNewExpressionMultidimensionalArrayString(t *testing.T) {
funcCtx := &class_context.ClassContext{}
stringClass := types.NewJavaClass("java.lang.String")
array2d := types.NewJavaArrayType(types.NewJavaArrayType(stringClass))
length2 := values.NewCustomValue(func(funcCtx *class_context.ClassContext) string { return "2" }, func() types.JavaType {
return types.NewJavaPrimer(types.JavaInteger)
})
length4 := values.NewCustomValue(func(funcCtx *class_context.ClassContext) string { return "4" }, func() types.JavaType {
return types.NewJavaPrimer(types.JavaInteger)
})

exp := values.NewNewArrayExpression(array2d, length2, length4)
got := exp.String(funcCtx)
require.Equal(t, "new String[2][4]", got)
}
54 changes: 54 additions & 0 deletions javajive_tests/decompiler/core/values/types/generic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package types_test

import (
"testing"

"github.com/yaklang/javajive/classparser/decompiler/core/class_context"
types "github.com/yaklang/javajive/classparser/decompiler/core/values/types"
)

func TestParseSignature(t *testing.T) {
ctx := &class_context.ClassContext{}
tests := []struct {
sig string
want string
}{
{"Ljava/util/function/BiFunction<Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;>;", "BiFunction<Integer, Integer, Integer>"},
{"Ljava/util/List<Ljava/lang/String;>;", "List<String>"},
{"Ljava/util/Map<Ljava/lang/String;Ljava/lang/Integer;>;", "Map<String, Integer>"},
{"Ljava/lang/String;", "String"},
{"Ljava/util/List;", "List"},
{"[Ljava/lang/String;", "String[]"},
}
for _, tt := range tests {
typ := types.ParseSignature(tt.sig)
if typ == nil {
t.Errorf("types.ParseSignature(%q) returned nil", tt.sig)
continue
}
got := typ.String(ctx)
if got != tt.want {
t.Errorf("types.ParseSignature(%q) = %q, want %q", tt.sig, got, tt.want)
}
}
}

func TestParseMethodSignature(t *testing.T) {
ctx := &class_context.ClassContext{}
sig := "(Ljava/util/function/BiFunction<Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;>;)V"
params, ret := types.ParseMethodSignature(sig)
if len(params) != 1 {
t.Fatalf("expected 1 param, got %d", len(params))
}
got := params[0].String(ctx)
want := "BiFunction<Integer, Integer, Integer>"
if got != want {
t.Errorf("param type = %q, want %q", got, want)
}
if ret == nil {
t.Fatal("return type is nil")
}
if ret.String(ctx) != "void" {
t.Errorf("return type = %q, want void", ret.String(ctx))
}
}
221 changes: 221 additions & 0 deletions javajive_tests/decompiler/core/values/types/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package types_test

import (
"math/rand"
"strings"
"testing"

"github.com/yaklang/javajive/classparser/decompiler/core/class_context"
types "github.com/yaklang/javajive/classparser/decompiler/core/values/types"
)

// TestSlashToDot verifies the fast '/'->'.' conversion against a table of edge cases.
func TestSlashToDot(t *testing.T) {
cases := []struct {
in string
want string
}{
{"", ""},
{"X", "X"},
{"/", "."},
{"a/b", "a.b"},
{"java/lang/String", "java.lang.String"},
{"no_slash_here", "no_slash_here"},
{"/leading", ".leading"},
{"trailing/", "trailing."},
{"a//b", "a..b"},
{"com/hazelcast/Foo$Bar", "com.hazelcast.Foo$Bar"},
}
for _, c := range cases {
if got := types.SlashToDot(c.in); got != c.want {
t.Errorf("types.SlashToDot(%q) = %q, want %q", c.in, got, c.want)
}
}
}

// TestSlashToDotEquivalence is a property test: types.SlashToDot must be byte-identical to the
// strings.Replace(s, "/", ".", -1) it replaces, for randomized inputs.
func TestSlashToDotEquivalence(t *testing.T) {
r := rand.New(rand.NewSource(1))
alphabet := []byte("ab/.$_/0/")
for i := 0; i < 5000; i++ {
n := r.Intn(40)
b := make([]byte, n)
for j := range b {
b[j] = alphabet[r.Intn(len(alphabet))]
}
s := string(b)
want := strings.Replace(s, "/", ".", -1)
if got := types.SlashToDot(s); got != want {
t.Fatalf("mismatch for %q: types.SlashToDot=%q strings.Replace=%q", s, got, want)
}
}
}

// TestSlashToDotNoAllocWhenClean documents the fast path: an input without '/' must be
// returned without copying (same backing string header), so the common case is free.
func TestSlashToDotNoAllocWhenClean(t *testing.T) {
s := "already.dotted.name"
got := types.SlashToDot(s)
if got != s {
t.Fatalf("types.SlashToDot(%q) = %q", s, got)
}
allocs := testing.AllocsPerRun(100, func() {
_ = types.SlashToDot("java.lang.String")
})
if allocs != 0 {
t.Errorf("types.SlashToDot on a slash-free string allocated %.0f times, want 0", allocs)
}
}

func TestParseJavaDescriptionPrimitives(t *testing.T) {
cases := map[string]string{
"B": types.JavaByte, "C": types.JavaChar, "D": types.JavaDouble, "F": types.JavaFloat,
"I": types.JavaInteger, "J": types.JavaLong, "S": types.JavaShort, "Z": types.JavaBoolean, "V": types.JavaVoid,
}
for desc, wantName := range cases {
typ, rest, err := types.ParseJavaDescription(desc + "REST")
if err != nil {
t.Fatalf("types.ParseJavaDescription(%q) err: %v", desc, err)
}
if rest != "REST" {
t.Errorf("desc %q rest = %q, want REST", desc, rest)
}
p, ok := typ.RawType().(*types.JavaPrimer)
if !ok {
t.Fatalf("desc %q: RawType %T, want *JavaPrimer", desc, typ.RawType())
}
if p.Name != wantName {
t.Errorf("desc %q: name %q, want %q", desc, p.Name, wantName)
}
}
}

// TestParseJavaDescriptionClass verifies L...; parsing produces a dotted class name and
// consumes exactly through the ';'.
func TestParseJavaDescriptionClass(t *testing.T) {
typ, rest, err := types.ParseJavaDescription("Ljava/lang/String;Lnext/Type;")
if err != nil {
t.Fatal(err)
}
if rest != "Lnext/Type;" {
t.Errorf("rest = %q, want Lnext/Type;", rest)
}
jc, ok := typ.RawType().(*types.JavaClass)
if !ok {
t.Fatalf("RawType %T, want *JavaClass", typ.RawType())
}
if jc.Name != "java.lang.String" {
t.Errorf("name %q, want java.lang.String", jc.Name)
}
}

// TestParseJavaDescriptionArray checks single and multi-dimensional arrays.
func TestParseJavaDescriptionArray(t *testing.T) {
t1, _, err := types.ParseJavaDescription("[I")
if err != nil {
t.Fatal(err)
}
if !t1.IsArray() || t1.ArrayDim() != 1 {
t.Errorf("[I: IsArray=%v dim=%d, want true/1", t1.IsArray(), t1.ArrayDim())
}
t2, _, err := types.ParseJavaDescription("[[Ljava/lang/String;")
if err != nil {
t.Fatal(err)
}
if !t2.IsArray() || t2.ArrayDim() != 2 {
t.Errorf("[[L...: IsArray=%v dim=%d, want true/2", t2.IsArray(), t2.ArrayDim())
}
t3, _, err := types.ParseJavaDescription("[[[J")
if err != nil {
t.Fatal(err)
}
if !t3.IsArray() || t3.ArrayDim() != 3 {
t.Errorf("[[[J: IsArray=%v dim=%d, want true/3", t3.IsArray(), t3.ArrayDim())
}
if got := t3.String(&class_context.ClassContext{}); got != "long[][][]" {
t.Errorf("[[[J string = %q, want long[][][]", got)
}
if got := t3.ElementType().String(&class_context.ClassContext{}); got != "long[][]" {
t.Errorf("[[[J element string = %q, want long[][]", got)
}
}

// TestParseMethodDescriptor checks param counts and return types for representative method
// descriptors -- the hot path that previously re-ran strings.Replace per occurrence.
func TestParseMethodDescriptor(t *testing.T) {
cases := []struct {
desc string
paramCount int
retName string // JavaPrimer name when applicable
}{
{"()V", 0, types.JavaVoid},
{"(II)I", 2, types.JavaInteger},
{"(Ljava/lang/String;)Z", 1, types.JavaBoolean},
{"(Ljava/lang/String;[IJ)V", 3, types.JavaVoid},
}
for _, c := range cases {
typ, err := types.ParseMethodDescriptor(c.desc)
if err != nil {
t.Fatalf("types.ParseMethodDescriptor(%q) err: %v", c.desc, err)
}
ft := typ.FunctionType()
if ft == nil {
t.Fatalf("desc %q: FunctionType nil", c.desc)
}
if len(ft.ParamTypes) != c.paramCount {
t.Errorf("desc %q: params=%d, want %d", c.desc, len(ft.ParamTypes), c.paramCount)
}
if p, ok := ft.ReturnType.RawType().(*types.JavaPrimer); ok {
if p.Name != c.retName {
t.Errorf("desc %q: ret %q, want %q", c.desc, p.Name, c.retName)
}
}
}
}

// TestParseMethodDescriptorInternsClasses proves the optimization end-to-end: parsing two
// method descriptors that both reference java/lang/String reuses the same interned leaf.
func TestParseMethodDescriptorInternsClasses(t *testing.T) {
a, err := types.ParseMethodDescriptor("(Ljava/lang/String;)V")
if err != nil {
t.Fatal(err)
}
b, err := types.ParseMethodDescriptor("(ILjava/lang/String;)I")
if err != nil {
t.Fatal(err)
}
pa := a.FunctionType().ParamTypes[0].RawType()
pb := b.FunctionType().ParamTypes[1].RawType()
if pa != pb {
t.Fatalf("expected the java/lang/String leaf to be interned and shared across descriptors")
}
}

// BenchmarkSlashToDot vs BenchmarkStringsReplaceSlash is the algorithm comparison for the
// '/'->'.' conversion on a typical class name.
func BenchmarkSlashToDot(b *testing.B) {
s := "com/hazelcast/client/impl/protocol/DefaultMessageTaskFactoryProvider"
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = types.SlashToDot(s)
}
}

func BenchmarkStringsReplaceSlash(b *testing.B) {
s := "com/hazelcast/client/impl/protocol/DefaultMessageTaskFactoryProvider"
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = strings.Replace(s, "/", ".", -1)
}
}

// BenchmarkParseMethodDescriptorCached vs Uncached shows the flyweight effect on the hot
// descriptor path (same descriptor parsed repeatedly, as in a real constant pool).
func BenchmarkParseMethodDescriptor(b *testing.B) {
desc := "(Ljava/lang/String;Ljava/util/Map;I[Ljava/lang/Object;)Ljava/lang/String;"
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = types.ParseMethodDescriptor(desc)
}
}
Loading
Loading