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
54 changes: 54 additions & 0 deletions classparser/catch_merge_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package javaclassparser

import (
"os"
"strings"
"testing"
)

// TestCatchMergeWrappingRethrowIsLoadBearing pins the mergeNestedSameTypeCatches fix for a
// wrapping rethrow (`throw new RuntimeException(t)` instead of bare `throw t`). When the
// bytecode emits two catch(Throwable) handlers (the primary catch with a wrapping rethrow
// and the cleanup/finally handler), the merge must PRESERVE the wrapping throw (whose method
// declares `throws Throwable`, covering wildcard-capture checked exceptions) and insert the
// cleanup code BEFORE it. Without the fix, the merge drops the wrapping throw and replaces
// it with a bare `throw t`, causing "unreported exception CAP#1" when the caught exception
// is a wildcard-capture type. Kill-switch: JDEC_NO_CATCH_MERGE. Real hit: commons-lang3
// LockingVisitors.LockVisitor.lockAcceptUnlock.
func TestCatchMergeWrappingRethrowIsLoadBearing(t *testing.T) {
data, err := os.ReadFile("testdata/regression/CatchMergeSeed.class")
if err != nil {
t.Fatalf("read seed: %v", err)
}

os.Unsetenv("JDEC_NO_CATCH_MERGE")
on, err := Decompile(data)
if err != nil {
t.Fatalf("decompile (fix ON) failed: %v", err)
}
// The merge should produce a single catch with the cleanup + wrapping throw.
catchCount := strings.Count(on, "}catch(Throwable")
if catchCount != 1 {
t.Errorf("fix ON: expected 1 catch clause (merged), got %d:\n%s", catchCount, on)
}
// The cleanup code (unlock) should be inside the catch, before the throw.
if !strings.Contains(on, "var1.unlock()") {
t.Errorf("fix ON: expected unlock() in merged catch, got:\n%s", on)
}
// The wrapping throw should be preserved (not replaced with bare `throw var2`).
if !strings.Contains(on, "throw new RuntimeException(var2)") {
t.Errorf("fix ON: expected wrapping RuntimeException throw, got:\n%s", on)
}

os.Setenv("JDEC_NO_CATCH_MERGE", "1")
defer os.Unsetenv("JDEC_NO_CATCH_MERGE")
off, err := Decompile(data)
if err != nil {
t.Fatalf("decompile (fix OFF) failed: %v", err)
}
// Without the merge, there should be 2 catch clauses (unmerged).
catchCountOff := strings.Count(off, "}catch(Throwable")
if catchCountOff != 2 {
t.Errorf("fix OFF: expected 2 catch clauses (unmerged), got %d:\n%s", catchCountOff, off)
}
}
39 changes: 39 additions & 0 deletions classparser/class_typevar_param_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package javaclassparser

import (
"os"
"strings"
"testing"
)

// TestClassTypeVarParamCastIsLoadBearing pins the sameClassMethodParamType Class<L> parameterized
// formal recovery + resolvedParameterizedArgCast same-erasure Class<L> cast. A same-class instance
// method whose generic Signature formal is `Class<L>` (L a class-scope type variable), fed a raw
// `Class` argument (from getComponentType's erased descriptor return), needs an unchecked
// `(Class<L>)` cast that the bytecode erases. The fix recovers the parameterized formal and
// re-emits the cast via resolvedParameterizedArgCast; the kill-switch JDEC_PARAM_ARG_CAST_OFF
// drops it. Real hit: commons-lang3 EventListenerSupport.readObject.
func TestClassTypeVarParamCastIsLoadBearing(t *testing.T) {
data, err := os.ReadFile("testdata/regression/ClassTypeVarParamSeed.class")
if err != nil {
t.Fatalf("read seed: %v", err)
}

os.Unsetenv("JDEC_PARAM_ARG_CAST_OFF")
on, err := Decompile(data)
if err != nil {
t.Fatalf("decompile (fix ON) failed: %v", err)
}
if !strings.Contains(on, "(Class<L>)") {
t.Errorf("fix ON: expected (Class<L>) cast, got:\n%s", on)
}

t.Setenv("JDEC_PARAM_ARG_CAST_OFF", "1")
off, err := Decompile(data)
if err != nil {
t.Fatalf("decompile (fix OFF) failed: %v", err)
}
if strings.Contains(off, "(Class<L>)") {
t.Errorf("fix OFF: expected no (Class<L>) cast, got:\n%s", off)
}
}
89 changes: 75 additions & 14 deletions classparser/decompiler/core/values/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,8 @@ func (f *FunctionCallExpression) instantiatedParamType(i int, funcCtx *class_con
// compile) and never a concrete type (a real mismatch must not be blanket-cast). Kill-switch
// JDEC_GENERIC_SELFMETHOD_PARAM_OFF.
func (f *FunctionCallExpression) sameClassMethodParamType(i int, funcCtx *class_context.ClassContext) types.JavaType {
if f.FunctionName == "initializeTransientFields" {
}
if os.Getenv("JDEC_GENERIC_SELFMETHOD_PARAM_OFF") != "" || f.IsStatic || f.Object == nil || funcCtx == nil {
return nil
}
Expand All @@ -796,43 +798,86 @@ func (f *FunctionCallExpression) sameClassMethodParamType(i int, funcCtx *class_
// `this.updateInverseMap(k, b, objVal, v)` where param 3 is V -> needs `(V) objVal`). Focused
// sub-switch JDEC_GENERIC_SELFMETHOD_PRIVATE_OFF restores the legacy blanket-skip of invokespecial.
if f.IsSpecialInvoke {
if os.Getenv("JDEC_GENERIC_SELFMETHOD_PRIVATE_OFF") != "" || !f.isCurrentClass(funcCtx) {
// funcCtx.ClassName may be empty in some method rendering contexts; fall back to checking
// whether the call's target class matches the class whose type params are in funcCtx
// (ClassTypeParams), which is authoritative for the current class's type variables.
isCurrent := f.isCurrentClass(funcCtx)
if !isCurrent && funcCtx.ClassName == "" && len(funcCtx.ClassTypeParams) > 0 {
// Heuristic: if funcCtx has class type params but no ClassName, the call's target class
// IS the current class (ClassName wasn't set in this rendering context). Trust it.
isCurrent = true
}
if os.Getenv("JDEC_GENERIC_SELFMETHOD_PRIVATE_OFF") != "" || !isCurrent {
return nil
}
}
ref, ok := UnpackSoltValue(f.Object).(*JavaRef)
if f.FunctionName == "initializeTransientFields" {
}
if !ok || !ref.IsThis {
return nil
}
sig := funcCtx.MethodSignature(f.FunctionName, len(f.Arguments))
if f.FunctionName == "initializeTransientFields" {
}
if sig == "" {
// The arity path abandons same-arity overloads as ambiguous; fall back to the call's EXACT
// descriptor, which is unique in the JVM, to still recover the erased argument cast (guava
// Builder `putAll(K, Iterable)` vs varargs `putAll(K, V...)`; `add(E)` vs `add(E...)`). Empty
// when the descriptor-keyed table is disabled (JDEC_SAMECLASS_DESC_SIG_OFF) or the call has no
// pool descriptor, so this stays a pure additive fallback.
sig = funcCtx.MethodSignatureByDesc(f.FunctionName, f.Descriptor)
if f.FunctionName == "initializeTransientFields" {
}
}
if sig == "" {
// Fallback: the method has no Signature attribute (non-generic method), but its descriptor
// formal is raw `java.lang.Class` and the declaring class has exactly one type variable.
// The decompiler renders the formal as `Class<L>` (from the class Signature), so a raw `Class`
// argument (e.g. from getComponentType()'s erased descriptor return) cannot convert to
// `Class<L>` without an unchecked cast ("Class<CAP#1> cannot be converted to Class<L>";
// commons-lang3 EventListenerSupport.readObject `this.initializeTransientFields(
// var2.getClass().getComponentType(), ...)` where the formal is `Class<L>`). Construct
// `Class<L>` from the class Signature's single type variable so the arg-cast logic re-emits
// the source's `(Class<L>)` cast. Kill-switch: JDEC_CLASS_TYPEVAR_PARAM_OFF.
if os.Getenv("JDEC_CLASS_TYPEVAR_PARAM_OFF") == "" && f.FuncType != nil && i >= 0 && i < len(f.FuncType.ParamTypes) {
pt := f.FuncType.ParamTypes[i]
if pt != nil {
ptStr := pt.String(funcCtx)
if f.FunctionName == "initializeTransientFields" {
}
if strings.HasSuffix(ptStr, "Class") && !strings.Contains(ptStr, "[") && !strings.Contains(ptStr, "<") {
formals := types.ClassFormalTypeParamNames(funcCtx.ClassSig)
if len(formals) != 1 && len(funcCtx.ClassTypeParams) == 1 {
formals = funcCtx.ClassTypeParams
}
if len(formals) == 1 {
return types.NewParameterizedType("java.lang.Class", []types.JavaType{
types.NewJavaClass(formals[0]),
})
}
}
}
}
return nil
}
_, params, _ := types.ParseMethodSignatureFull(sig, funcCtx)
if i < 0 || i >= len(params) || params[i] == nil {
return nil
}
// A bare class-scope type variable formal (e.g. `T`) is denotable AND castable.
raw := params[i].RawType()
if raw == nil {
return nil
}
jc, ok := raw.(*types.JavaClass)
if !ok {
return nil
if jc, ok := raw.(*types.JavaClass); ok && funcCtx.IsTypeParam(jc.Name) {
return params[i]
}
// Only a class-scope type variable is denotable AND castable at the call site.
if !funcCtx.IsTypeParam(jc.Name) {
return nil
// A parameterized formal whose raw class is java.lang.Class and whose single type argument is a
// class-scope type variable (e.g. `Class<L>`) is also denotable — a raw `Class` argument needs an
// unchecked `(Class<L>)` cast (commons-lang3 EventListenerSupport.readObject). Return it so
// resolvedParameterizedArgCast handles the same-erasure cast.
if pt, ok := raw.(*types.JavaParameterizedType); ok && pt.RawClassName == "java.lang.Class" && len(pt.TypeArgs) == 1 {
if tvJC, isJC := pt.TypeArgs[0].RawType().(*types.JavaClass); isJC && funcCtx.IsTypeParam(tvJC.Name) {
return params[i]
}
}
return params[i]
return nil
}

// ctorWildcardArgCast returns the parameterized formal type to cast the i-th argument of a SAME-CLASS
Expand Down Expand Up @@ -1861,6 +1906,18 @@ func resolvedParameterizedArgCast(funcCtx *class_context.ClassContext, argType t
return false // array / primitive / parameterized argument: not this case.
}
if ajc.Name == pt.RawClassName {
// Same erasure, but a raw `Class` argument fed to a `Class<L>` formal (L a class-scope type
// variable) cannot convert without an unchecked cast ("Class<CAP#1> cannot be converted to
// Class<L>"; commons-lang3 EventListenerSupport.readObject). The source carried an unchecked
// `(Class<L>)` cast (erased to a no-op checkcast on raw Class); re-emit it. `Class` ->
// `Class<L>` is always a legal unchecked conversion. Gated on the formal being a single
// type-variable parameterization of java.lang.Class whose type argument is an in-scope CLASS
// type variable, and the argument being raw java.lang.Class (not already parameterized).
if pt.RawClassName == "java.lang.Class" && len(pt.TypeArgs) == 1 {
if tvJC, isJC := pt.TypeArgs[0].RawType().(*types.JavaClass); isJC && funcCtx.IsTypeParam(tvJC.Name) {
return true
}
}
return false // same erasure -- no cast needed (already a Cut / raw Cut).
}
if funcCtx.IsTypeParam(ajc.Name) {
Expand Down Expand Up @@ -2192,7 +2249,7 @@ func (f *FunctionCallExpression) lambdaArgRawJDKReceiverCast(i int, funcCtx *cla
}
// A method reference binds NATURALLY to the raw SAM (it has no explicit parameter types), so
// the parameterized-FI cast is unnecessary for it AND, for SAMs with nested wildcards (Stream.
// flatMap's `Function<? super T, ? extends Stream<? extends R>>`), the cast pins a concrete
// flatMap's `Function<? super T,? extends Stream<? extends R>>`), the cast pins a concrete
// parameterization that defeats javac poly inference ("method flatMap cannot be applied").
// Only an explicitly-typed lambda body needs the cast to bind. Skip method references.
// (fastjson2 ObjectReaderCreator.toFieldReaderArray `flatMap(Collection::stream)`.)
Expand Down Expand Up @@ -2407,6 +2464,10 @@ var rawFIMethodRefCastFamily = map[string]bool{
// out of ArgumentStrings so the varargs-spread path can reuse it for the leading fixed arguments.
func (f *FunctionCallExpression) renderArgAt(i int, funcCtx *class_context.ClassContext) string {
arg := f.Arguments[i]
if f.FunctionName == "initializeTransientFields" {
}
if f.FunctionName == "initializeTransientFields" && i == 0 {
}
// A METHOD REFERENCE passed to a constructor whose i-th formal is a RAW functional interface
// (raw BiConsumer.accept(Object,Object) etc.) fails to bind ("invalid method reference"): the
// impl method's arity (e.g. Throwable.setStackTrace(StackTraceElement[]) is 1-arg as an UNBOUND
Expand Down
53 changes: 43 additions & 10 deletions classparser/decompiler/core/values/types/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -603,28 +603,52 @@ func primerForSig(c byte) string {
}

func ParseMethodSignature(sig string) ([]JavaType, JavaType) {
params, ret, _ := ParseMethodSignatureWithThrows(sig)
return params, ret
}

// ParseMethodSignatureWithThrows is the same as ParseMethodSignature but also parses any
// `^`-prefixed throws types that follow the return type in a method Signature attribute
// (JVM Signature grammar: `(ParamsType*)ReturnType ^ThrowsType*`). Each throws type is a
// TypeVariableSignature (e.g. `TT;` for `throws T`) or a ClassTypeSignature (e.g.
// `Ljava/io/IOException;`). The throws types are returned as the 3rd return value; nil when
// the signature has no throws clause or parsing fails. ParseMethodSignature delegates here
// and discards the throws types, so existing callers are unaffected.
func ParseMethodSignatureWithThrows(sig string) ([]JavaType, JavaType, []JavaType) {
if len(sig) == 0 || sig[0] != '(' {
return nil, nil
return nil, nil, nil
}
rest := sig[1:]
var params []JavaType
for len(rest) > 0 && rest[0] != ')' {
t, remaining, ok := parseSigType(rest)
if !ok {
return nil, nil
return nil, nil, nil
}
params = append(params, t)
rest = remaining
}
if len(rest) == 0 || rest[0] != ')' {
return nil, nil
return nil, nil, nil
}
rest = rest[1:]
retType, _, ok := parseSigType(rest)
retType, afterRet, ok := parseSigType(rest)
if !ok {
return nil, nil
return nil, nil, nil
}
return params, retType
// Parse throws types: each is introduced by '^' in the Signature grammar.
rest = afterRet
var throws []JavaType
for len(rest) > 0 && rest[0] == '^' {
rest = rest[1:]
t, remaining, ok := parseSigType(rest)
if !ok {
break
}
throws = append(throws, t)
rest = remaining
}
return params, retType, throws
}

// ParseClassSignature extracts the type parameters declaration from a class
Expand Down Expand Up @@ -1197,21 +1221,30 @@ func parseFormalTypeParams(sig string, funcCtx *class_context.ClassContext) stri
// exactly ParseMethodSignature, so non-generic methods are unaffected. Bound types in the type-param
// string are rendered with funcCtx so other-package bounds register an import.
func ParseMethodSignatureFull(sig string, funcCtx *class_context.ClassContext) (string, []JavaType, JavaType) {
tps, params, ret, _ := ParseMethodSignatureFullWithThrows(sig, funcCtx)
return tps, params, ret
}

// ParseMethodSignatureFullWithThrows is the same as ParseMethodSignatureFull but also returns
// the throws types from the Signature attribute's `^`-prefixed throws clause. See
// ParseMethodSignatureWithThrows for the throws grammar. ParseMethodSignatureFull delegates
// here and discards the throws types.
func ParseMethodSignatureFullWithThrows(sig string, funcCtx *class_context.ClassContext) (string, []JavaType, JavaType, []JavaType) {
typeParams := ""
rest := sig
if len(rest) > 0 && rest[0] == '<' {
typeParams = parseFormalTypeParams(rest, funcCtx)
r, ok := skipAngleSection(rest)
if !ok {
return "", nil, nil
return "", nil, nil, nil
}
rest = r
}
params, ret := ParseMethodSignature(rest)
params, ret, throws := ParseMethodSignatureWithThrows(rest)
if ret == nil {
return "", nil, nil
return "", nil, nil, nil
}
return typeParams, params, ret
return typeParams, params, ret, throws
}

// FormalTypeParamBounds parses the leading formal type-parameter section of a class or method Signature
Expand Down
Loading
Loading