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: 20 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,26 @@
> iso 口径的 `cannot find symbol`/`private access` 大多是扁平 `$` 假阳性, 不在此列(见 CODEC_TODO §3)。
>
> 数字快照(javac 21, 本机 `~/.m2` 含可选依赖; tree errLines / 缺陷类, 复跑见下方命令):
> codec 0/0 ✅ · gson 0/0 ✅ · jsoup 1/1 · snakeyaml 1/1 · fastjson2 25/14 · guava 27/23 · commons-lang3 12/9 · spring 31/19。(合计 97)
> (本轮修: 方法形参自由类型变量注入(dumper.go 扁平内部类 enclosing-arity 注入块增方法形参扫描,
> codec 0/0 ✅ · gson 0/0 ✅ · jsoup 1/1 · snakeyaml 1/1 · fastjson2 25/14 · guava 27/23 · commons-lang3 11/8 · spring 31/19。(合计 96)
> (本轮修: `AtomicReference<V>` 的 V 形参方法实参补造型(`jdkMethodParamTypeArgIndex` 增 AtomicReference 分支, `JDEC_ATOMIC_REF_PARAM_OFF`)—— 字段 `AtomicReference<T> reference` 的 `get()` 被读进 Object 局部,
> 再回传给 `compareAndSet(V,V)`/`getAndSet(V)`/`set(V)`(descriptor 擦成 `compareAndSet/set(Object,Object)`)时,
> 裸 Object 实参被 javac 按 `AtomicReference<V>.compareAndSet(V,V)` 定型判「Object cannot be converted to T」。
> 修法: 仿 `jdkMapFamily`/`jdkListFamily` 形参映射族, 在 JDK 形参类型实参索引表补 `AtomicReference` 分支(V 是唯一类型实参,
> 全部 V 形参位映射到 0): `compareAndSet/weakCompareAndSet(V,V)` argc==2 两形参、`getAndSet/set/lazySet(V)` argc==1 形参 0、
> `getAndAccumulate/accumulateAndGet(V, BinaryOperator<V>)` argc==2 仅形参 0(V), 形参 1 是 operator 不动。
> `instantiatedParamType` 把 V 形参解析成接收者 T 后, 既有 arg-cast 路径重下 `(T)` unchecked 造型(字节码里该值已流入 V 槽, 保义)。
> 限 `ntype==1`(裸/非通配参数化 `AtomicReference<V>`), 既有通配符早退(`AtomicReference<?>`)不受影响。承重测试
> `atomic_ref_vparam_test.go`(AtomicRefVParamSeed) + A/B 交叉 `atomic_ref_vparam_cast_test.go`(commons-lang3 AtomicInitializer)。
> 治 commons-lang3 AtomicInitializer 1 条, commons-lang3 12→11、缺陷类 9→8, 合计 97→96, 零回归。)
> (本轮调查 jsoup/snakeyaml 清零未果, 记录潜伏链供后续: ① jsoup `XmlTreeBuilder.insert(Token$Comment)` 的「`Comment var3 = var2; ...;
> var3 = new XmlDeclaration();` 首声明窄化致兄弟再赋失败(`XmlDeclaration cannot be converted to Comment`)」**单点可修**——
> 跨类兄弟臂合并已把 slot ref 扩宽到 LUB(Node), 但首声明 RHS 仍是窄臂类型 Comment; 修法: 在 AssignStatement 首声明处见 ref 被合并 helper
> 扩宽(经 `PhiWidened` 标记证)时采纳 slot 类型。**但**: 该修解锁 jsoup `QueryParser.combinator` **潜伏 7 条** definite-assignment
> (var5_1/var5_2 单分支赋值后无条件用, 属 T4b computeIfAbsent 惯用式, 之前被 XmlTreeBuilder 类型错遮蔽)——原 1 错变 7 错, 净 -6。
> ② snakeyaml `SafeConstructor.createNumber` 即 **T4b 文档化的 special-project**(try/catch 合流 `var6` 只在 catch 赋值,
> `return var6` 未初始化)—— 核心数据流 + 全量 A/B, 留专项。结论: 两单点清零均被 T4b 遮蔽依赖阻塞, 须先解 T4b(单用折叠前判 slot 下游 load
> 或合流 load 建 phi)才能净清零; 本轮先回退 jsoup 修保 96 合计、零回归, AtomicReference 修复保留。)
> (上一轮修: 方法形参自由类型变量注入(dumper.go 扁平内部类 enclosing-arity 注入块增方法形参扫描,
> `JDEC_INNER_METHODPARAM_TYPEVAR_INJECT_OFF`)—— 静态泛型方法内的匿名类捕获方法的类型变量, javac 发出的
> 该匿名类 Signature **不带** `<...>` 形参段(只列自由变量引用: guava `Futures$2` 的类签名是
> `Ljava/lang/Object;LFuture<TO;>;`, 无 `<O:...>` 前缀), 于是 O 从父类 `Future<O>` 被识别为自由变量并声明,
Expand Down
5 changes: 3 additions & 2 deletions classparser/CODEC_TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@
|---|---:|---:|---:|---:|---|
| **commons-codec** 1.15 | 106 | **0** | **0** | 0 | ✅ **完整往返**(107/107 verify + 调用差分逐字节一致) |
| **gson** 2.8.9 | 195 | **0** | **0** | 0 | ✅ **完整往返**(199/199 verify) |
| **commons-lang3** 3.12.0 | 345 | 11 | 18 | 0 | 泛型擦除长尾 |
| **commons-lang3** 3.12.0 | 345 | 8 | 11 | 0 | 泛型擦除长尾 |
| **jsoup** 1.10.2 | 238 | 1 | 1 | 0 | 单类长尾 |
| **snakeyaml** 2.2 | 231 | 1 | 1 | 0 | definite-assignment 单点 |
| **spring-core** 5.3.27 | 978 | 29 | 55 | 0 | 泛型擦除造型 + 三元 LUB + bool/int 槽位长尾 |
| **fastjson2** 2.0.43 | 681 | 15 | 32 | 0 | 泛型擦除 + 槽位复用长尾 |
| **guava** 28.2-android | 1892 | 20 | 28 | 0 | 泛型擦除/边界 + 扁平内部类长尾 |
| **合计** | | **77** | **135** | **0** | 类级干净率 **96.6%**(2175/2252) |
| **合计** | | **76** | **134** | **0** | 类级干净率 **96.6%**(2176/2252) |

**codec 与 gson 已证北极星全链路**(承重于 `test/cross/jar_roundtrip_test.go`):
`decompile → javac 重编译(0 error) → archive/zip 重打包 → java -Xverify:all 逐类加载校验全通过`; codec 更经调用差分(Base64 / Hex / MD5 / SHA-256)与原始 jar 逐字节一致。
Expand Down Expand Up @@ -183,6 +183,7 @@ iso 把每个扁平单元单独编译, 以下失败是方法学产物, 在 tree(
| `JDEC_BOOL_ACCUM_SLOT_SPLIT_OFF` | 布尔累加器复用不相交 int 循环计数器槽位拆分(`reachingBoolAccumulatorSlotSplit`, 与 `reachingBoolReturnSlotSplit`/`reachingBoolFieldSlotSplit` 同族): `boolean flag=false; flag|=someZcall()` 的 `iconst_0` 初值被 AssignVarGuarded 见作 int 类别、续用了停在同槽表项里的(已死)int 循环计数器 ref, 合并两不相交活跃区; 该槽随后经 `flag|=Zcall` 自累加(`ior/iand/ixor` 回存同槽, 兄弟操作数为 Z 返回调用)定型 boolean, 早循环渲染成 `boolean<int`/`array[boolean]`/`boolean++`, javac 报「bad operand types / boolean cannot be converted to int / bad '++' operand」。见证: 前向从 `iconst_0` store 找 slot 的 load 喂入自 `ior/iand/ixor` 回存同槽且兄弟操作数为 Z 返回 invoke(`slotStoreFeedsBooleanAccumulate`/`loadFeedsBooleanAccumulate`/`opcodeInvokeReturnsBoolean`); 同 disjoint-web + phi 门控; 命中把 0 转布尔 false 令 AssignVarGuarded 铸新布尔 flag, int 计数器保留自身 ref。修 spring ASM `ClassWriter.toByteArray`(spring 52→51) |
| `JDEC_ARRAY_PARAM_REF_ARG_CAST_OFF` | 数组形参收 null-Object 实参造型(`arrayParamRefArgCast`, 在 `renderArgAt`): 形参是数组类型(`byte[]`/`Object[]`)而实参静态类型是非数组引用类(通常 `java.lang.Object`, 来自 null 初始化局部)时, 类-类造型分支不触发(数组类型 `RawType()` 是 `*JavaArrayType` 非 `*JavaClass`, ok1 为假), 裸 Object 实参不可赋给数组形参, javac 报「Object cannot be converted to byte[]」。字节码里该值(null 或 checkcast)已占数组槽, 补 `(byte[])` 造型保义。紧门控: 形参为数组 + 实参为非数组引用且擦除为 Object(具体类实参传数组形参是真类型错, 不遮蔽)。修 spring ASM `Attribute.computeAttributesSize`/`putAttributes`(Object→byte[]) + cglib `Enhancer`(Object→Object[]), spring 51→48 |
| `JDEC_REF_SLOT_THROWABLE_ARM_MERGE_OFF` | try/catch 异常槽 Throwable 族上型臂合并(`reachingRefSlotThrowableArmMerge`, 与 sibling/subtype/object 臂合并同族但限 java.lang.Throwable 族): 多 catch handler 把各自捕获的异常写入同一 JVM 槽, 捕获后的读是一个逻辑 `Throwable cause` 变量(类型为各臂 LUB)。DFS 序里子型臂(InterruptedException)先铸槽变量, getCause()→Throwable 上型臂被 AssignVarGuarded 拆成独立变量, 于是 `cause instanceof X`/`(X)cause` 绑到窄型 InterruptedException 变量, javac 报「InterruptedException cannot be converted to X」。既有 subtype 合并只在 val⊂current 时保留 current, sibling 合并在 LUB==某臂时退出, 都不覆盖「val 为严格上型」的加宽方向。为 hierarchy.go 补 java.lang.Throwable 族常见异常层级边 + `IsThrowableRooted`(对 jdkSuperEdges 做 Throwable 闭包); 两臂皆 Throwable 子类、val 严格上型(CommonSuperType==val)且 phi 共载时 `ResetVarType(vt)` 把共享 ref 加宽到 LUB —— 合并后 catch 变量的用法都是 Throwable 级(instanceof/cast/getMessage/getCause/rethrow), 加宽绝不回归窄型无造型读。修 spring core codec `Decoder.decode`/`Encoder.encode`(spring 48→46) |
| `JDEC_ATOMIC_REF_PARAM_OFF` | `java.util.concurrent.atomic.AtomicReference<V>` 的 V 形参方法实参造型(`jdkMethodParamTypeArgIndex` 增 AtomicReference 分支): 字段/局部 `AtomicReference<T>` 的 `get()` 读进 Object 局部后回传给 `compareAndSet(V,V)`/`weakCompareAngeSet(V,V)`/`getAndSet(V)`/`set(V)`/`lazySet(V)`/`getAndAccumulate(V,BinaryOperator)`/`accumulateAndGet(V,BinaryOperator)`(descriptor 擦成 Object), 裸 Object 实参被 javac 按 `AtomicReference<V>.m(V)` 定型判「Object cannot be converted to T」。V 是唯一类型实参, 全部 V 形参位映射到 0(compareAndSet 两形参、单参方法的形参 0、accumulator 的形参 0; operator 形参不动)。`instantiatedParamType` 把 V 形参解析成接收者 T 后, 既有 arg-cast 路径重下 `(T)` unchecked 造型。限 `ntype==1`(裸/非通配参数化), 既有通配符早退(`AtomicReference<?>`)不受影响。修 commons-lang3 `AtomicInitializer.get`(commons-lang3 12→11、缺陷类 9→8) |

### 结构化 / pop / switch / 枚举 / 注解
| 开关 | 作用域 |
Expand Down
43 changes: 43 additions & 0 deletions classparser/atomic_ref_vparam_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package javaclassparser

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

// TestAtomicRefVParamArgCastIsLoadBearing pins the AtomicReference<V> V-parameter argument cast
// (jdkMethodParamTypeArgIndex AtomicReference branch; kill-switch JDEC_ATOMIC_REF_PARAM_OFF). A field
// `AtomicReference<T>` whose get() is read into an Object-typed local, then passed back to
// `compareAndSet(V, V)` / `getAndSet(V)` / `set(V)` (descriptor erased to Object), renders as a bare
// Object argument and javac rejects "Object cannot be converted to T" (commons-lang3
// AtomicInitializer<T> `this.reference.compareAndSet(null, var1)`). With the fix ON the call site
// re-emits the source's unchecked `(T)` cast. With the kill-switch the bare argument returns -- the
// exact recompile blocker the fix removes.
func TestAtomicRefVParamArgCastIsLoadBearing(t *testing.T) {
data, err := os.ReadFile("testdata/regression/AtomicRefVParamSeed.class")
if err != nil {
t.Fatalf("read seed: %v", err)
}

os.Unsetenv("JDEC_ATOMIC_REF_PARAM_OFF")
on, err := Decompile(data)
if err != nil {
t.Fatalf("decompile (fix ON) failed: %v", err)
}
// The V-typed argument (var2) must carry a `(T)` cast at the compareAndSet call site.
if !strings.Contains(on, "this.reference.compareAndSet((T)(null),(T)(var2))") {
t.Errorf("fix ON: expected `(T)` casts on the AtomicReference V-args, got:\n%s", on)
}

t.Setenv("JDEC_ATOMIC_REF_PARAM_OFF", "1")
off, err := Decompile(data)
if err != nil {
t.Fatalf("decompile (fix OFF) failed: %v", err)
}
// With the kill-switch the bare uncast argument returns -- reproducing the recompile blocker.
if !strings.Contains(off, "this.reference.compareAndSet(null,var2)") ||
strings.Contains(off, "compareAndSet((T)") {
t.Errorf("fix OFF: expected the bare uncast argument (kill-switch not load-bearing), got:\n%s", off)
}
}
30 changes: 30 additions & 0 deletions classparser/decompiler/core/values/types/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,36 @@ func jdkMethodParamTypeArgIndex(rawClass, method string, argc, paramIndex, ntype
if (method == "set" || method == "add") && argc == 2 && ntype == 1 && paramIndex == 1 && jdkListFamily[rawClass] && os.Getenv("JDEC_LIST_SET_PARAM_OFF") == "" {
return 0
}
// AtomicReference<V>: the V-typed value-parameter methods whose descriptor erases V to Object. The
// canonical case is `compareAndSet(V, V)` / `weakCompareAndSet(V, V)` (argc 2, both params V) and
// `getAndSet(V)` / `set(V)` / `lazySet(V)` (argc 1, param 0 V); the accumulator pair
// `getAndAccumulate(V, BinaryOperator<V>)` / `accumulateAndGet(V, BinaryOperator<V>)` carry V only at
// param 0 (param 1 is the operator). An Object-typed argument (a `reference.get()` read typed Object)
// flows in without the source's `(V)` cast and javac -- re-resolving against the generic signature --
// rejects it ("Object cannot be converted to T"; commons-lang3 AtomicInitializer<T>
// `this.reference.compareAndSet(null, var1)` where reference is `AtomicReference<T>`). V is the sole
// type arg, so all qualifying params map to 0. Restricted to ntype==1 (a bare or
// non-wildcard-parameterized AtomicReference<V>); the existing wildcard gate in
// InstantiateJDKMethodParam already returns nil for `AtomicReference<?>`. Kill-switch
// JDEC_ATOMIC_REF_PARAM_OFF.
if rawClass == "java.util.concurrent.atomic.AtomicReference" && ntype == 1 &&
os.Getenv("JDEC_ATOMIC_REF_PARAM_OFF") == "" {
switch method {
case "compareAndSet", "weakCompareAndSet", "weakCompareAndSetPlain":
if argc == 2 && (paramIndex == 0 || paramIndex == 1) {
return 0
}
case "getAndSet", "set", "lazySet":
if argc == 1 && paramIndex == 0 {
return 0
}
case "getAndAccumulate", "accumulateAndGet":
// (V, BinaryOperator<V>): only the leading V param is the receiver's type arg.
if argc == 2 && paramIndex == 0 {
return 0
}
}
}
return -1
}

Expand Down
Binary file not shown.
24 changes: 24 additions & 0 deletions classparser/testdata/regression/AtomicRefVParamSeed.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Regression seed for the AtomicReference<V> V-parameter argument cast
// (jdkMethodParamTypeArgIndex AtomicReference branch; kill-switch JDEC_ATOMIC_REF_PARAM_OFF).
//
// A field `AtomicReference<T> reference` whose `get()` is read into an Object-typed local, then
// passed back to `compareAndSet(V, V)` / `getAndSet(V)` / `set(V)` (descriptor erased to
// compareAndSet/set(Object,Object)/set(Object)), renders as a bare Object argument and javac rejects
// "Object cannot be converted to T" (commons-lang3 AtomicInitializer<T>
// `this.reference.compareAndSet(null, var1)`). The argument already flowed into the V slot in
// bytecode, so re-emitting the source's unchecked `(T)` cast is behaviour-preserving.
public class AtomicRefVParamSeed<T> {
private final java.util.concurrent.atomic.AtomicReference<T> reference =
new java.util.concurrent.atomic.AtomicReference<>();

public T get(java.util.function.Supplier<T> init) {
Object var1 = this.reference.get();
if (var1 == null) {
var1 = init.get();
if (!this.reference.compareAndSet(null, (T) var1)) {
var1 = this.reference.get();
}
}
return (T) var1;
}
}
110 changes: 110 additions & 0 deletions test/cross/atomic_ref_vparam_cast_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package cross

// 承重测试:「AtomicReference<V> 的 V 形参方法(compareAndSet/getAndSet/set...)缺 (V) 实参造型」治本
// (CODEC_TODO 泛型擦除·JDK 方法形参造型族)。
//
// 真实 commons-lang3 AtomicInitializer<T>.get():
// Object var1 = this.reference.get(); // reference 是 AtomicReference<T>, get() 返 Object
// if (var1 == null) {
// var1 = this.initialize();
// if (!(this.reference.compareAndSet(null, var1))) { // 源码带 (T) 造型
// var1 = this.reference.get();
// }
// }
// return (T) var1;
// compareAndSet 的形参在字节码里被擦成 Object, 反编译不补造型; javac 却按
// AtomicReference<V>.compareAndSet(V, V) 定型判 "Object cannot be converted to T"。治本
// (jdkMethodParamTypeArgIndex 增 AtomicReference 分支, 使 instantiatedParamType 把
// compareAndSet 形参 0/1 解析为接收者 V, 既有 arg-cast 路径遂重下 (T) 造型)。
// JDEC_ATOMIC_REF_PARAM_OFF=1 关掉治本必复现。

import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

classparser "github.com/yaklang/javajive/classparser"
)

// atomicRefVParamErrCount decompiles AtomicInitializer under the kill-switch, recompiles it against
// the jar, and counts the AtomicReference V-parameter cast defect: the Object-typed argument to
// `reference.compareAndSet(...)` passed without the `(T)` cast, so javac rejects "Object cannot be
// converted to T".
func atomicRefVParamErrCount(t *testing.T, jarPath string, killOff bool) int {
t.Helper()
const sw = "JDEC_ATOMIC_REF_PARAM_OFF"
const entry = "org/apache/commons/lang3/concurrent/AtomicInitializer.class"
prev, had := os.LookupEnv(sw)
if killOff {
os.Setenv(sw, "1")
} else {
os.Unsetenv(sw)
}
defer func() {
if had {
os.Setenv(sw, prev)
} else {
os.Unsetenv(sw)
}
}()

jfs, err := classparser.NewJarFSFromLocal(jarPath)
if err != nil {
t.Fatalf("open jar: %v", err)
}
src, err := jfs.ReadFile(entry)
if err != nil {
t.Fatalf("read %s: %v", entry, err)
}
dir := t.TempDir()
dst := filepath.Join(dir, "AtomicInitializer.java")
if err := os.WriteFile(dst, src, 0o644); err != nil {
t.Fatalf("write %s: %v", dst, err)
}

javac := lookJavac(t)
args := append(append([]string{}, javacLocaleArgs...),
"-encoding", "UTF-8", "--release", "8", "-nowarn", "-Xmaxerrs", "100000",
"-cp", jarPath, "-d", t.TempDir(), dst)
out, _ := exec.Command(javac, args...).CombinedOutput()

n := 0
for _, line := range strings.Split(string(out), "\n") {
if !strings.Contains(line, ": error:") || !strings.Contains(line, "AtomicInitializer.java") {
continue
}
if strings.Contains(line, "Object cannot be converted to T") {
n++
}
}
return n
}

// TestAtomicRefVParamCastIsLoadBearing pins commons-lang3 AtomicInitializer.get: an Object-typed
// argument to `reference.compareAndSet(...)` (receiver recovered as AtomicReference<T>) must be wrapped
// in a `(T)` cast, compareAndSet's value parameter resolved to the receiver's V via the AtomicReference
// parameter table. Disabling the fix via the kill-switch must reintroduce the "Object cannot be converted
// to T" error.
func TestAtomicRefVParamCastIsLoadBearing(t *testing.T) {
lookJavac(t)
jarPath := resolveJar(jarSpecs["commons-lang3"].relPath)
if jarPath == "" {
t.Skip("commons-lang3 jar not found under ~/.m2; skipping")
}

on := atomicRefVParamErrCount(t, jarPath, false) // fix ON
off := atomicRefVParamErrCount(t, jarPath, true) // fix OFF (kill-switch)
t.Logf("AtomicInitializer AtomicReference V-param errors: ON=%d OFF=%d", on, off)

if off == 0 {
t.Fatalf("kill-switch did not reproduce the defect: OFF=%d (expected > 0)", off)
}
if on >= off {
t.Errorf("fix is NOT load-bearing: ON=%d OFF=%d (ON must be strictly fewer)", on, off)
}
if on != 0 {
t.Errorf("fix did not clear the AtomicReference V-param error: ON=%d (want 0)", on)
}
}
Loading