Skip to main content

ddc_dec/
classdec.rs

1//! Class-level rendering: header, fields (with static initializers),
2//! constructors, methods and nested member classes.
3
4use jdc_core::emit::{escape_string, format_float, Printer};
5use jdc_core::types::JavaType;
6use jdc_core::Ctx;
7
8use crate::access::*;
9use crate::PoolField;
10use crate::ctx::{java_type_to_generic, DexCtx};
11use crate::method::decompile_method;
12use crate::{desc_type, DexPool, PoolClass, PoolMethod, StaticValue};
13use ddc_dex::insn::InsnKind;
14
15#[derive(Debug, Clone)]
16pub struct ClassOptions {
17    /// Prefix each file with a provenance comment.
18    pub provenance: bool,
19}
20
21impl Default for ClassOptions {
22    fn default() -> Self {
23        ClassOptions { provenance: true }
24    }
25}
26
27/// Decompile one class (plus nested member classes) to a Java source file.
28///
29/// Classes with large method bodies run in a monitored thread with a
30/// deadline: pathological CFGs can drive the shared structurer's walk into
31/// an exponential exploration that never returns. On timeout the class is
32/// reported failed and the thread is abandoned (reaped at process exit).
33/// One registered monitored decompile: the receiver the worker polls at
34/// the tail, the class name (for diagnostics), and the deadline counted
35/// from the spawn.
36pub type PendingMonitor = (
37    std::sync::mpsc::Receiver<Result<String, String>>,
38    String,
39    std::time::Instant,
40);
41
42pub fn decompile_class(
43    pool: &std::sync::Arc<DexPool>,
44    class: &PoolClass,
45    opts: &ClassOptions,
46    pending: &std::sync::Mutex<Vec<PendingMonitor>>,
47) -> anyhow::Result<String> {
48    if class_is_risky(pool, class) {
49        // Detached monitored thread: the CALLER registers the receiver and
50        // moves on (awaiting happens at the end of the run) — a spinning
51        // pathological method no longer stalls its worker.
52        let pool2 = pool.clone();
53        let cls = class.clone();
54        let opts2 = opts.clone();
55        let (tx, rx) = std::sync::mpsc::channel::<Result<String, String>>();
56        let name = class.name.clone();
57        // 10s: pure insurance — the structurer's walk budgets and the
58        // ladder giant short-circuit bound pathological methods
59        // deterministically now (weixin cdp/l1: 4.2s single-class), but
60        // full-run worker contention can ~2× that; the old 5s dropped
61        // l1's file entirely (every referrer cannot-finds).
62        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
63        let _ = std::thread::Builder::new()
64            .stack_size(64 * 1024 * 1024)
65            .spawn(move || {
66                let r = decompile_class_impl(&pool2, &cls, &opts2).map_err(|e| format!("{:#}", e));
67                let _ = tx.send(r);
68            });
69        pending.lock().unwrap().push((rx, name, deadline));
70        return Err(anyhow::anyhow!(
71            "deferred to monitored thread (pathological-CFG guard)"
72        ));
73    }
74    decompile_class_impl(pool, class, opts)
75}
76
77/// Any method in the exponential-walk danger zone → run under the
78/// deadline. Calibrated between real workloads: routine R8 classes top out
79/// near 11k insns (reqable's biggest `<clinit>`); the exponential hangs
80/// (weibo's gson TypeAdapters) sit at 24k+.
81fn class_is_risky(pool: &DexPool, class: &PoolClass) -> bool {
82    // Header peek only — decoding every body here would double the APK's
83    // total decode work.
84    for m in class.all_methods() {
85        if m.code_off == 0 {
86            continue;
87        }
88        if let Some(dex) = pool.dex(m.dex_idx) {
89            if let Some((_regs, insns)) = dex.code_stats(m.code_off) {
90                if insns > 16_000 {
91                    return true;
92                }
93            }
94        }
95    }
96    false
97}
98
99fn decompile_class_impl(
100    pool: &DexPool,
101    class: &PoolClass,
102    opts: &ClassOptions,
103) -> anyhow::Result<String> {
104    let ctx = DexCtx::new(pool, class);
105    // Size the class buffer up front: corpus-average classes render to
106    // ~1KB per method — without the reserve the String doubles through
107    // 4-6 realloc+copy rounds per class (~2× the final size in memmove).
108    // Cap the reserve: weixin's monster classes (hundreds of methods)
109    // would reserve megabytes of untouched capacity per class — large
110    // blocks take mimalloc's commit/purge path (madvise churn showed up
111    // in profiles). 256KB covers the corpus-average class dozens of
112    // times over; bigger classes pay a few extra doublings.
113    let mut out = String::with_capacity(
114        (class.all_methods().count() * 1024).min(256 * 1024) + 512,
115    );
116    if opts.provenance {
117        static BANNER: std::sync::OnceLock<String> = std::sync::OnceLock::new();
118        out.push_str(BANNER.get_or_init(|| {
119            format!(
120                "// Decompiled by https://github.com/ejfkdev/ddc {}\n",
121                env!("CARGO_PKG_VERSION")
122            )
123        }));
124        // Provenance: which input image this class was lifted from (jadx's
125        // `loaded from: classes.dex` pattern). Byte-stable across runs —
126        // deliberately NO timestamp so outputs diff cleanly. Direct
127        // push_str: the per-class format! temporaries were 3 allocations
128        // × every class in the corpus.
129        if let Some(dex) = pool.dex(class.dex_idx) {
130            if let Some(label) = pool.dex_labels.get(class.dex_idx) {
131                out.push_str("// From: ");
132                out.push_str(label);
133                out.push_str(" (DEX ");
134                out.push_str(&dex.version);
135                out.push_str(")\n");
136            }
137        }
138    }
139    if let Some(src) = &class.source_file {
140        out.push_str("// Source file: ");
141        out.push_str(src);
142        out.push('\n');
143    }
144    if class.is_synthetic() {
145        out.push_str("// synthetic\n");
146    }
147    let (pkg, _) = split_name(&class.name);
148    // Two-pass import assembly: the body renders FIRST (into a temp
149    // buffer) with the obscured render state installed — expression
150    // positions DISCOVER additional obscured refs on the fly — then the
151    // file assembles as provenance + package + imports + body. The one
152    // extra body copy is the price of correct import placement.
153    let view_shadow = is_view_subclass(pool, class);
154    let mut obscured_map = compute_obscured_renders(pool, class, view_shadow);
155    // Same-package simple-name collisions: an import shadows every
156    // same-package use of that simple in this file — drop those from
157    // the map (their refs stay qualified, erroring honestly).
158    let mut blocked: jdc_core::FxHashSet<String> = pool
159        .package_simples()
160        .get(&pkg)
161        .cloned()
162        .unwrap_or_default();
163    // Blocked names must include the RENAMED displays of same-package
164    // classes too: refs and imports render through the rename registry,
165    // so an import matching a DISPLAY (not the raw simple) shadows that
166    // class's in-file uses exactly the same (weixin n0/y0 displays as
167    // y02 while h72 declares a raw class y0 — the raw-only blocked set
168    // let the y02 import through on one side and killed it on the
169    // other, stranding 622 bare `y02` refs).
170    // Renamed-display blocking: a ZERO-COPY view into the process-wide
171    // per-package cache (built once from the rename registry — iterating
172    // `blocked` through apply_class_rename per FILE was O(files ×
173    // package): WhatsApp's ~10k-class package X cost 1,269 cumulative
174    // worker-seconds, decompile 24s → 134s; per-file clones of the
175    // extension set still cost 184s).
176    static PKG_RENAMED_DISPLAYS: std::sync::OnceLock<
177        jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>,
178    > = std::sync::OnceLock::new();
179    let blocked_renamed: Option<&'static jdc_core::FxHashSet<String>> = {
180        let cache = PKG_RENAMED_DISPLAYS.get_or_init(|| {
181            let mut m: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
182                jdc_core::FxHashMap::default();
183            if let Some(ren) = jdc_core::rename::with_renames(|r| r.clone()) {
184                for (raw, disp) in ren.iter() {
185                    if raw == disp {
186                        continue;
187                    }
188                    let pkg_key = raw.rfind('/').map(|i| &raw[..i]).unwrap_or("");
189                    let tail = disp
190                        .rsplit(['/', '$'])
191                        .next()
192                        .unwrap_or(disp.as_str())
193                        .to_string();
194                    m.entry(pkg_key.to_string()).or_default().insert(tail);
195                }
196            }
197            m
198        });
199        cache.get(&pkg)
200    };
201    // In-file member shadowing: the own class's FIELDS and its family's
202    // nested-class tails are in scope inside the body and shadow any
203    // single-type import of the same simple (JLS 6.4.1/6.4.2) — an
204    // obscured simple colliding with them cannot be imported.
205    {
206        let own_raw = class.name.rsplit('/').next().unwrap_or("");
207        let prefix = format!("{}$", own_raw);
208        let extras: Vec<String> = blocked
209            .iter()
210            .filter(|b| b.starts_with(&prefix))
211            .filter_map(|b| b.rsplit('$').next().map(|t| t.to_string()))
212            .collect();
213        blocked.extend(extras);
214        for f in class.static_fields.iter().chain(class.instance_fields.iter()) {
215            blocked.insert(crate::classdec::java_ident(&f.name).into_owned());
216        }
217    }
218    obscured_map.retain(|_internal, simple| {
219        !blocked.contains(simple)
220            && !blocked_renamed.is_some_and(|e| e.contains(simple))
221    });
222    // Display-simple collisions cannot coexist as single-type imports:
223    // importing simple `n` while THIS file declares `n` is a JLS 7.5.1
224    // error, and two imports of the same simple make every use of it
225    // ambiguous (weibo `class a2 extends a2` with `import a.a.b.c.a2`
226    // + `import a.e.a.a.a2` — both shadow-renamed from package-`a`
227    // classes named `a`). Drop every colliding entry (own-display hits
228    // drop ALL claimants; multi-internal simples keep the least
229    // internal for determinism) — dropped refs fall back to the
230    // qualified display render, which resolves against the renamed
231    // output files.
232    let own_display_internal = crate::apply_class_rename(&class.name);
233    let own_disp_simple = own_display_internal
234        .rsplit(['/', '$'])
235        .next()
236        .unwrap_or("")
237        .to_string();
238    {
239        let mut by_simple: std::collections::BTreeMap<&str, Vec<&String>> =
240            std::collections::BTreeMap::new();
241        for (internal, simple) in obscured_map.iter() {
242            by_simple.entry(simple.as_str()).or_default().push(internal);
243        }
244        let mut drop: Vec<String> = Vec::new();
245        for (simple, mut internals) in by_simple {
246            if simple == own_disp_simple.as_str() {
247                drop.extend(internals.into_iter().cloned());
248            } else if internals.len() > 1 {
249                internals.sort();
250                drop.extend(internals.into_iter().skip(1).cloned());
251            }
252        }
253        for d in &drop {
254            obscured_map.remove(d);
255        }
256    }
257    set_obscured_state(
258        class.name.clone(),
259        obscured_map.clone(),
260        blocked.clone(),
261        blocked_renamed,
262        view_shadow,
263    );
264    let mut body_buf = String::with_capacity(out.capacity() / 2);
265    let body_res = emit_class_body(pool, class, &ctx, opts, &mut body_buf, 0);
266    let recorded = take_recorded_and_clear();
267    body_res?;
268    if !pkg.is_empty() {
269        out.push('\n');
270        out.push_str("package ");
271        let dotted = pkg.replace('/', ".");
272        out.push_str(&sanitize_fq(&dotted));
273        out.push_str(";\n");
274        // Imports: the metadata map plus the expression-level
275        // discoveries, pool-known only (an import of an unknown class
276        // is a hard error), display-renamed, sorted for determinism.
277        let mut import_set: jdc_core::FxHashSet<String> =
278            obscured_map.keys().cloned().collect();
279        for r in recorded {
280            if pool.get(&r).is_some() {
281                // The import renders under the RENAMED display — the
282                // blocked check must use the same name the refs render.
283                let display = crate::apply_class_rename(&r);
284                let simple = display.rsplit(['/', '$']).next().unwrap_or("");
285                if !simple.is_empty()
286                    && !blocked.contains(simple)
287                    && !blocked_renamed.is_some_and(|e| e.contains(simple))
288                {
289                    import_set.insert(r);
290                }
291            }
292        }
293        let mut imports: Vec<String> = import_set
294            .iter()
295            .map(|internal| crate::classdec::dotted(&crate::apply_class_rename(internal)))
296            .collect();
297        imports.sort();
298        // Body-discovered (recorded) refs joined after the pre-emission
299        // dedupe — re-check at render: one import per display simple,
300        // none against this file's own display (JLS 7.5.1). A dropped
301        // recorded ref's body render is ambiguous regardless; skipping
302        // its import at least avoids the duplicate-import error.
303        let own_display_dotted = crate::classdec::dotted(&own_display_internal);
304        let own_dot_simple = own_display_dotted.rsplit('.').next().unwrap_or("");
305        let mut seen_simples: jdc_core::FxHashSet<String> =
306            jdc_core::FxHashSet::default();
307        for display in imports {
308            let simple = display.rsplit('.').next().unwrap_or("");
309            if simple == own_dot_simple || !seen_simples.insert(simple.to_string()) {
310                continue;
311            }
312            out.push_str(&format!("import {};\n", display));
313        }
314    }
315    out.push('\n');
316    out.push_str(&body_buf);
317    Ok(out)
318}
319
320/// Render the class header, fields, methods and nested member classes into
321/// `out` (used at top level and recursively for nested members).
322// `opts` is only consumed by the nested-member recursion below — that
323// is its purpose (propagating emission options into inline children).
324/// One enum constant: the source identifier plus constructor
325/// arguments beyond the compiler-mandated `(String name, int ordinal)`.
326use jdc_core::ir::expr::{ConstVal, Expr};
327use jdc_core::ir::stmt::Stmt;
328struct EnumConst {
329    #[allow(dead_code)]
330    field: String,
331    name: String,
332    extra_args: Vec<Expr>,
333}
334
335/// Collect enum constants for a true `enum` rendering. Every ACC_ENUM
336/// static field must be initialized in `<clinit>` by
337/// `Self.field = new Self("NAME", ordinal, ...)` — the shape javac/d8
338/// always emit. Returns None (caller falls back to the desugared
339/// `/* enum */ class` form) when anything is missing: R8 variance,
340/// constant-specific bodies (the field holds an anonymous subclass), or
341/// a <clinit> that failed to decompile.
342fn collect_enum_constants(
343    pool: &DexPool,
344    class: &PoolClass,
345    ctx: &DexCtx<'_>,
346) -> Option<(Vec<EnumConst>, crate::method::MethodBody)> {
347    let _ = ctx;
348    let const_fields: Vec<&PoolField> = class
349        .static_fields
350        .iter()
351        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
352        .collect();
353    // No ACC_ENUM constant fields: R8 drops them ALL when nothing
354    // outside reads them (weixin sp2/ji — only the synthetic $VALUES
355    // array survives). An ACC_ENUM CLASS still promotes: the synthetic-
356    // constant scan below lifts the constants out of the array literal.
357    // Non-enum classes have no business here.
358    if const_fields.is_empty() && class.access & crate::access::ACC_ENUM == 0 {
359        return None;
360    }
361    // JLS 8.9.2: a restored true-enum ctor may not read the enum's own
362    // static fields (javac rejects even the QUALIFIED `E.field` form).
363    // Meituan Robust injects a hotpatch guard reading
364    // `changeQuickRedirect` into every instrumented ctor — legal in
365    // bytecode (injected after javac), and strip_robust_enum_ctor_guard
366    // removes it from the restored source. A self-static sget of any
367    // OTHER type makes restoration inexpressible → stay on the fallback.
368    for m in class.all_methods() {
369        if &*m.name != "<init>" {
370            continue;
371        }
372        let dex = pool.dex(m.dex_idx)?;
373        let ci = dex.code_at(m.code_off)?;
374        for ins in &ci.insns {
375            if let ddc_dex::insn::InsnKind::SGet { field_idx, .. } = ins.kind {
376                let fr = dex.field(field_idx);
377                if dex.class_name(fr.class_idx) == class.name.as_str()
378                    && dex.type_name(fr.type_idx) != "Lcom/meituan/robust/ChangeQuickRedirect;"
379                {
380                    return None;
381                }
382            }
383        }
384    }
385    let clinit = class.all_methods().find(|m| &*m.name == "<clinit>")?;
386    let mut body = decompile_method(pool, class, clinit).ok().flatten()?;
387    // The structurer can leave the constant-build sequence inside a
388    // nested Stmt::Block (jd1.u: stmt4 wraps all 26 ctors beside a
389    // trailing loop) — the passes below index a FLAT statement list.
390    if let Stmt::Block(vs) = &mut body.body {
391        crate::passes::flatten_top_blocks(vs);
392    }
393
394    // Meituan Robust hotpatch guard (weibo's lifecycle enums — a big
395    // slice of the 1,137 fallback population): the ENTIRE constant-build
396    // sequence lives in one branch of a top-level
397    // `if (PatchProxy.isSupportClinit(..)) { .. } else { .. }`, so the
398    // flat scans below see nothing and the enum falls back (its dex
399    // `Enum.valueOf(X.class,..)` then fails — X is no Enum subtype,
400    // weibo ×853). Hoist the constant-carrying branch to the top level,
401    // but only when the top level itself has no constant defs (an
402    // ordinary clinit must not be touched, and a mixed shape is beyond
403    // the flat-scan contract). Restoration semantics already normalize
404    // the clinit (constants move to the header, inits drop); the
405    // dispatch shim is inexpressible in a true enum regardless.
406    if let Stmt::Block(vs) = &mut body.body {
407        let self_cls: &str = class.name.as_str();
408        let top_def = |st: &Stmt| -> bool {
409            let v = match st {
410                Stmt::LocalDef { init: Some(e), .. } => e,
411                Stmt::ExprStmt(Expr::Assign { value, .. }) => value,
412                _ => return false,
413            };
414            matches!(v, Expr::New { cls, args, .. }
415                if cls.as_ref() == self_cls && args.len() >= 2)
416        };
417        if !vs.iter().any(top_def) {
418            let branch_news = |st: &Stmt| -> usize {
419                let mut n = 0usize;
420                crate::passes::visit_all_exprs(st, &mut |x| {
421                    if let Expr::New { cls, args, .. } = x {
422                        if cls.as_ref() == self_cls && args.len() >= 2 {
423                            n += 1;
424                        }
425                    }
426                });
427                n
428            };
429            let mut pick: Option<(usize, bool)> = None;
430            for (i, st) in vs.iter().enumerate() {
431                if let Stmt::If { then_stmt, else_stmt, .. } = st {
432                    let t = branch_news(then_stmt) > 0;
433                    let e = else_stmt
434                        .as_ref()
435                        .map(|s2| branch_news(s2) > 0)
436                        .unwrap_or(false);
437                    if t != e {
438                        pick = Some((i, t));
439                        break;
440                    }
441                }
442            }
443            if let Some((i, then_is_const)) = pick {
444                let st = std::mem::replace(&mut vs[i], Stmt::Block(Vec::new()));
445                if let Stmt::If { then_stmt, else_stmt, .. } = st {
446                    let keep = if then_is_const {
447                        Some(then_stmt)
448                    } else {
449                        else_stmt
450                    };
451                    if let Some(bx) = keep {
452                        let inner = match *bx {
453                            Stmt::Block(v) => v,
454                            other => vec![other],
455                        };
456                        vs.splice(i..=i, inner);
457                        // The branch can be double-wrapped (Block[Block[..]]);
458                        // the passes below index a FLAT list.
459                        crate::passes::flatten_top_blocks(vs);
460                    }
461                }
462            }
463        }
464    }
465
466    // R8/d8 split the constant build across an intermediate local:
467    //   Self v0 = new Self("NAME", i, ...);
468    //   a = v0;                       // sput to the ACC_ENUM field
469    //   Self[] v5 = new Self[n]; v5[k] = v0; ...; $VALUES = v5;
470    // Pass 1 registers each intermediate (or direct) new; pass 2 binds
471    // them to the ACC_ENUM fields; references to the locals are then
472    // rewritten into constant identifiers so the $VALUES build keeps
473    // compiling once the definitions drop.
474    use std::collections::HashMap;
475    let mut const_name: Vec<String> = Vec::new();
476    let mut const_field: Vec<String> = Vec::new();
477    let mut const_extra: Vec<Vec<Expr>> = Vec::new();
478    // Declared ordinal per constant (the ctor's int arg) — the merge at
479    // the end orders field-bound and synthetic constants by it and
480    // demands a dense 0..n run (Java derives ordinals from declaration
481    // position).
482    let mut const_ord: Vec<i64> = Vec::new();
483    let mut var_of: HashMap<u32, usize> = HashMap::default(); // local id -> const idx
484    let mut drop_stmts: Vec<usize> = Vec::new();
485
486    // Collect (immutable borrows) first; the mutable passes come after.
487    if !matches!(&body.body, Stmt::Block(_)) { return None; }
488
489    // Pass 1: definitions. (immutable borrow; rewrite comes later)
490    // Rolling reaching-defs of clinit locals for resolving enum-ctor
491    // extra args (resolve_enum_arg); `tainted` flips at the first
492    // non-straight-line statement — past it, linear-scan defs are no
493    // longer provable and Local args reject the enum mode.
494    let mut defs: HashMap<u32, &Expr> = HashMap::default();
495    let mut tainted = false;
496    let self_name: std::sync::Arc<str> = class.name.as_str().into();
497    let self_ty =
498        jdc_core::ir::expr::TypeRef::J(JavaType::Object(class.name.as_str().into()));
499    // A REUSED intermediate local: `v = new Self(..); A = v; v = new
500    // Self(..); B = v;` — the first def is a LocalDef, the rest are
501    // Assigns to the same var. Register every def site; pass 2 resolves
502    // a local sput to the def site CURRENT at that statement (rolling),
503    // not to the var (Kotlin EnumEntries enums, RegexOption: 8→7 count
504    // mismatch used to abort the whole promotion).
505    let mut def_site: HashMap<usize, (u32, usize)> = HashMap::default();
506    for (i, st) in match &body.body {
507        Stmt::Block(v) => v.iter().enumerate(),
508        _ => return None,
509    } {
510        // (var, New) for both def shapes: LocalDef and Assign-to-local.
511        let def_shape: Option<(u32, &Expr)> = match st {
512            Stmt::LocalDef {
513                var,
514                init: Some(e),
515                ..
516            } => Some((*var, e)),
517            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
518                match (&**target, &**value) {
519                    (Expr::Local { var, .. }, e) => Some((*var, e)),
520                    _ => None,
521                }
522            }
523            _ => None,
524        };
525        if let Some((var, Expr::New { cls: ncls, args, .. })) = def_shape {
526            if ncls.as_ref() == class.name && args.len() >= 2 {
527                if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
528                    (&args[0], &args[1])
529                {
530                    if java_ident(n).as_ref() != &**n || n.is_empty() { return None; }
531                    var_of.insert(var, const_name.len());
532                    def_site.insert(i, (var, const_name.len()));
533                    const_ord.push(*ord0 as i64);
534                    const_name.push(n.to_string());
535                    const_field.push(String::new());
536                    const_extra.push(resolve_enum_extras(
537                        &args[2..],
538                        &defs,
539                        tainted,
540                        &var_of,
541                        &const_name,
542                        &self_name,
543                        &self_ty,
544                    )?);
545                    drop_stmts.push(i);
546                }
547            }
548        }
549        track_def(st, &mut defs, &mut tainted);
550    }
551
552    // Pass 2: sputs to the ACC_ENUM fields (direct new or intermediate).
553    // Local sputs resolve through the ROLLING def map (updated at each
554    // def-site statement) — a reused `v` must bind to the constant
555    // defined most recently, not to the var's first registration.
556    let mut defs2: HashMap<u32, &Expr> = HashMap::default();
557    let mut tainted2 = false;
558    let mut cur_def: HashMap<u32, usize> = HashMap::default();
559    for (i, st) in match &body.body {
560        Stmt::Block(v) => v.iter().enumerate(),
561        _ => return None,
562    } {
563        if let Some((var, idx)) = def_site.get(&i) {
564            cur_def.insert(*var, *idx);
565        }
566        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
567            if let Expr::Field {
568                cls,
569                name: fname,
570                is_static: true,
571                ..
572            } = &**target
573            {
574                if cls.as_ref() != class.name {
575                    continue;
576                }
577                if !const_fields.iter().any(|f| f.name.as_str() == &**fname) {
578                    continue;
579                }
580                if const_field.iter().any(|f| !f.is_empty() && f == &**fname) { return None; // duplicate assignment
581                }
582                let idx = match &**value {
583                    Expr::Local { var, .. } => cur_def.get(var).copied()?,
584                    Expr::New { cls: ncls, args, .. }
585                        if ncls.as_ref() == class.name && args.len() >= 2 =>
586                    {
587                        if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
588                            (&args[0], &args[1])
589                        {
590                            if java_ident(n).as_ref() != &**n || n.is_empty() { return None; }
591                            let idx = const_name.len();
592                            const_ord.push(*ord0 as i64);
593                            const_name.push(n.to_string());
594                            const_field.push(String::new());
595                            const_extra.push(resolve_enum_extras(
596                                &args[2..],
597                                &defs2,
598                                tainted2,
599                                &var_of,
600                                &const_name,
601                                &self_name,
602                                &self_ty,
603                            )?);
604                            idx
605                        } else {
606                            return None;
607                        }
608                    }
609                    _ => return None,
610                };
611                const_field[idx] = fname.to_string();
612                drop_stmts.push(i);
613            }
614        }
615        track_def(st, &mut defs2, &mut tainted2);
616    }
617    // Own the reaching-def values: defs2 borrows body.body, and the
618    // synthetic-constant scan below mutates the $VALUES array in place.
619    let defs2_owned: std::collections::HashMap<u32, Expr> = defs2
620        .iter()
621        .map(|(k, v)| (*k, (*v).clone()))
622        .collect();
623    drop(defs2);
624    let defs2: std::collections::HashMap<u32, &Expr> =
625        defs2_owned.iter().map(|(k, v)| (*k, v)).collect();
626
627    // Every ACC_ENUM field bound, every intermediate matched.
628    // Every ACC_ENUM FIELD must be bound to a constant. The converse
629    // need not hold: R8 drops the FIELD of a constant nothing reads
630    // externally while the $VALUES array keeps it (weixin lite/api/n's
631    // ON_DESTROY — a pass-1 local with no pass-2 sput). Field-less
632    // constants keep their ctor-string source name.
633    if const_field.iter().filter(|f| !f.is_empty()).count() != const_fields.len() {
634        return None;
635    }
636    // Obfuscated enums rename the ACC_ENUM FIELD (d/e/f) while the ctor's
637    // name STRING keeps the source identifier — the promoted constant
638    // must be declared under the FIELD name: every reference in the
639    // pool resolves through it. Using the name string broke all
640    // cross-file references (weixin +2.9k when newly-promoted enums
641    // swapped d/e/f for TEXT_ENTER_EDITING/…).
642    for i in 0..const_name.len() {
643        let field = &const_field[i];
644        if !field.is_empty() && field != &const_name[i] {
645            let id = java_ident(field);
646            if id.is_empty() || id.as_ref() != field.as_str() {
647                return None;
648            }
649            const_name[i] = field.clone();
650        }
651    }
652    {
653        let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::default();
654        if !const_name.iter().all(|c| seen.insert(c.as_str())) { return None; }
655    }
656
657    // Synthetic constants: entries built INLINE inside the $VALUES
658    // array (`new Self("NAME", ord, ..)` with no ACC_ENUM field — R8
659    // drops the field when nothing outside reads it; weixin u2/i's
660    // "CONSTANT"). Promote them so the header declares the constant:
661    // the raw `new` in the array is "无法实例化枚举类", and every
662    // constant after the gap carries a SHIFTED ordinal (e was 2 in the
663    // dex, renders as position 1).
664    let mut synth: Vec<(i64, String, Vec<Expr>)> = Vec::new();
665    {
666        let self_cls: &str = class.name.as_str();
667        if let Stmt::Block(vs) = &mut body.body {
668            for st in vs.iter_mut() {
669                crate::passes::walk_stmt_exprs(st, &mut |e| {
670                    crate::passes::deep_rewrite(e, &mut |x| {
671                        let Expr::NewArray { elem, init: Some(list), .. } = x else {
672                            return;
673                        };
674                        if elem.erased() != JavaType::Object(self_cls.into()) {
675                            return;
676                        }
677                        for slot in list.iter_mut() {
678                            let (n, ord, rest) = match slot {
679                                Expr::New { cls, args, raw: false, .. }
680                                    if cls.as_ref() == self_cls && args.len() >= 2 =>
681                                {
682                                    match (&args[0], &args[1]) {
683                                        (
684                                            Expr::Const(ConstVal::Str(sv)),
685                                            Expr::Const(ConstVal::Int(o)),
686                                        ) => (sv.to_string(), *o as i64, args[2..].to_vec()),
687                                        _ => continue,
688                                    }
689                                }
690                                _ => continue,
691                            };
692                            if java_ident(&n).as_ref() != n.as_str() || n.is_empty() {
693                                continue;
694                            }
695                            if const_name.contains(&n)
696                                || synth.iter().any(|(_, s2, _)| *s2 == n)
697                            {
698                                continue;
699                            }
700                            let Some(extras) = resolve_enum_extras(
701                                &rest,
702                                &defs2,
703                                tainted2,
704                                &var_of,
705                                &const_name,
706                                &self_name,
707                                &self_ty,
708                            ) else {
709                                continue;
710                            };
711                            *slot = Expr::Field {
712                                owner: None,
713                                cls: self_name.clone(),
714                                name: std::sync::Arc::from(n.as_str()),
715                                ty: self_ty.clone(),
716                                is_static: true,
717                            };
718                            synth.push((ord, n, extras));
719                        }
720                    });
721                });
722            }
723        }
724    }
725
726    // Pass 3: rewrite references to the intermediate locals into the
727    // constant identifiers (static-field reads on Self). POSITION-AWARE:
728    // a REUSED local must read as the constant defined most recently at
729    // that statement — a var-level map made every `arr[k] = v` in the
730    // $VALUES build reference the LAST constant (weixin +2.9k when this
731    // regressed values() contents).
732    {
733        let mut cur: HashMap<u32, usize> = HashMap::default();
734        if let Stmt::Block(vs) = &mut body.body {
735            for (i, st) in vs.iter_mut().enumerate() {
736                if let Some((var, idx)) = def_site.get(&i) {
737                    cur.insert(*var, *idx);
738                }
739                crate::passes::walk_stmt_exprs(st, &mut |e| {
740                    crate::passes::deep_rewrite(e, &mut |x| {
741                        if let Expr::Local { var, .. } = x {
742                            if let Some(&idx) = cur.get(var) {
743                                let name: std::sync::Arc<str> =
744                                    std::sync::Arc::from(const_name[idx].as_str());
745                                *x = Expr::Field {
746                                    owner: None,
747                                    cls: self_name.clone(),
748                                    name,
749                                    ty: self_ty.clone(),
750                                    is_static: true,
751                                };
752                            }
753                        }
754                    });
755                });
756            }
757        }
758    }
759
760    // Pass 4: drop the definitions and their sputs.
761    if let Stmt::Block(vs) = &mut body.body {
762        let drop_set: std::collections::HashSet<usize> =
763            drop_stmts.iter().copied().collect();
764        vs.retain(|_| true);
765        let mut idx = 0usize;
766        vs.retain(|_| {
767            let keep = !drop_set.contains(&idx);
768            idx += 1;
769            keep
770        });
771    }
772    // The resolved args consumed the clinit locals' readers; prune the
773    // now-dead defs from the remnant (impure inits survive as bare
774    // expression statements — drop_dead_locals' standard contract).
775    crate::passes::drop_dead_locals(&mut body.body);
776
777    let mut merged: Vec<(i64, EnumConst)> = const_ord
778        .into_iter()
779        .zip(
780            const_field
781                .into_iter()
782                .zip(const_name)
783                .zip(const_extra)
784                .map(|((field, name), extra_args)| EnumConst {
785                    field,
786                    name,
787                    extra_args,
788                }),
789        )
790        .collect();
791    for (ord, name, extra_args) in synth {
792        merged.push((
793            ord,
794            EnumConst {
795                field: name.clone(),
796                name,
797                extra_args,
798            },
799        ));
800    }
801    if merged.is_empty() {
802        return None; // nothing extracted — keep the desugared form
803    }
804    merged.sort_by_key(|(o, _)| *o);
805    // Ordinals must be UNIQUE and ASCENDING; gaps are legal — R8 drops
806    // unused constants wholesale (weixin lite/api/n starts at ordinal
807    // 1). Java derives ordinal() from declaration position, so a gap
808    // shifts every later constant. Pad each gap with a synthetic
809    // constant (`_r<k>`) to keep positions faithful — only when no
810    // constant carries extra ctor args (a pad cannot invent them).
811    // The synthetic values()/valueOf() the true-enum render suppresses
812    // then see the pad (values() drift vs the dex $VALUES array — the
813    // compilable-and-ordinal-faithful side of the trade).
814    let has_extras = merged.iter().any(|(_, c)| !c.extra_args.is_empty());
815    let mut member_taken: jdc_core::FxHashSet<String> = merged
816        .iter()
817        .map(|(_, c)| c.name.clone())
818        .chain(
819            class
820                .static_fields
821                .iter()
822                .chain(class.instance_fields.iter())
823                .map(|f| f.name.to_string()),
824        )
825        .chain(class.all_methods().map(|m| m.name.to_string()))
826        .collect();
827    let mut padded: Vec<EnumConst> = Vec::with_capacity(merged.len());
828    let mut expect: i64 = 0;
829    let mut pad_k = 0u32;
830    for (ord, c) in merged {
831        if ord < expect {
832            return None; // duplicate/colliding ordinals — unfaithful
833        }
834        if ord > expect {
835            // Extras-bearing ctors: a pad may borrow an adjacent
836            // constant's literal extras ONLY when every ctor of the
837            // class is benign (no branch, no throw, no non-<init>
838            // invoke — pure param stores), so the invented value
839            // cannot change runtime behavior beyond a dead constant's
840            // stored field (jd1.u: gap at ordinal 7 + int extra arg
841            // aborted the whole promotion — 92 files of broken
842            // Enum.valueOf). Non-benign ctors keep the abort.
843            let tmpl: Vec<Expr> = if has_extras {
844                if !enum_ctors_benign(pool, class) {
845                    return None; // cannot synthesize the missing ctor args
846                }
847                match padded.last() {
848                    Some(prev) => prev.extra_args.clone(),
849                    None => c.extra_args.clone(),
850                }
851            } else {
852                Vec::new()
853            };
854            while expect < ord {
855                let pname = loop {
856                    let cand = format!("_r{pad_k}");
857                    pad_k += 1;
858                    if !member_taken.contains(&cand) {
859                        member_taken.insert(cand.clone());
860                        break cand;
861                    }
862                };
863                padded.push(EnumConst {
864                    field: pname.clone(),
865                    name: pname,
866                    extra_args: tmpl.clone(),
867                });
868                expect += 1;
869            }
870        }
871        expect = ord + 1;
872        padded.push(c);
873    }
874    let out = padded;
875    Some((out, body))
876}
877
878/// True when every `<init>` of the class is a pure param-store: no
879/// branch, no throw, no switch, no goto, and no invoke other than a
880/// constructor call (the super delegation). A benign ctor cannot
881/// validate or leak its arguments, so gap-pad constants may borrow a
882/// neighbor's literal extras without observable effect.
883fn enum_ctors_benign(pool: &DexPool, class: &PoolClass) -> bool {
884    let mut checked = 0usize;
885    for m in class.all_methods() {
886        if &*m.name != "<init>" {
887            continue;
888        }
889        checked += 1;
890        let Some(dex) = pool.dex(m.dex_idx) else {
891            return false;
892        };
893        let Some(ci) = dex.code_at(m.code_off) else {
894            return false;
895        };
896        for ins in &ci.insns {
897            use ddc_dex::insn::InsnKind;
898            match ins.kind {
899                InsnKind::If { .. }
900                | InsnKind::Throw { .. }
901                | InsnKind::PackedSwitch { .. }
902                | InsnKind::SparseSwitch { .. }
903                | InsnKind::Goto { .. } => return false,
904                InsnKind::Invoke { method_idx, .. } => {
905                    let mr = dex.method(method_idx);
906                    if dex.string(mr.name_idx) != "<init>" {
907                        return false;
908                    }
909                }
910                _ => {}
911            }
912        }
913    }
914    checked > 0
915}
916
917/// Update the rolling reaching-def map for enum-arg resolution. Only
918/// flat single-target definitions keep the tracking sound; any control
919/// flow taints it (a linear scan can no longer prove WHICH definition
920/// reaches later capture sites).
921fn track_def<'e>(
922    st: &'e Stmt,
923    defs: &mut std::collections::HashMap<u32, &'e Expr>,
924    tainted: &mut bool,
925) {
926    match st {
927        Stmt::LocalDef { var, init, .. } => match init {
928            Some(e) => {
929                defs.insert(*var, e);
930            }
931            None => {
932                defs.remove(var);
933            }
934        },
935        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
936            if let Expr::Local { var, .. } = &**target {
937                if matches!(op, jdc_core::ir::expr::AssignOp::Plain) {
938                    defs.insert(*var, value);
939                } else {
940                    defs.remove(var);
941                }
942            }
943        }
944        // Plain expression statements (calls) don't define locals.
945        Stmt::ExprStmt(_) => {}
946        _ => *tainted = true,
947    }
948}
949
950/// Resolve every enum-ctor extra arg to a self-contained expression, or
951/// reject the enum mode (None). R8 reuses ONE register across all
952/// constant constructions — revenuecat's LogIntent builds 11 of 12
953/// emoji lists through the same `list` local, reassigned between the
954/// `new Self(.., list)` sites — and enum constants render OUTSIDE the
955/// clinit where no local is in scope: an unresolved `Local` used to
956/// print as the vt-dummy name (`DEBUG(var0)` — per-constant
957/// cannot-find). Each Local is replaced by its reaching pure definition
958/// (recursively) or, when it names an earlier constant's intermediate,
959/// by a static-field reference to that constant. Any expression shape
960/// WITHOUT locals passes through untouched (enum args may be arbitrary
961/// expressions — BinOp/Cast/Method/New — each renders exactly once per
962/// constant, so no duplication concern applies). Only an unresolvable
963/// Local (missing/tainted def, depth > 4) rejects the whole enum
964/// detection; the class then falls back to plain-field rendering, which
965/// always compiles.
966fn resolve_enum_extras(
967    extras: &[Expr],
968    defs: &std::collections::HashMap<u32, &Expr>,
969    tainted: bool,
970    var_of: &std::collections::HashMap<u32, usize>,
971    const_name: &[String],
972    self_name: &std::sync::Arc<str>,
973    self_ty: &jdc_core::ir::expr::TypeRef,
974) -> Option<Vec<Expr>> {
975    extras
976        .iter()
977        .map(|e| {
978            let mut out = e.clone();
979            let mut fail = false;
980            resolve_locals_in(
981                &mut out, defs, tainted, var_of, const_name, self_name, self_ty, 0,
982                &mut fail,
983            );
984            if fail {
985                None
986            } else {
987                Some(out)
988            }
989        })
990        .collect()
991}
992
993#[allow(clippy::too_many_arguments)]
994fn resolve_locals_in(
995    e: &mut Expr,
996    defs: &std::collections::HashMap<u32, &Expr>,
997    tainted: bool,
998    var_of: &std::collections::HashMap<u32, usize>,
999    const_name: &[String],
1000    self_name: &std::sync::Arc<str>,
1001    self_ty: &jdc_core::ir::expr::TypeRef,
1002    depth: u32,
1003    fail: &mut bool,
1004) {
1005    crate::passes::deep_rewrite(e, &mut |x| {
1006        if let Expr::Local { var, .. } = x {
1007            // An earlier constant's intermediate: a static-field read of
1008            // that constant (declared above — backward reference, legal
1009            // in enum ctor args).
1010            if let Some(&ci) = var_of.get(var) {
1011                if let Some(nm) = const_name.get(ci) {
1012                    *x = Expr::Field {
1013                        owner: None,
1014                        cls: self_name.clone(),
1015                        name: std::sync::Arc::from(nm.as_str()),
1016                        ty: self_ty.clone(),
1017                        is_static: true,
1018                    };
1019                    return;
1020                }
1021            }
1022            if tainted || depth > 4 {
1023                *fail = true;
1024                return;
1025            }
1026            let Some(d) = defs.get(var).copied() else {
1027                *fail = true;
1028                return;
1029            };
1030            let mut sub = d.clone();
1031            resolve_locals_in(
1032                &mut sub, defs, tainted, var_of, const_name, self_name, self_ty,
1033                depth + 1, fail,
1034            );
1035            if *fail {
1036                return;
1037            }
1038            *x = sub;
1039        }
1040    });
1041}
1042
1043/// Render enum-constant constructor arguments via the shared expression
1044/// emitter (the VarTable is irrelevant for argument printing — no local
1045/// names appear — but the API requires one).
1046fn render_enum_args(ctx: &DexCtx<'_>, pool: &DexPool, args: &[Expr], out: &mut String) {
1047    let dummy_vt = jdc_core::var::VarTable::default();
1048    let mut p = Printer::new(ctx, &dummy_vt);
1049    for (i, a) in args.iter().enumerate() {
1050        if i > 0 {
1051            out.push_str(", ");
1052        }
1053        p.expr(a, 0, out);
1054    }
1055    let _ = pool;
1056}
1057
1058/// Synthesize `throw new AbstractMethodError()` stubs for abstract
1059/// interface methods a CONCRETE class fails to implement. R8 tree-shakes
1060/// an interface method whose call sites it proved unreachable, leaving a
1061/// concrete class ART accepts but javac rejects ("X不是抽象的, 并且未覆盖
1062/// 抽象方法…"). jadx reproduces the same incomplete class; the stub
1063/// restores compilability AND matches ART semantics exactly (invoking a
1064/// stripped abstract method throws AbstractMethodError). `provided` is
1065/// computed from the DEX method table (not the render), so a method ddc
1066/// merely failed to render is never double-declared, and a same-erasure
1067/// return-type-specialized sibling (Family B: void `invoke` beside the
1068/// Object bridge) counts as provided → no colliding stub.
1069struct ParsedStub {
1070    ret: JavaType,
1071    args: Vec<JavaType>,
1072}
1073
1074/// Standard ctor argument signatures of the common Throwable-family
1075/// framework classes. R8 strips custom exceptions to empty shells
1076/// (`final class k extends Error {}`) whose callers reach Error(String)
1077/// through OUTLINED ctor invokes — the method ref owner is the
1078/// FRAMEWORK super, invisible to the owner-keyed ctor_ref_sigs, so the
1079/// ref-driven bridges cannot fire (uuyc `throw new Bd.k("...")` ×525).
1080/// Mirroring the classic set is dex-faithful (ART resolves <init> up
1081/// the chain too) and each bridge is a plain super(..) delegation.
1082fn throwable_ctor_sigs(fw: &str) -> Option<&'static [&'static str]> {
1083    // The empty sig is included because emitting ANY explicit ctor
1084    // suppresses Java's implicit default: dex callers legally reach
1085    // Throwable() up the chain (`new X()` against the empty shell), and
1086    // without the no-arg bridge they strand (weixin +1,464 找不到合适
1087    // 的构造器 when the set omitted it). `public X() { super(); }` is
1088    // exactly the implicit default.
1089    const SIGS: &[&str] = &[
1090        "",
1091        "Ljava/lang/String;",
1092        "Ljava/lang/String;Ljava/lang/Throwable;",
1093        "Ljava/lang/Throwable;",
1094    ];
1095    matches!(
1096        fw,
1097        "java/lang/Throwable"
1098            | "java/lang/Error"
1099            | "java/lang/Exception"
1100            | "java/lang/RuntimeException"
1101            | "java/io/IOException"
1102            | "java/io/InterruptedIOException"
1103            | "java/io/UnsupportedEncodingException"
1104            | "java/io/EOFException"
1105            | "java/io/FileNotFoundException"
1106            | "java/net/SocketException"
1107            | "java/net/URISyntaxException"
1108            | "java/lang/CloneNotSupportedException"
1109            | "java/lang/InterruptedException"
1110            | "java/lang/ReflectiveOperationException"
1111            | "java/lang/ClassNotFoundException"
1112            | "java/lang/NoSuchFieldException"
1113            | "java/lang/NoSuchMethodException"
1114            | "java/lang/InstantiationException"
1115            | "java/lang/IllegalAccessException"
1116            | "java/lang/reflect/InvocationTargetException"
1117            | "java/security/GeneralSecurityException"
1118            | "java/util/concurrent/ExecutionException"
1119            | "java/util/concurrent/TimeoutException"
1120            | "java/text/ParseException"
1121    )
1122    .then_some(SIGS)
1123}
1124
1125fn synth_missing_interface_stubs(
1126    pool: &DexPool,
1127    class: &PoolClass,
1128    out: &mut String,
1129    depth: usize,
1130    emitted_any: &mut bool,
1131) {
1132    use crate::access::*;
1133    // Concrete classes only: interfaces / abstract classes / annotations
1134    // legitimately leave methods unimplemented.
1135    if class.access & (ACC_INTERFACE | ACC_ABSTRACT | ACC_ANNOTATION) != 0 {
1136        return;
1137    }
1138    fn argsig(d: &str) -> &str {
1139        let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1140        let hi = d.find(')').unwrap_or(d.len());
1141        &d[lo..hi]
1142    }
1143    // Classic framework interfaces whose abstract methods a concrete
1144    // implementer must carry. R8 strips an implementation whose call
1145    // sites it proved unreachable, and the closure walk cannot see
1146    // framework method lists (not in the dex) — without the table,
1147    // `implements Runnable` with a stripped run() fails javac (lark
1148    // Runnable.run ×8, reqable Parcelable.writeToParcel ×15,
1149    // Closeable.close ×3). The stub stays ART-faithful: dispatch on
1150    // the missing proto throws AbstractMethodError at runtime too.
1151    const FW_IFACE_METHODS: &[(&str, &[(&str, &str)])] = &[
1152        ("java/lang/Runnable", &[("run", "()V")]),
1153        ("java/lang/AutoCloseable", &[("close", "()V")]),
1154        ("java/io/Closeable", &[("close", "()V")]),
1155        ("java/io/Flushable", &[("flush", "()V")]),
1156        ("java/util/concurrent/Callable", &[("call", "()Ljava/lang/Object;")]),
1157        ("java/lang/Comparable", &[("compareTo", "(Ljava/lang/Object;)I")]),
1158        ("java/util/Iterator", &[("hasNext", "()Z"), ("next", "()Ljava/lang/Object;")]),
1159        ("java/util/Iterable", &[("iterator", "()Ljava/util/Iterator;")]),
1160        ("java/util/Enumeration", &[("hasMoreElements", "()Z"), ("nextElement", "()Ljava/lang/Object;")]),
1161        ("java/util/Comparator", &[("compare", "(Ljava/lang/Object;Ljava/lang/Object;)I")]),
1162        (
1163            "android/os/Parcelable",
1164            &[("writeToParcel", "(Landroid/os/Parcel;I)V"), ("describeContents", "()I")],
1165        ),
1166        ("android/view/View$OnClickListener", &[("onClick", "(Landroid/view/View;)V")]),
1167        (
1168            "android/content/ServiceConnection",
1169            &[
1170                ("onServiceConnected", "(Landroid/content/ComponentName;Landroid/os/IBinder;)V"),
1171                ("onServiceDisconnected", "(Landroid/content/ComponentName;)V"),
1172            ],
1173        ),
1174        (
1175            "java/lang/Thread$UncaughtExceptionHandler",
1176            &[("uncaughtException", "(Ljava/lang/Thread;Ljava/lang/Throwable;)V")],
1177        ),
1178    ];
1179    // Transitive interface closure — seeded from the WHOLE superclass
1180    // chain, not just the class's own `implements`: weixin rd extends
1181    // abstract k0 implements ey2.a, R8 stripped rd's onAttach, and the
1182    // old own-interfaces-only seed saw an empty requirement (×51
1183    // weixin a.onAttach family). Pool interfaces contribute their
1184    // method lists; framework interfaces consult the table above.
1185    // (name, argsig) -> the unique declaring desc, or None when two
1186    // ancestors declare the same erasure with DIFFERENT descriptors —
1187    // a Java-inexpressible diamond where any synthesized stub would
1188    // itself fail "无法覆盖/无法实现" (weibo +1016 regression when the
1189    // first-wins desc was stubbed against a conflicting sibling).
1190    let mut required: jdc_core::FxHashMap<(String, String), Option<(String, String)>> =
1191        jdc_core::FxHashMap::default();
1192    let mut defaults: jdc_core::FxHashSet<(String, String)> = jdc_core::FxHashSet::default();
1193    let mut stack: Vec<String> = Vec::new();
1194    {
1195        let mut cur: Option<&PoolClass> = Some(class);
1196        let mut hops = 0;
1197        while let Some(c) = cur {
1198            hops += 1;
1199            if hops > 64 {
1200                break;
1201            }
1202            stack.extend(c.interfaces.iter().cloned());
1203            let mut hit_fw = false;
1204            cur = c.super_name.as_ref().and_then(|s| {
1205                if s == "java/lang/Object" {
1206                    None
1207                } else {
1208                    let pc = pool.get(s);
1209                    if pc.is_none() {
1210                        hit_fw = true;
1211                    }
1212                    pc
1213                }
1214            });
1215            if hit_fw {
1216                // A FRAMEWORK superclass contributes methods the pool
1217                // walk cannot see: the class may already provide (or be
1218                // barred from overriding — Context.getString is final)
1219                // any requirement, and a stub here collided ×669 weibo.
1220                // No stubs past the framework boundary.
1221                return;
1222            }
1223        }
1224    }
1225    let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1226    while let Some(iname) = stack.pop() {
1227        if !seen.insert(iname.clone()) {
1228            continue;
1229        }
1230        let Some(ic) = pool.get(&iname) else {
1231            for (iface, methods) in FW_IFACE_METHODS {
1232                if *iface == iname.as_str() {
1233                    for (mn, md) in *methods {
1234                        let key = (mn.to_string(), argsig(md).to_string());
1235                        match required.entry(key) {
1236                            std::collections::hash_map::Entry::Vacant(v) => {
1237                                v.insert(Some((mn.to_string(), md.to_string())));
1238                            }
1239                            std::collections::hash_map::Entry::Occupied(mut o) => {
1240                                if o.get().as_ref().is_some_and(|(_, d)| d != md) {
1241                                    o.insert(None);
1242                                }
1243                            }
1244                        }
1245                    }
1246                }
1247            }
1248            continue;
1249        };
1250        for m in ic.all_methods() {
1251            if m.is_static() || &*m.name == "<clinit>" || &*m.name == "<init>" {
1252                continue;
1253            }
1254            let key = (m.name.to_string(), argsig(&m.desc).to_string());
1255            if m.code_off == 0 {
1256                let mdesc = m.desc.to_string();
1257                match required.entry(key) {
1258                    std::collections::hash_map::Entry::Vacant(v) => {
1259                        v.insert(Some((m.name.to_string(), mdesc)));
1260                    }
1261                    std::collections::hash_map::Entry::Occupied(mut o) => {
1262                        if o.get().as_ref().is_some_and(|(_, d)| *d != mdesc) {
1263                            o.insert(None);
1264                        }
1265                    }
1266                }
1267            } else {
1268                defaults.insert(key);
1269            }
1270        }
1271        stack.extend(ic.interfaces.iter().cloned());
1272    }
1273    if required.is_empty() {
1274        return;
1275    }
1276    // Methods provided by the class + its concrete superclass chain, plus
1277    // the java/lang/Object publics that satisfy common requirements.
1278    let mut provided: jdc_core::FxHashSet<(String, String)> = jdc_core::FxHashSet::default();
1279    provided.insert(("equals".into(), "Ljava/lang/Object;".into()));
1280    provided.insert(("hashCode".into(), String::new()));
1281    provided.insert(("toString".into(), String::new()));
1282    // (name, desc) pairs whose erasure twin exists elsewhere in the
1283    // chain with a DIFFERENT desc — stub-hostile shapes (see below).
1284    let mut chain_sigs: jdc_core::FxHashMap<(String, String), jdc_core::FxHashSet<String>> =
1285        jdc_core::FxHashMap::default();
1286    let mut cur: Option<&PoolClass> = Some(class);
1287    while let Some(c) = cur {
1288        for m in c.all_methods() {
1289            if !m.is_static() && &*m.name != "<init>" && &*m.name != "<clinit>" {
1290                chain_sigs
1291                    .entry((m.name.to_string(), argsig(&m.desc).to_string()))
1292                    .or_default()
1293                    .insert(m.desc.to_string());
1294            }
1295        }
1296        for m in c.all_methods() {
1297            // A method PROVIDES the implementation if it is not abstract:
1298            // bytecode (code_off != 0) OR native (JNI body, code_off == 0
1299            // but ACC_NATIVE — `…ToNative` methods satisfy their interface
1300            // and must not be stubbed twice).
1301            if m.access & ACC_ABSTRACT == 0 && !m.is_static() {
1302                // A RENAMED method no longer provides its raw (name,
1303                // argsig): impostor overrides (return type satisfying no
1304                // ancestor declaration) and erasure-clash losers render
1305                // under their registry display, and the ancestor
1306                // requirement falls to the stub below — matching ART,
1307                // where the interface proto never resolved either.
1308                let renamed = jdc_core::rename::field_display(&c.name, &m.name, &m.desc)
1309                    .is_some();
1310                if !renamed {
1311                    provided.insert((m.name.to_string(), argsig(&m.desc).to_string()));
1312                }
1313            }
1314        }
1315        cur = c.super_name.as_ref().and_then(|s| {
1316            if s == "java/lang/Object" {
1317                None
1318            } else {
1319                pool.get(s)
1320            }
1321        });
1322    }
1323    let mut chain_conflict: jdc_core::FxHashSet<(String, String)> =
1324        jdc_core::FxHashSet::default();
1325    for ((name, _argsig), descs) in chain_sigs.iter() {
1326        if descs.len() > 1 {
1327            for d in descs {
1328                chain_conflict.insert((name.clone(), d.clone()));
1329            }
1330        }
1331    }
1332    let mut missing: Vec<(String, String)> = Vec::new();
1333    for (key, m) in &required {
1334        let Some(m) = m else { continue }; // conflicting ancestor descs
1335        if defaults.contains(key) || provided.contains(key) {
1336            continue;
1337        }
1338        // Kotlin bridge variant (return kotlin/Unit beside the real
1339        // boolean/object twin a framework interface declares): stubbing
1340        // it renders `java.lang.Void addAll(Collection)` that neither
1341        // implements the twin nor serves its callers — the poisoned
1342        // return type cascades through every call site (weibo +703
1343        // regression). The honest 未覆盖 error for the twin stays.
1344        if m.1.ends_with(")Lkotlin/Unit;") {
1345            continue;
1346        }
1347        // A chain method sharing the erasure but NOT the descriptor
1348        // (abstract super with a different return, an impostor the
1349        // rename pass left because it satisfies another ancestor):
1350        // the stub would clash with it — skip; the class keeps the
1351        // single honest 未覆盖 error instead of gaining a 无法覆盖 one.
1352        if chain_conflict.contains(&(m.0.clone(), m.1.clone())) {
1353            continue;
1354        }
1355        missing.push(m.clone());
1356    }
1357    missing.sort();
1358    let ind = "    ".repeat(depth + 1);
1359    for (mname, mdesc) in missing {
1360        let lo = mdesc.find('(').map(|i| i + 1).unwrap_or(0);
1361        let hi = mdesc.find(')').unwrap_or(mdesc.len());
1362        let ret_s = &mdesc[hi + 1..];
1363        let args_v = split_arg_descs(&mdesc[lo..hi]);
1364        let d = ParsedStub { ret: crate::desc_type(ret_s), args: args_v };
1365        if *emitted_any {
1366            out.push('\n');
1367        }
1368        out.push_str(&ind);
1369        out.push_str("public ");
1370        out.push_str(&type_name(pool, &d.ret));
1371        out.push(' ');
1372        out.push_str(&java_ident(&mname));
1373        out.push('(');
1374        for (i, a) in d.args.iter().enumerate() {
1375            if i > 0 {
1376                out.push_str(", ");
1377            }
1378            out.push_str(&type_name(pool, a));
1379            out.push_str(&format!(" p{}", i + 1));
1380        }
1381        out.push_str(") {\n");
1382        out.push_str(&ind);
1383        out.push_str("    throw new AbstractMethodError();\n");
1384        out.push_str(&ind);
1385        out.push_str("}\n");
1386        *emitted_any = true;
1387    }
1388}
1389
1390/// Synthetic storage fields backing name()/ordinal() on a FALLBACK enum
1391/// (a plain class cannot inherit java.lang.Enum's). The enum ctor's
1392/// (String, int) trace params feed them (injected in emit_method).
1393const ENUM_NAME_FIELD: &str = "$ddcName";
1394const ENUM_ORD_FIELD: &str = "$ddcOrdinal";
1395
1396/// True when the class renders as a fallback enum BASE: ACC_ENUM,
1397/// restoration failed (enum_consts None — caller-gated), super is the
1398/// Enum root. Constant subclasses inherit the synthesized members.
1399fn fallback_enum_base(class: &PoolClass) -> bool {
1400    class.is_enum()
1401        && class
1402            .super_name
1403            .as_ref()
1404            .map(|s| s == "java/lang/Enum" || s == "java/lang/Object")
1405            .unwrap_or(true)
1406}
1407
1408/// Full gate for the synthetic Enum surface: fallback base WITH the
1409/// trace ctor and no `$ddc*` field-name clash. The ctor injection in
1410/// emit_method, the member synthesis and the dex-valueOf suppression
1411/// must all agree, or fields/ctor/renders desynchronize.
1412fn enum_synth_active(class: &PoolClass) -> bool {
1413    fallback_enum_base(class)
1414        && enum_trace_ctor(class)
1415        && !class
1416            .static_fields
1417            .iter()
1418            .chain(class.instance_fields.iter())
1419            .any(|f| f.name == ENUM_NAME_FIELD || f.name == ENUM_ORD_FIELD)
1420}
1421
1422/// Does the class have the compiler-generated `(String, int, ..)` enum
1423/// ctor whose leading params feed the synthetic name/ordinal fields?
1424fn enum_trace_ctor(class: &PoolClass) -> bool {
1425    class.all_methods().any(|m| {
1426        &*m.name == "<init>"
1427            && m.parsed_desc()
1428                .map(|d| {
1429                    matches!(d.args.first(), Some(JavaType::Object(s))
1430                        if s.as_ref() == "java/lang/String")
1431                        && matches!(d.args.get(1), Some(JavaType::Int))
1432                })
1433                .unwrap_or(false)
1434    })
1435}
1436
1437/// Synthesize the java.lang.Enum member surface a FALLBACK enum base
1438/// lacks (it renders as a plain class — `extends Enum` is illegal in
1439/// source): storage fields `$ddcName`/`$ddcOrdinal` (fed by the ctor
1440/// trace-param injection in emit_method), `name()`, `ordinal()`,
1441/// `valueOf(String)` and `compareTo`, plus `values()` when R8 stripped
1442/// the dex copy. Kills the illegal dex `Enum.valueOf(X.class, ..)`
1443/// ("无法将类 Enum<E>中的方法 valueOf应用到给定类型" — weixin ×1,307,
1444/// weibo ×477) and makes ordinal() EXACT (the interim values()-scan
1445/// misread enums whose $VALUES carries register-reuse duplicates,
1446/// protobuf ub). Purely additive: the dex never declares these members
1447/// on the enum class (they are Enum inheritances), so no collision —
1448/// except the dex valueOf, suppressed at the method loop.
1449fn synth_enum_members(
1450    pool: &DexPool,
1451    class: &PoolClass,
1452    out: &mut String,
1453    depth: usize,
1454    emitted_any: &mut bool,
1455) {
1456    use crate::access::*;
1457    if !fallback_enum_base(class) {
1458        return;
1459    }
1460    let self_ty = JavaType::Object(class.name.as_str().into());
1461    let self_name = type_name(pool, &self_ty);
1462    let self_arr = JavaType::Array(Box::new(self_ty));
1463    let arr_name = type_name(pool, &self_arr);
1464    let traced = enum_synth_active(class);
1465    let has_values = class.all_methods().any(|m| {
1466        &*m.name == "values"
1467            && m.access & ACC_STATIC != 0
1468            && m.parsed_desc()
1469                .map(|d| d.ret == self_arr && d.args.is_empty())
1470                .unwrap_or(false)
1471    });
1472    let ind = "    ".repeat(depth + 1);
1473    let push_line = |out: &mut String, l: &str| {
1474        out.push_str(&ind);
1475        out.push_str(l);
1476        out.push('\n');
1477    };
1478    // values(): from the $VALUES array field when the dex copy is gone.
1479    if !has_values {
1480        let want = format!("[L{};", class.name);
1481        if let Some(vf) = class.static_fields.iter().find(|f| f.desc == want) {
1482            if *emitted_any {
1483                out.push('\n');
1484            }
1485            *emitted_any = true;
1486            push_line(out, &format!("public static {} values() {{", arr_name));
1487            push_line(
1488                out,
1489                &format!("    return ({}) {}.clone();", arr_name, java_ident(&vf.name)),
1490            );
1491            push_line(out, "}");
1492        }
1493    }
1494    if !traced {
1495        return; // no (String,int) ctor: fields would stay default — keep
1496                // the scan-based ordinal only (below) and the dex valueOf.
1497    }
1498    if *emitted_any {
1499        out.push('\n');
1500    }
1501    *emitted_any = true;
1502    push_line(out, &format!("private java.lang.String {};", ENUM_NAME_FIELD));
1503    push_line(out, &format!("private int {};", ENUM_ORD_FIELD));
1504    out.push('\n');
1505    push_line(out, &format!(
1506        "public java.lang.String name() {{\n{}    return this.{};\n{}}}",
1507        ind, ENUM_NAME_FIELD, ind
1508    ));
1509    out.push('\n');
1510    push_line(out, &format!(
1511        "public int ordinal() {{\n{}    return this.{};\n{}}}",
1512        ind, ENUM_ORD_FIELD, ind
1513    ));
1514    out.push('\n');
1515    push_line(out, &format!(
1516        "public int compareTo({0} o) {{\n{1}    return this.{2} - o.{2};\n{1}}}",
1517        self_name, ind, ENUM_ORD_FIELD
1518    ));
1519    out.push('\n');
1520    push_line(out, &format!(
1521        "public static {0} valueOf(java.lang.String s) {{\n{1}    for ({0} v : values()) {{\n{1}        if (v.name().equals(s)) {{\n{1}            return v;\n{1}        }}\n{1}    }}\n{1}    throw new java.lang.IllegalArgumentException(s);\n{1}}}",
1522        self_name, ind
1523    ));
1524}
1525
1526/// Scan-based ordinal() fallback for fallback enums WITHOUT the trace
1527/// ctor (rare): position in values(). Kept separate so synth_enum_members
1528/// can bail after values() synthesis.
1529fn synth_enum_ordinal_scan(
1530    pool: &DexPool,
1531    class: &PoolClass,
1532    out: &mut String,
1533    depth: usize,
1534    emitted_any: &mut bool,
1535) {
1536    if !fallback_enum_base(class) || enum_trace_ctor(class) {
1537        return;
1538    }
1539    if class
1540        .all_methods()
1541        .any(|m| &*m.name == "ordinal" && &*m.desc == "()I")
1542    {
1543        return;
1544    }
1545    let self_arr = JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into())));
1546    let arr_name = type_name(pool, &self_arr);
1547    let ind = "    ".repeat(depth + 1);
1548    if *emitted_any {
1549        out.push('\n');
1550    }
1551    *emitted_any = true;
1552    out.push_str(&ind);
1553    out.push_str("public int ordinal() {\n");
1554    out.push_str(&ind);
1555    out.push_str(&format!("    {} vs = values();\n", arr_name));
1556    out.push_str(&ind);
1557    out.push_str("    for (int i = 0; i < vs.length; i++) {\n");
1558    out.push_str(&ind);
1559    out.push_str("        if (vs[i] == this) {\n");
1560    out.push_str(&ind);
1561    out.push_str("            return i;\n");
1562    out.push_str(&ind);
1563    out.push_str("        }\n");
1564    out.push_str(&ind);
1565    out.push_str("    }\n");
1566    out.push_str(&ind);
1567    out.push_str("    return 0;\n");
1568    out.push_str(&ind);
1569    out.push_str("}\n");
1570}
1571
1572/// Strip the Meituan Robust hotpatch guard from a RESTORED enum ctor:
1573/// `Object[] v3 = {name, ordinal}; Class[] v4 = ..; boolean s =
1574/// PatchProxy.isSupport(v3, this, changeQuickRedirect, ..); if (s) {
1575/// PatchProxy.accessDispatch(..); return; }`. The guard reads the enum's
1576/// own static field — bytecode-legal (Robust instruments after javac),
1577/// source-illegal (JLS 8.9.2, both simple AND qualified forms). It is
1578/// dead code for decompiled output (no patch is ever loaded). Removal is
1579/// surgical: statements containing a PatchProxy call, the locals feeding
1580/// its arguments, their defs and element writes — a fixpoint over the
1581/// top-level list. collect_enum_constants' dex gate already refused
1582/// restoration when self-static reads go beyond the redirect field.
1583fn strip_robust_enum_ctor_guard(body: &mut Stmt) {
1584    let is_patchy = |e: &Expr| {
1585        matches!(e, Expr::Method { cls, name, .. }
1586            if cls.contains("PatchProxy")
1587                && (&**name == "isSupport"
1588                    || &**name == "accessDispatch"
1589                    || &**name == "proxy"
1590                    || &**name == "isSupportClinit"
1591                    || &**name == "accessDispatchClinit"))
1592    };
1593    let Stmt::Block(vs) = body else { return };
1594    if !vs.iter().any(|st| {
1595        let mut h = false;
1596        crate::passes::visit_all_exprs(st, &mut |x| {
1597            if is_patchy(x) {
1598                h = true;
1599            }
1600        });
1601        h
1602    }) {
1603        return;
1604    }
1605    let mut dead: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
1606    // Locals read inside the PatchProxy call args of a statement (the
1607    // pack arrays) — dead once the statement goes.
1608    let patchy_arg_locals = |st: &Stmt, out: &mut jdc_core::FxHashSet<u32>| {
1609        crate::passes::visit_all_exprs(st, &mut |x| {
1610            if let Expr::Method { args, .. } = x {
1611                if is_patchy(x) {
1612                    for a in args {
1613                        let mut aa = a.clone();
1614                        crate::passes::deep_rewrite(&mut aa, &mut |y| {
1615                            if let Expr::Local { var, .. } = y {
1616                                out.insert(*var);
1617                            }
1618                        });
1619                    }
1620                }
1621            }
1622        });
1623    };
1624    for _ in 0..4 {
1625        let before = vs.len();
1626        let mut i = 0usize;
1627        while i < vs.len() {
1628            let action = {
1629                let st = &vs[i];
1630                match st {
1631                    Stmt::LocalDef { var, .. } if dead.contains(var) => Some((*var, false)),
1632                    Stmt::LocalDef { var, init: Some(e), .. } => {
1633                        let mut has_patchy = false;
1634                        let mut ec = e.clone();
1635                        crate::passes::deep_rewrite(&mut ec, &mut |x| {
1636                            if is_patchy(x) {
1637                                has_patchy = true;
1638                            }
1639                        });
1640                        if has_patchy {
1641                            Some((*var, true))
1642                        } else {
1643                            None
1644                        }
1645                    }
1646                    Stmt::If { .. } => {
1647                        let mut h = false;
1648                        crate::passes::visit_all_exprs(st, &mut |x| {
1649                            if is_patchy(x) {
1650                                h = true;
1651                            }
1652                        });
1653                        if h {
1654                            Some((u32::MAX, true))
1655                        } else {
1656                            None
1657                        }
1658                    }
1659                    // `v3[0] = str;` / `v3 = ..;` writes to a dead pack.
1660                    Stmt::ExprStmt(Expr::Assign { target, .. }) => {
1661                        let mut tgt_dead = false;
1662                        let mut tc = target.clone();
1663                        crate::passes::deep_rewrite(&mut tc, &mut |x| {
1664                            if let Expr::Local { var, .. } = x {
1665                                if dead.contains(var) {
1666                                    tgt_dead = true;
1667                                }
1668                            }
1669                        });
1670                        if tgt_dead {
1671                            Some((u32::MAX, false))
1672                        } else {
1673                            None
1674                        }
1675                    }
1676                    _ => None,
1677                }
1678            };
1679            if let Some((var, collect_args)) = action {
1680                if collect_args {
1681                    patchy_arg_locals(&vs[i], &mut dead);
1682                }
1683                if var != u32::MAX {
1684                    dead.insert(var);
1685                }
1686                vs.remove(i);
1687            } else {
1688                i += 1;
1689            }
1690        }
1691        if vs.len() == before {
1692            break;
1693        }
1694    }
1695}
1696
1697#[allow(clippy::only_used_in_recursion)]
1698fn emit_class_body(
1699    pool: &DexPool,
1700    class: &PoolClass,
1701    ctx: &DexCtx<'_>,
1702    opts: &ClassOptions,
1703    out: &mut String,
1704    depth: usize,
1705) -> anyhow::Result<()> {
1706    let ind = indent(depth);
1707    // The class's own header must use the RENAMED display name — the
1708    // ctor path (is_init) already renames; a header declaring `class a`
1709    // while the file/ctor say `a_2` leaves the ctor looking like a
1710    // method with no return type (MinisApp's Y2.a vs y2.a package
1711    // case-collision: 2,518 javac parse errors).
1712    let cname = crate::apply_class_rename(&class.name);
1713    let (_, simple) = split_name(&cname);
1714    // A class emitted as its OWN top-level file (depth 0) must declare a
1715    // flat `$` name — `class Outer.Inner` is not declarable at file
1716    // scope. An INLINE nested member (depth > 0) declares its own
1717    // segment inside the parent's body.
1718    let simple = if depth == 0 {
1719        simple.to_string()
1720    } else {
1721        // R8 names can END in `$`: an empty last `$`-segment would
1722        // render `class  {` — keep the whole simple name.
1723        let seg = simple.rsplit('$').next().unwrap_or(&simple);
1724        if seg.is_empty() {
1725            simple.to_string()
1726        } else {
1727            seg.to_string()
1728        }
1729    };
1730    // A class NAMED `var`-style (taobao ships `tb.var`) cannot be
1731    // declared — restricted contextual type names escape here and in
1732    // the ctor, file name, and every type reference.
1733    let simple = if is_restricted_type_name(&simple) {
1734        format!("_{simple}")
1735    } else {
1736        simple
1737    };
1738    let is_iface = class.is_interface();
1739    let is_enum = class.is_enum();
1740
1741    let mut head = String::new();
1742    let a = class.access;
1743    if a & ACC_PUBLIC != 0 || widen_class(&class.name) {
1744        head.push_str("public ");
1745    }
1746    // Inline nested members need their `static` (interfaces/annotations
1747    // are implicitly static; a missing `static` on a member class makes
1748    // every `new Report(...)` site an "outer instance required" error).
1749    if depth > 0 && !is_iface && ctx.nested_is_static(&class.name) {
1750        head.push_str("static ");
1751    }
1752    if a & ACC_FINAL != 0 && !is_enum {
1753        head.push_str("final ");
1754    }
1755    if a & ACC_ABSTRACT != 0 && !is_iface {
1756        head.push_str("abstract ");
1757    }
1758    if a & ACC_ANNOTATION != 0 {
1759        head.push('@');
1760    }
1761    let mut enum_consts: Option<(Vec<EnumConst>, crate::method::MethodBody)> = if is_enum {
1762        collect_enum_constants(pool, class, ctx)
1763    } else {
1764        None
1765    };
1766    if let Some(ecs) = &enum_consts {
1767        // True `enum` declaration: constants render in the header, the
1768        // desugared boilerplate (const fields, their <clinit> inits —
1769        // stripped by strip_enum_const_inits — and the ACC_ENUM flags)
1770        // disappears. R8-renamed values()/valueOf() stay (they do not
1771        // collide with the compiler-generated ones); javac-named ones
1772        // are skipped at the method loop below.
1773        head.push_str("enum ");
1774        head.push_str(&sanitize_ref(&simple));
1775        let _ = ecs;
1776    } else if is_enum {
1777        // An enum with constant-specific bodies carries ACC_ABSTRACT —
1778        // `abstract final` is an illegal modifier combination; abstract
1779        // (already pushed above) suppresses the hardcoded final.
1780        let abstract_ = a & ACC_ABSTRACT != 0;
1781        head.push_str(if abstract_ {
1782            "/* enum */ class "
1783        } else {
1784            "/* enum */ final class "
1785        });
1786        head.push_str(&sanitize_ref(&simple));
1787    } else if is_iface {
1788        head.push_str("interface ");
1789        head.push_str(&sanitize_ref(&simple));
1790    } else {
1791        head.push_str("class ");
1792        head.push_str(&sanitize_ref(&simple));
1793    }
1794    // The obscured-super import was emitted at the package line; the
1795    // clause must render the SIMPLE name (the qualified form binds to
1796    // the class itself even with the import present).
1797    let render_super = |sup: &String| -> String { print_class_name(pool, sup) };
1798    if is_iface {
1799        // A @interface cannot declare extends at all (JLS 9.6): the dex
1800        // interface table lists java/lang/annotation/Annotation for
1801        // every annotation type — rendering it produced "对于
1802        // @interfaces, 不允许 'extends'" (weibo 1100).
1803        let is_annot = a & ACC_ANNOTATION != 0;
1804        if !class.interfaces.is_empty() && !is_annot {
1805            head.push_str(" extends ");
1806            head.push_str(&join_dotted(pool, &class.interfaces));
1807        }
1808    } else {
1809        if let Some(sup) = &class.super_name {
1810            // Suppress `extends` for TRUE `enum` declarations (they
1811            // implicitly extend java.lang.Enum) and for the enum BASE in
1812            // fallback form (super IS java/lang/Enum — `class X extends
1813            // Enum` is illegal). But an ENUM CONSTANT SUBCLASS rendered
1814            // in fallback (`/* enum */ class j$1`, ACC_ENUM, super = the
1815            // enum base) MUST keep `extends j`: without it the constant
1816            // loses its subtype relation and every `field = new j$1(..)`
1817            // fails ("j$1无法转换为j", weibo jsoup TokeniserState ×172).
1818            let suppress_extends =
1819                is_enum && (enum_consts.is_some() || sup == "java/lang/Enum");
1820            if sup != "java/lang/Object" && !suppress_extends {
1821                head.push_str(" extends ");
1822                head.push_str(&render_super(sup));
1823            }
1824        }
1825        if !class.interfaces.is_empty() {
1826            head.push_str(if is_iface {
1827                " extends "
1828            } else {
1829                " implements "
1830            });
1831            head.push_str(&join_dotted(pool, &class.interfaces));
1832        }
1833    }
1834
1835    out.push_str(&ind);
1836    out.push_str(&head);
1837    out.push_str(" {\n");
1838
1839    // True-enum constant list: the constants lead the body, ahead of
1840    // any remaining fields.
1841    if let Some((ecs, _)) = &enum_consts {
1842        for (i, ec) in ecs.iter().enumerate() {
1843            out.push_str(&indent(depth + 1));
1844            out.push_str(&java_ident(&ec.name));
1845            if !ec.extra_args.is_empty() {
1846                let mut line = String::from("(");
1847                render_enum_args(ctx, pool, &ec.extra_args, &mut line);
1848                line.push(')');
1849                out.push_str(&line);
1850            }
1851            if i + 1 < ecs.len() {
1852                out.push_str(",\n");
1853            } else {
1854                out.push_str(";\n");
1855            }
1856        }
1857        out.push('\n');
1858    }
1859
1860    // Fields.
1861    // A static final whose dex-encoded static value is OVERWRITTEN by
1862    // the clinit must render as a blank final: the dex encoded array
1863    // value is just the class-load default (ART lets <clinit> reassign
1864    // static finals), but Java forbids touching an initialized final
1865    // ("无法为 static final 变量 $stable 分配值" — Compose `$stable = 0`
1866    // + clinit `= 8`; WalletBaseUI HARDCODE_* = 0 + clinit computed
1867    // value; the original null-valued shape: Kotlin `object` INSTANCE,
1868    // okio SegmentPool). Scan the clinit's raw SPuts once.
1869    let clinit_sputs: Option<jdc_core::FxHashSet<(std::sync::Arc<str>, std::sync::Arc<str>)>> =
1870        // Gate: only classes with a VALUED final static can hit the
1871        // conflict — skip the clinit decode otherwise (decoding it for
1872        // every class cost seconds on 98k-class corpora).
1873        (|| {
1874            let has_valued_final = class.static_fields.iter().enumerate().any(|(i, f)| {
1875                f.access & crate::access::ACC_FINAL != 0
1876                    && class.static_values.get(i).is_some()
1877            });
1878            if !has_valued_final {
1879                return None;
1880            }
1881            let m = class.all_methods().find(|m| &*m.name == "<clinit>")?;
1882            let dex = pool.dex(m.dex_idx)?;
1883            let ci = dex.code_at(m.code_off)?;
1884            let mut set: jdc_core::FxHashSet<(
1885                std::sync::Arc<str>,
1886                std::sync::Arc<str>,
1887            )> = jdc_core::FxHashSet::default();
1888            for ins in &ci.insns {
1889                if let InsnKind::SPut { field_idx, .. } = ins.kind {
1890                    let fid = dex.field(field_idx);
1891                    // clinit only writes own-class fields here; match
1892                    // (name, type) against the field list below.
1893                    set.insert((
1894                        std::sync::Arc::from(dex.string(fid.name_idx)),
1895                        std::sync::Arc::from(dex.type_name(fid.type_idx)),
1896                    ));
1897                }
1898            }
1899            Some(set)
1900        })();
1901    let mut field_emitted = false;
1902    for (i, f) in class.static_fields.iter().enumerate() {
1903        // Enum constant fields became the header list above.
1904        if enum_consts.is_some() && f.access & crate::access::ACC_ENUM != 0 {
1905            continue;
1906        }
1907        if field_emitted || !class.instance_fields.is_empty() {
1908            out.push('\n');
1909        }
1910        field_emitted = true;
1911        // A blank-final: static final, clinit-assigned. Interfaces are
1912        // excluded — their fields are implicitly `public static final`
1913        // and MUST carry an initializer.
1914        let blank_final = f.access & crate::access::ACC_FINAL != 0
1915            && !class.is_interface()
1916            && clinit_sputs
1917                .as_ref()
1918                .is_some_and(|s| {
1919                    s.contains(&(
1920                        std::sync::Arc::from(f.name.as_str()),
1921                        std::sync::Arc::from(f.desc.as_str()),
1922                    ))
1923                });
1924        emit_field(
1925            pool,
1926            f,
1927            if blank_final {
1928                None
1929            } else {
1930                class.static_values.get(i)
1931            },
1932            &class.name,
1933            out,
1934            depth + 1,
1935            true,
1936            class.is_interface(),
1937        );
1938    }
1939    if !class.instance_fields.is_empty() && !class.static_fields.is_empty() {
1940        out.push('\n');
1941    }
1942    for (i, f) in class.instance_fields.iter().enumerate() {
1943        if i > 0 {
1944            out.push('\n');
1945        }
1946        emit_field(pool, f, None, &class.name, out, depth + 1, false, false);
1947    }
1948
1949    // Methods.
1950    // Duplicated (name, parameter-types) pairs in one class are javac
1951    // "method is already defined" errors (weibo: 11.5k hits). Source
1952    // cannot declare them, so exactly one survives: covariant bridges
1953    // (`Object get(int)` beside `ByteString get(int)` — the compiler
1954    // GENERATES bridges), plus R8 output that lost the bridge flag
1955    // (okio Buffer.clone, interface getView re-declarations). A
1956    // non-bridge method outranks a bridge for the same signature
1957    // (bridge bodies are delegation stubs); first occurrence otherwise.
1958    // Never drop a signature outright — the last method standing for a
1959    // key is always rendered.
1960    // Keyed on the DISPLAY name: a member-renamed method (return-type-
1961    // only collisions on isolated classes) no longer collides with its
1962    // sibling — both render, each caller resolving through the registry.
1963    fn sig_key<'m>(cls: &str, renames_on: bool, m: &'m PoolMethod) -> (&'m str, &'m str) {
1964        let d: &'m str = &m.desc;
1965        let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1966        let hi = d.find(')').unwrap_or(d.len());
1967        let n = if renames_on {
1968            jdc_core::rename::field_display(cls, &m.name, d).unwrap_or(&*m.name)
1969        } else {
1970            &*m.name
1971        };
1972        (n, &d[lo..hi])
1973    }
1974    let renames_on = jdc_core::rename::member_rename_active();
1975    let suppress_dex_valueof = class.is_enum()
1976        && enum_consts.is_none()
1977        && enum_synth_active(class);
1978    let methods: Vec<&PoolMethod> = class.all_methods().collect();
1979    let mut claim: jdc_core::FxHashMap<(&str, &str), usize> = jdc_core::FxHashMap::default();
1980    for (i, m) in methods.iter().enumerate() {
1981        let key = sig_key(&class.name, renames_on, m);
1982        match claim.get(&key) {
1983            Some(&j)
1984                if m.access & crate::access::ACC_BRIDGE == 0
1985                    && methods[j].access & crate::access::ACC_BRIDGE != 0 =>
1986            {
1987                claim.insert(key, i);
1988            }
1989            None => {
1990                claim.insert(key, i);
1991            }
1992            _ => {}
1993        }
1994    }
1995    // NOTE: a same-signature bridge is already retired by the claim map
1996    // above. An ERASURE-shaped bridge (SAM/variance: params are the
1997    // Object-erased types of a sibling's) must NOT be skipped: with raw
1998    // (non-generic) interface rendering the bridge is exactly what
1999    // satisfies the interface — skipping it turned every Kotlin lambda
2000    // class into "不是抽象的, 并且未覆盖…invoke(Object,Object)" (reqable
2001    // +17), while the "对invoke的引用不明确" it appeared to fix was a
2002    // missing-kotlin-classpath cascade, not a ddc bug.
2003    let mut emitted_any = !class.static_fields.is_empty() || !class.instance_fields.is_empty();
2004    for (i, m) in methods.iter().enumerate() {
2005        if &*m.name == "<clinit>" {
2006            continue; // rendered after the fields
2007        }
2008        if claim.get(&sig_key(&class.name, renames_on, m)) != Some(&i) {
2009            continue;
2010        }
2011        // Fallback enum base: the dex valueOf body is
2012        // `Enum.valueOf(X.class, str)` — illegal once X is a plain class
2013        // (T is not bounded by Enum). Suppress it; synth_enum_members
2014        // renders the values()-scan replacement.
2015        if suppress_dex_valueof
2016            && m.is_static()
2017            && &*m.name == "valueOf"
2018            && m.parsed_desc().map(|d| {
2019                d.args.len() == 1
2020                    && matches!(d.args[0], JavaType::Object(ref sn)
2021                        if sn.as_ref() == "java/lang/String")
2022                    && d.ret == JavaType::Object(class.name.as_str().into())
2023            }).unwrap_or(false)
2024        {
2025            continue;
2026        }
2027        // True-enum rendering: javac auto-generates values()/valueOf()
2028        // — the original-named ones must not re-declare (R8-renamed
2029        // copies stay, they do not collide).
2030        if let Some((ecs, _)) = &enum_consts {
2031            let d = m.parsed_desc();
2032            let self_arr = d.as_ref().map(|d| d.ret == JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into()))));
2033            let self_ret = d.as_ref().map(|d| d.ret == JavaType::Object(class.name.as_str().into()));
2034            let no_args = d.as_ref().map(|d| d.args.is_empty()).unwrap_or(false);
2035            let one_str = d
2036                .as_ref()
2037                .map(|d| d.args.len() == 1 && matches!(d.args[0], JavaType::Object(ref n) if n.as_ref() == "java/lang/String"))
2038                .unwrap_or(false);
2039            let static_ = m.is_static();
2040            if static_ && no_args && self_arr == Some(true) && &*m.name == "values" {
2041                continue;
2042            }
2043            if static_ && one_str && self_ret == Some(true) && &*m.name == "valueOf" {
2044                continue;
2045            }
2046            // Enum constructors must be private in source form.
2047            let m_owned: Option<PoolMethod> = if &*m.name == "<init>" {
2048                let mut c = (*m).clone();
2049                c.access = (c.access & !(crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED | crate::access::ACC_PRIVATE)) | crate::access::ACC_PRIVATE;
2050                Some(c)
2051            } else {
2052                None
2053            };
2054            let m_ref: &PoolMethod = m_owned.as_ref().unwrap_or(m);
2055            let mark = out.len();
2056            if emitted_any {
2057                out.push('\n');
2058            }
2059            if emit_method(pool, class, ctx, m_ref, depth + 1, true, out)? {
2060                emitted_any = true;
2061            } else {
2062                out.truncate(mark);
2063            }
2064            let _ = ecs;
2065            continue;
2066        }
2067        let mark = out.len();
2068        if emitted_any {
2069            out.push('\n');
2070        }
2071        if emit_method(pool, class, ctx, m, depth + 1, enum_consts.is_some(), out)? {
2072            emitted_any = true;
2073        } else {
2074            out.truncate(mark);
2075        }
2076    }
2077    // Inherited-ctor bridges: dex method refs resolve through the
2078    // hierarchy, so `new C(args)` against a class C with NO declared
2079    // <init> legally targets the SUPERCLASS ctor (weixin tenpay
2080    // `new m(map)` — m declares nothing, i.<init>(HashMap) does). Java
2081    // has no inherited constructors: without a bridge, C's implicit
2082    // default ctor is the only one and every arg-carrying construction
2083    // fails ("无法将类 m中的构造器 m应用到给定类型; 需要: 没有参数").
2084    // Mirror the nearest ctor-declaring ancestor's public/protected
2085    // ctors as thin `super(..)` delegations.
2086    if !class.is_interface()
2087        && enum_consts.is_none()
2088        && !class.all_methods().any(|m| &*m.name == "<init>")
2089    {
2090        if let Some(mut sup) = class.super_name.clone() {
2091            loop {
2092                if sup == "java/lang/Object" {
2093                    break;
2094                }
2095                let Some(sc) = pool.get(&sup) else { break };
2096                let ctors: Vec<&PoolMethod> = sc
2097                    .all_methods()
2098                    .filter(|m| &*m.name == "<init>")
2099                    .collect();
2100                if !ctors.is_empty() {
2101                    for sm in ctors {
2102                        if sm.access & crate::access::ACC_PRIVATE != 0
2103                            || (sm.access
2104                                & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
2105                                == 0)
2106                        {
2107                            continue; // private/package-private: no legal bridge
2108                        }
2109                        let Some(d) = sm.parsed_desc() else { continue };
2110                        let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
2111                            "public "
2112                        } else {
2113                            "protected "
2114                        };
2115                        if emitted_any {
2116                            out.push('\n');
2117                        }
2118                        out.push_str(&format!("    {}", "    ".repeat(depth)));
2119                        out.push_str(mods);
2120                        out.push_str(&sanitize_ref(&simple));
2121                        out.push('(');
2122                        let mut names = Vec::with_capacity(d.args.len());
2123                        for (i, a) in d.args.iter().enumerate() {
2124                            if i > 0 {
2125                                out.push_str(", ");
2126                            }
2127                            out.push_str(&type_name(pool, a));
2128                            let nm = format!("p{}", i + 1);
2129                            out.push(' ');
2130                            out.push_str(&nm);
2131                            names.push(nm);
2132                        }
2133                        out.push_str(") {\n");
2134                        out.push_str(&format!("    {}    super({});\n", "    ".repeat(depth), names.join(", ")));
2135                        out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2136                        emitted_any = true;
2137                    }
2138                    break;
2139                }
2140                match &sc.super_name {
2141                    Some(n) => sup = n.clone(),
2142                    None => break,
2143                }
2144            }
2145        }
2146    }
2147
2148    // Referenced-but-undeclared inherited-ctor bridges. The block above
2149    // covers a class with NO declared ctor; this covers a class that
2150    // DECLARES ctors but is missing one that dex callers reach THROUGH
2151    // it. Dex resolves a `<init>` method-ref up the superclass chain, so
2152    // a subclass `super(2, recv, owner, name, sig, flags)` may target
2153    // `m.<init>(int,Object,Class,String,String,int)` that `m`
2154    // (FunctionReferenceImpl) does NOT declare — its super `l` does.
2155    // Java has no inherited ctors, so without a bridge on `m` every such
2156    // subclass fails ("无法将类 m中的构造器 m应用到给定类型"). Mirror each
2157    // referenced sig C lacks from the nearest ancestor declaring it, as a
2158    // thin `super(..)` delegation. Ref-gated (only sigs actually called
2159    // through C) so ordinary classes gain nothing.
2160    if !class.is_interface()
2161        && enum_consts.is_none()
2162        && class.all_methods().any(|m| &*m.name == "<init>")
2163    {
2164        if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
2165            fn arg_sig(d: &str) -> &str {
2166                let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2167                let hi = d.find(')').unwrap_or(d.len());
2168                &d[lo..hi]
2169            }
2170            let declared: jdc_core::FxHashSet<&str> = class
2171                .all_methods()
2172                .filter(|m| &*m.name == "<init>")
2173                .map(|m| arg_sig(&m.desc))
2174                .collect();
2175            let mut missing: Vec<&String> = refs
2176                .iter()
2177                .filter(|s| !declared.contains(s.as_str()))
2178                .collect();
2179            missing.sort();
2180            for sig in missing {
2181                // Nearest ancestor declaring a public/protected <init>
2182                // with this exact arg signature.
2183                let mut sup = class.super_name.clone();
2184                let mut found: Option<&PoolMethod> = None;
2185                while let Some(s) = sup {
2186                    if s == "java/lang/Object" {
2187                        break;
2188                    }
2189                    let Some(sc) = pool.get(&s) else { break };
2190                    if let Some(sm) = sc.all_methods().find(|m| {
2191                        &*m.name == "<init>"
2192                            && arg_sig(&m.desc) == sig.as_str()
2193                            && m.access & crate::access::ACC_PRIVATE == 0
2194                            && m.access
2195                                & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
2196                                != 0
2197                    }) {
2198                        found = Some(sm);
2199                        break;
2200                    }
2201                    sup = sc.super_name.clone();
2202                }
2203                let Some(sm) = found else {
2204                    // The walk terminated at java/lang/Object — which
2205                    // always declares <init>()V, so an empty-sig ref
2206                    // through this class resolved there dex-wise. The
2207                    // Java implicit default ctor is gone (the class
2208                    // declares other ctors); mirror it as an explicit
2209                    // public bridge (AgentMenuTask$Request →
2210                    // AppBrandProxyUIProcessTask$ProcessRequest:
2211                    // "需要: Parcel 找到: 没有参数", weixin 205).
2212                    if sig.is_empty() {
2213                        if emitted_any {
2214                            out.push('\n');
2215                        }
2216                        out.push_str(&format!("    {}", "    ".repeat(depth)));
2217                        out.push_str("public ");
2218                        out.push_str(&sanitize_ref(&simple));
2219                        out.push_str("() {\n");
2220                        out.push_str(&format!(
2221                            "    {}    super();\n",
2222                            "    ".repeat(depth)
2223                        ));
2224                        out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2225                        emitted_any = true;
2226                    }
2227                    continue;
2228                };
2229                let Some(d) = sm.parsed_desc() else { continue };
2230                let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
2231                    "public "
2232                } else {
2233                    "protected "
2234                };
2235                if emitted_any {
2236                    out.push('\n');
2237                }
2238                out.push_str(&format!("    {}", "    ".repeat(depth)));
2239                out.push_str(mods);
2240                out.push_str(&sanitize_ref(&simple));
2241                out.push('(');
2242                let mut names = Vec::with_capacity(d.args.len());
2243                for (i, a) in d.args.iter().enumerate() {
2244                    if i > 0 {
2245                        out.push_str(", ");
2246                    }
2247                    out.push_str(&type_name(pool, a));
2248                    let nm = format!("p{}", i + 1);
2249                    out.push(' ');
2250                    out.push_str(&nm);
2251                    names.push(nm);
2252                }
2253                out.push_str(") {\n");
2254                out.push_str(&format!(
2255                    "    {}    super({});\n",
2256                    "    ".repeat(depth),
2257                    names.join(", ")
2258                ));
2259                out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2260                emitted_any = true;
2261            }
2262        }
2263    }
2264
2265    // Framework-ancestor ctor bridge. The two blocks above mirror POOL
2266    // ancestors; both stop at a framework (off-pool) superclass. A class
2267    // whose DIRECT superclass is a framework type (in android.jar) that
2268    // declares a ctor the class lacks also needs a bridge — `super(args)`
2269    // resolves against the jar. Only the DIRECT-super-is-framework case is
2270    // safe: an intermediate pool ancestor with no matching ctor would
2271    // break the `super()` chain (that ancestor needs its own bridge, which
2272    // its own refs may not trigger). Covers weixin g3 extends
2273    // org.xml.sax.SAXException (`new g3(String)` → SAXException(String)),
2274    // the custom-exception / Parcelable family the pool walk can't reach.
2275    if !class.is_interface() && enum_consts.is_none() {
2276        let direct_fw = class
2277            .super_name
2278            .as_ref()
2279            .map(|s| s != "java/lang/Object" && pool.get(s).is_none())
2280            .unwrap_or(false);
2281        if direct_fw {
2282            if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
2283                fn arg_sig(d: &str) -> &str {
2284                    let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2285                    let hi = d.find(')').unwrap_or(d.len());
2286                    &d[lo..hi]
2287                }
2288                let declared: jdc_core::FxHashSet<&str> = class
2289                    .all_methods()
2290                    .filter(|m| &*m.name == "<init>")
2291                    .map(|m| arg_sig(&m.desc))
2292                    .collect();
2293                let mut missing: Vec<&String> = refs
2294                    .iter()
2295                    .filter(|s| !declared.contains(s.as_str()))
2296                    .collect();
2297                missing.sort();
2298                for sig in missing {
2299                    // Never bridge the empty sig here: a no-ctor class
2300                    // gets Java's implicit default, and a class WITH
2301                    // ctors is covered by the referenced-but-undeclared
2302                    // block's Object-walk tail (emitting in both was
2303                    // `已在类 b0中定义了构造器 b0()` ×6 weixin).
2304                    if sig.is_empty() {
2305                        continue;
2306                    }
2307                    let args = split_arg_descs(sig);
2308                    if emitted_any {
2309                        out.push('\n');
2310                    }
2311                    out.push_str(&format!("    {}", "    ".repeat(depth)));
2312                    out.push_str("public ");
2313                    out.push_str(&sanitize_ref(&simple));
2314                    out.push('(');
2315                    let mut names = Vec::with_capacity(args.len());
2316                    for (i, a) in args.iter().enumerate() {
2317                        if i > 0 {
2318                            out.push_str(", ");
2319                        }
2320                        out.push_str(&type_name(pool, a));
2321                        let nm = format!("p{}", i + 1);
2322                        out.push(' ');
2323                        out.push_str(&nm);
2324                        names.push(nm);
2325                    }
2326                    out.push_str(") {\n");
2327                    out.push_str(&format!(
2328                        "    {}    super({});\n",
2329                        "    ".repeat(depth),
2330                        names.join(", ")
2331                    ));
2332                    out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2333                    emitted_any = true;
2334                }
2335            }
2336        }
2337    }
2338
2339    // Throwable-family ctor bridge for empty-shell exception classes
2340    // (see throwable_ctor_sigs). Walks the pool chain to the nearest
2341    // FRAMEWORK ancestor, stopping at any pool ancestor that DECLARES
2342    // ctors (the mirror block above covers that shape) — so chains of
2343    // empty shells (C extends P extends Error) each get the bridges and
2344    // every super(..) resolves against its parent's bridge.
2345    if !class.is_interface()
2346        && enum_consts.is_none()
2347        && !class.all_methods().any(|m| &*m.name == "<init>")
2348    {
2349        let mut fw_anc: Option<&str> = None;
2350        if let Some(sup) = class.super_name.as_deref() {
2351            let mut cur = sup;
2352            let mut hops = 0;
2353            loop {
2354                hops += 1;
2355                if hops > 32 || cur == "java/lang/Object" {
2356                    break;
2357                }
2358                match pool.get(cur) {
2359                    Some(sc) => {
2360                        if sc.all_methods().any(|m| &*m.name == "<init>") {
2361                            break;
2362                        }
2363                        match sc.super_name.as_deref() {
2364                            Some(n) => cur = n,
2365                            None => break,
2366                        }
2367                    }
2368                    None => {
2369                        fw_anc = Some(cur);
2370                        break;
2371                    }
2372                }
2373            }
2374        }
2375        if let Some(sigs) = fw_anc.and_then(throwable_ctor_sigs) {
2376            // The framework-ancestor bridge above already emitted every
2377            // sig the dex method-ref table attributes to this class —
2378            // emitting the table copy too was `已在类 z中定义了构造器
2379            // z(String)` (+60 weixin dup-def). The empty sig is exempt
2380            // there, so the table keeps it.
2381            let ref_sigs = pool.ctor_ref_sigs(&class.name);
2382            for sig in sigs {
2383                if !sig.is_empty()
2384                    && ref_sigs.is_some_and(|rs| rs.contains(*sig))
2385                {
2386                    continue;
2387                }
2388                let args = split_arg_descs(sig);
2389                if emitted_any {
2390                    out.push('\n');
2391                }
2392                out.push_str(&format!("    {}", "    ".repeat(depth)));
2393                out.push_str("public ");
2394                out.push_str(&sanitize_ref(&simple));
2395                out.push('(');
2396                let mut names = Vec::with_capacity(args.len());
2397                for (i, a) in args.iter().enumerate() {
2398                    if i > 0 {
2399                        out.push_str(", ");
2400                    }
2401                    out.push_str(&type_name(pool, a));
2402                    let nm = format!("p{}", i + 1);
2403                    out.push(' ');
2404                    out.push_str(&nm);
2405                    names.push(nm);
2406                }
2407                out.push_str(") {\n");
2408                out.push_str(&format!(
2409                    "    {}    super({});\n",
2410                    "    ".repeat(depth),
2411                    names.join(", ")
2412                ));
2413                out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2414                emitted_any = true;
2415            }
2416        }
2417    }
2418
2419    // Static resolver helpers extracted from branched-delegation ctors
2420    // (passes::extract_branched_delegation_helper) — the computation the
2421    // Java first-statement rule cannot host inline.
2422    for h in crate::passes::take_ctor_helpers(&class.name) {
2423        if emitted_any {
2424            out.push('\n');
2425        }
2426        out.push_str(&indent(depth + 1));
2427        out.push_str("private static ");
2428        out.push_str(&type_name(pool, &h.ret));
2429        out.push(' ');
2430        out.push_str(&h.name);
2431        out.push('(');
2432        for (i, (ty, nm)) in h.params.iter().enumerate() {
2433            if i > 0 {
2434                out.push_str(", ");
2435            }
2436            out.push_str(&type_name(pool, ty));
2437            out.push(' ');
2438            out.push_str(&java_ident(nm));
2439        }
2440        out.push_str(") {\n");
2441        let p = Printer::new(ctx, &h.vt);
2442        let text = p.with_indent(depth + 2).into_string(&h.body);
2443        for line in text.lines() {
2444            if line.trim().is_empty() {
2445                continue;
2446            }
2447            out.push_str(line);
2448            out.push('\n');
2449        }
2450        out.push_str(&indent(depth + 1));
2451        out.push_str("}\n");
2452        emitted_any = true;
2453    }
2454
2455    // Missing-abstract-method stubs (R8 tree-shook an interface method a
2456    // concrete class no longer implements; javac rejects the incomplete
2457    // class). Synthesized last so they sit after the real members.
2458    synth_missing_interface_stubs(pool, class, out, depth, &mut emitted_any);
2459
2460    // Fallback enum base: recover the Enum member surface the
2461    // plain-class rendering lacks (name/ordinal/valueOf/compareTo +
2462    // synthetic storage fields fed by the ctor injection).
2463    if class.is_enum() && enum_consts.is_none() {
2464        synth_enum_members(pool, class, out, depth, &mut emitted_any);
2465        synth_enum_ordinal_scan(pool, class, out, depth, &mut emitted_any);
2466    }
2467
2468    // Static initializer. INTERFACES cannot carry a `static { }` block in
2469    // Java — their clinit only assigns constants, which static_values (or
2470    // the `= null` default) already render as field initializers; skip
2471    // the block entirely.
2472    let skip_clinit = class.is_interface();
2473    if let Some((_, clinit_body)) = enum_consts.take() {
2474        // True-enum <clinit>: the constant assignments are already gone;
2475        // render the remainder (the $VALUES array build) directly.
2476        // Skip an empty remainder (all statements were constant inits).
2477        let empty = match &clinit_body.body {
2478            Stmt::Block(v) => v.iter().all(|s| matches!(s, Stmt::Block(b) if b.is_empty())),
2479            _ => false,
2480        };
2481        if !empty {
2482            if emitted_any {
2483                out.push('\n');
2484            }
2485            out.push_str(&indent(depth + 1));
2486            out.push_str("static {\n");
2487            let p = Printer::new(ctx, &clinit_body.vt);
2488            let text = p.with_indent(depth + 2).into_string(&clinit_body.body);
2489            for line in text.lines() {
2490                if line.trim().is_empty() {
2491                    continue;
2492                }
2493                out.push_str(line);
2494                out.push('\n');
2495            }
2496            out.push_str(&indent(depth + 1));
2497            out.push_str("}\n");
2498            emitted_any = true;
2499        }
2500    } else if let Some(clinit) = (!skip_clinit)
2501        .then(|| class.all_methods().find(|m| &*m.name == "<clinit>"))
2502        .flatten()
2503    {
2504        let mark = out.len();
2505        if emitted_any {
2506            out.push('\n');
2507        }
2508        if emit_method(pool, class, ctx, clinit, depth + 1, false, out)? {
2509            emitted_any = true;
2510        } else {
2511            out.truncate(mark);
2512        }
2513    }
2514
2515    // Nested member classes (clean `$` tails only; anonymous/local/lambda
2516    // classes are emitted as their own top-level files by the driver).
2517    for nested in nested_members(pool, class, ctx) {
2518        if emitted_any {
2519            out.push('\n');
2520        }
2521        let nested_ctx = DexCtx::new(pool, nested);
2522        out.push('\n');
2523        emit_class_body(pool, nested, &nested_ctx, opts, out, depth + 1)?;
2524        emitted_any = true;
2525    }
2526
2527    out.push_str(&ind);
2528    out.push_str("}\n");
2529    Ok(())
2530}
2531
2532/// Member classes of `class` (direct children by the `$` chain / member
2533/// annotations), excluding anonymous / local / lambda shapes.
2534fn nested_members<'a>(
2535    pool: &'a DexPool,
2536    class: &'a PoolClass,
2537    ctx: &DexCtx<'_>,
2538) -> Vec<&'a PoolClass> {
2539    let mut out = Vec::new();
2540    for name in pool.children_of(&class.name) {
2541        let Some(pc) = pool.get(name) else { continue };
2542        // Only a REAL `outer$tail` name is an inline member: the
2543        // children index also carries annotation-derived outers
2544        // (EnclosingClass) with no naming relationship to the child
2545        // (obfuscated apps pair a 1-char name with a long enclosing
2546        // descriptor) — the blind slice panicked there (alipay
2547        // `a.a.a.a.c`, exposed once children_of actually returned data).
2548        let Some(rest) = name
2549            .strip_prefix(class.name.as_str())
2550            .and_then(|t| t.strip_prefix('$'))
2551        else {
2552            continue;
2553        };
2554        if rest.is_empty() || rest.starts_with('-') {
2555            continue;
2556        }
2557        if ctx.find_outer(name).as_deref() != Some(class.name.as_str()) {
2558            continue;
2559        }
2560        let tail = rest.rsplit('$').next().unwrap_or(rest);
2561        if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
2562            continue; // anonymous
2563        }
2564        if tail.starts_with(|c: char| c.is_ascii_digit()) {
2565            continue; // local
2566        }
2567        out.push(pc);
2568    }
2569    out
2570}
2571
2572// Eight parameters are all load-bearing (pool, field, static value,
2573// owner class, output, depth, staticness, interface-init requirement);
2574// bundling them into a struct would obscure each call site.
2575#[allow(clippy::too_many_arguments)]
2576fn emit_field(
2577    pool: &DexPool,
2578    f: &crate::PoolField,
2579    init: Option<&StaticValue>,
2580    f_class: &str,
2581    out: &mut String,
2582    depth: usize,
2583    is_static: bool,
2584    require_init: bool,
2585) {
2586    let ind = indent(depth);
2587    let mut line = String::new();
2588    let a = f.access;
2589    if a & ACC_PUBLIC != 0 || widen_field(f_class, &f.name) {
2590        line.push_str("public ");
2591    } else if a & ACC_PRIVATE != 0 {
2592        line.push_str("private ");
2593    } else if a & ACC_PROTECTED != 0 {
2594        line.push_str("protected ");
2595    }
2596    if is_static {
2597        line.push_str("static ");
2598    }
2599    if a & ACC_FINAL != 0 && !unfinal_field(f_class, &f.name) {
2600        line.push_str("final ");
2601    }
2602    if a & ACC_SYNTHETIC != 0 {
2603        line.push_str("/* synthetic */ ");
2604    }
2605    if a & ACC_TRANSIENT_HINT != 0 {
2606        line.push_str("transient ");
2607    }
2608    if a & ACC_VOLATILE_HINT != 0 {
2609        line.push_str("volatile ");
2610    }
2611    let ty = type_name(pool, &desc_type(&f.desc));
2612    line.push_str(&ty);
2613    line.push(' ');
2614    let fname = jdc_core::rename::field_display(f_class, &f.name, &f.desc).unwrap_or(&*f.name);
2615    line.push_str(&java_ident(fname));
2616    let mut rendered = None;
2617    if let Some(v) = init {
2618        rendered = render_static_value(pool, v, f_class);
2619    }
2620    if rendered.is_none() && require_init {
2621        // Interface fields MUST have an initializer in Java; the dex may
2622        // not carry a static_values entry for a compile-time-constant the
2623        // compiler folded away. Keep it compilable.
2624        let default = match desc_type(&f.desc) {
2625            JavaType::Boolean => "false",
2626            JavaType::Byte | JavaType::Short | JavaType::Char | JavaType::Int => "0",
2627            JavaType::Long => "0L",
2628            JavaType::Float => "0.0F",
2629            JavaType::Double => "0.0",
2630            _ => "null",
2631        };
2632        rendered = Some(default.to_string());
2633    }
2634    if let Some(text) = rendered {
2635        // A long field holding an `Int(i64)` static value needs the `L`
2636        // suffix: without it the literal is an int and overflows
2637        // (`long d = -6343169151696340687` failed javac).
2638        let text = if matches!(desc_type(&f.desc), JavaType::Long)
2639            && text.chars().all(|c| c.is_ascii_digit() || c == '-')
2640        {
2641            format!("{text}L")
2642        } else {
2643            text
2644        };
2645        line.push_str(" = ");
2646        line.push_str(&text);
2647    }
2648    line.push(';');
2649    out.push_str(&ind);
2650    out.push_str(&line);
2651    out.push('\n');
2652}
2653
2654// DEX field access_flags bits 0x40/0x80 are bridge/varargs for METHODS and
2655// volatile(0x40)/transient(0x80) for fields.
2656pub const ACC_VOLATILE_HINT: u32 = 0x40;
2657pub const ACC_TRANSIENT_HINT: u32 = 0x80;
2658
2659fn render_static_value(pool: &DexPool, v: &StaticValue, owner: &str) -> Option<String> {
2660    Some(match v {
2661        StaticValue::Int(i) => i.to_string(),
2662        StaticValue::Float(f) => format_float(*f as f64, true),
2663        StaticValue::Double(d) => format_float(*d, false),
2664        StaticValue::Str(s) => format!("\"{}\"", escape_string(s)),
2665        StaticValue::Type(t) => format!("{}.class", dotted(t)),
2666        StaticValue::Boolean(b) => b.to_string(),
2667        StaticValue::Null => "null".into(),
2668        StaticValue::Field(cls, name, desc) => {
2669            // The rename registry owns the display name (obscuring-field
2670            // renames key on (owner, name, desc)) — a const field ref must
2671            // render the SAME name as the declaration.
2672            let n: std::borrow::Cow<str> = match jdc_core::rename::field_display(cls, name, desc) {
2673                Some(d) => std::borrow::Cow::Borrowed(d),
2674                None => java_ident(name),
2675            };
2676            if cls == owner {
2677                n.into_owned()
2678            } else {
2679                format!("{}.{}", print_class_name(pool, cls), n)
2680            }
2681        }
2682        StaticValue::Other => return None,
2683    })
2684}
2685
2686/// Render one method straight into the class buffer `out`. Returns
2687/// whether anything was written (skips: no descriptor, deferred to a
2688/// monitored thread). The old shape returned a per-method String that
2689/// the caller copied in — one extra full copy of every method body.
2690/// `enum_promoted`: the class rendered as a true `enum` declaration
2691/// (constants in the header), which changes what a ctor signature may
2692/// declare.
2693fn emit_method(
2694    pool: &DexPool,
2695    class: &PoolClass,
2696    ctx: &DexCtx<'_>,
2697    m: &PoolMethod,
2698    depth: usize,
2699    enum_promoted: bool,
2700    out: &mut String,
2701) -> anyhow::Result<bool> {
2702    let ind = indent(depth);
2703    let desc = m.parsed_desc();
2704
2705    // Signature.
2706    let mut sig = String::with_capacity(192);
2707    let a = m.access;
2708    if a & ACC_PUBLIC != 0 || widen_method(&class.name, &m.name, &m.desc) {
2709        sig.push_str("public ");
2710    } else if a & ACC_PRIVATE != 0 {
2711        // A fallback enum BASE (`/* enum */ class`, not a promoted
2712        // `enum`) renders its constant-specific-body subclasses as
2713        // SEPARATE same-package classes (`j$6 extends j`); their ctors
2714        // call `super(name, ordinal, ..)`, which a PRIVATE base ctor
2715        // forbids ("j(String,int) has private access"). Widen the base
2716        // ctor to package-private so the subclass links. The base is
2717        // abstract → no external instantiation risk. Only the BASE
2718        // (super is Enum/Object), not the constant subclass itself.
2719        let fallback_enum_base_ctor = class.is_enum()
2720            && !enum_promoted
2721            && &*m.name == "<init>"
2722            && class
2723                .super_name
2724                .as_ref()
2725                .map(|s| s == "java/lang/Enum" || s == "java/lang/Object")
2726                .unwrap_or(true);
2727        if !fallback_enum_base_ctor {
2728            sig.push_str("private ");
2729        }
2730    } else if a & ACC_PROTECTED != 0 {
2731        sig.push_str("protected ");
2732    }
2733    let is_clinit = &*m.name == "<clinit>";
2734    let is_init = &*m.name == "<init>";
2735    if a & ACC_STATIC != 0 || is_clinit {
2736        sig.push_str("static ");
2737    }
2738    if a & ACC_FINAL != 0 {
2739        sig.push_str("final ");
2740    }
2741    // `abstract synchronized` is an illegal combination — obfuscated
2742    // builds mark abstract bridges synchronized (WhatsApp
2743    // SQLiteOpenHelper).
2744    if a & (ACC_SYNCHRONIZED | ACC_DECLARED_SYNCHRONIZED) != 0 && a & ACC_ABSTRACT == 0 {
2745        sig.push_str("synchronized ");
2746    }
2747    if a & ACC_NATIVE != 0 {
2748        sig.push_str("native ");
2749    }
2750    if a & ACC_ABSTRACT != 0 {
2751        sig.push_str("abstract ");
2752    }
2753    if a & ACC_SYNTHETIC != 0 {
2754        sig.push_str("/* synthetic */ ");
2755    }
2756
2757    // Body (needed for parameter names even for abstract methods).
2758    let mut body = decompile_method(pool, class, m).ok().flatten();
2759    // An interface method WITH a body is a `default` method (JLS 9.4.3)
2760    // unless static/private — dex carries no `default` flag, so the
2761    // plain form rendered an abstract signature with a body and javac
2762    // rejected every one ("接口抽象方法不能带有主体", weixin 1138). A
2763    // default REQUIRES a body: only push it once the body is confirmed
2764    // (a failed decompile renders a body-less declaration).
2765    if class.is_interface()
2766        && a & (ACC_STATIC | ACC_ABSTRACT | ACC_NATIVE | ACC_PRIVATE | ACC_ANNOTATION) == 0
2767        && body.is_some()
2768    {
2769        sig.insert_str(0, "default ");
2770    }
2771    let param_names: Vec<String> = body
2772        .as_ref()
2773        .map(|b| {
2774            let mut ps: Vec<(u16, String)> =
2775                b.vt.vars
2776                    .iter()
2777                    .filter(|v| v.is_param && v.name != "this")
2778                    .map(|v| (v.slot, v.name.clone()))
2779                    .collect();
2780            ps.sort_by_key(|(s, _)| *s);
2781            ps.into_iter().map(|(_, n)| n).collect()
2782        })
2783        .unwrap_or_else(|| {
2784            (0..desc.as_ref().map(|d| d.args.len()).unwrap_or(0))
2785                .map(|i| format!("p{}", i + 1))
2786                .collect()
2787        });
2788
2789    // Non-static member-inner ctor: the synthetic outer instance rides
2790    // as args[0] (typed as the direct enclosing class). Every emission
2791    // site already passes it implicitly — qualified `outer.new Inner(..)`,
2792    // `this.new Inner(..)` from inside the outer, `super(..)` after
2793    // skip_outer_arg — so the signature drops the param and the body's
2794    // references become `Outer.this`. Without this, every construction
2795    // site fails javac arity ("无法将类…构造器…应用到给定类型" — the
2796    // guava androidx inner-class families, d8 capture lambdas).
2797    let mut inner_arg0 = 0usize;
2798    if is_init {
2799        let tail = class.name.rsplit('$').next().unwrap_or("");
2800        let digit_simple = !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit());
2801        if let Some(d) = desc.as_ref() {
2802            if let Some(JavaType::Object(outer)) = d.args.first() {
2803                // The direct enclosing class: nesting annotation when
2804                // present, else the `$`-chain parent (find_outer_name —
2805                // d8 lambdas are `Outer$$ExternalSyntheticLambdaN`, where
2806                // a plain rsplit leaves a trailing `$`). It must agree
2807                // with the this$0 type for the rewrite to fire.
2808                let enclosing: Option<String> = class
2809                    .nesting
2810                    .enclosing_class
2811                    .clone()
2812                    .or_else(|| crate::find_outer_name(pool, &class.name));
2813                let direct = enclosing.as_deref() == Some(outer.as_ref());
2814                if direct && !digit_simple && ctx.class_has_this0(&class.name) {
2815                    let param0 = body.as_ref().and_then(|b| {
2816                        b.vt.vars
2817                            .iter()
2818                            .find(|v| v.is_param && v.name != "this")
2819                            .map(|v| v.id)
2820                    });
2821                    let written = match (&body, param0) {
2822                        (Some(b), Some(p0)) => crate::passes::local_is_written(&b.body, p0),
2823                        _ => false,
2824                    };
2825                    if let (Some(p0), false) = (param0, written) {
2826                        if let Some(b) = body.as_mut() {
2827                            // this()-delegations to the SAME class drop the
2828                            // outer arg (all of its ctors share the strip);
2829                            // a super target joins when it is an inner of
2830                            // the same enclosing family.
2831                            let mut eligible: Vec<String> = vec![class.name.clone()];
2832                            if let Some(sup) = &class.super_name {
2833                                let sup_enclosing =
2834                                    crate::find_outer_name(pool, sup);
2835                                if sup_enclosing.as_deref() == Some(outer.as_ref())
2836                                    && ctx.class_has_this0(sup)
2837                                {
2838                                    eligible.push(sup.clone());
2839                                }
2840                            }
2841                            let renamed = crate::apply_class_rename(outer);
2842                            let display = dotted(renamed.as_ref());
2843                            let ty =
2844                                jdc_core::ir::expr::TypeRef::J(JavaType::Object(outer.clone()));
2845                            crate::passes::rewrite_inner_ctor_outer_param(
2846                                &mut b.body,
2847                                p0,
2848                                &display,
2849                                &ty,
2850                                &eligible,
2851                            );
2852                            inner_arg0 = 1;
2853                        }
2854                    }
2855                }
2856            }
2857        }
2858    }
2859
2860    if is_clinit {
2861        // `static { ... }` — the caller strips the method name/params.
2862    } else {
2863        let Some(d) = &desc else { return Ok(false) };
2864        if is_init {
2865            // The ctor name must equal the DECLARED class name of its
2866            // file: flat `$` at depth 0 (own file), own segment when
2867            // inlined in the parent at depth > 0. Case-renamed classes
2868            // use their display name.
2869            let dname = crate::apply_class_rename(&class.name);
2870            let (_, simple) = split_name(&dname);
2871            // emit_method's depth is the METHOD indent = class depth + 1:
2872            // own-file classes (depth 0 header → method depth 1) need the
2873            // flat `$` ctor name; inline nested members use their segment.
2874            let base = if depth <= 1 {
2875                simple.to_string()
2876            } else {
2877                // R8 names can END in `$` (`ThreadMsg$$$`): the last
2878                // `$`-segment is empty — keep the whole simple name.
2879                let seg = simple.rsplit('$').next().unwrap_or(&simple);
2880                if seg.is_empty() {
2881                    simple.to_string()
2882                } else {
2883                    seg.to_string()
2884                }
2885            };
2886            let name = if is_restricted_type_name(&base) {
2887                format!("_{base}")
2888            } else {
2889                base
2890            };
2891            // The ctor name must equal the DECLARED class name — the same
2892            // injective `_u<hex>` sanitizer the header uses (java_ident's
2893            // lossy `-`→`_` fold left `class _u2dDeprecatedOkio` with a
2894            // `private _DeprecatedOkio()` ctor: "方法声明无效; 需要返回
2895            // 类型", which as a PARSE error also masks every semantic
2896            // error in the whole javac run).
2897            sig.push_str(&sanitize_ref(&name));
2898        } else {
2899            sig.push_str(&type_name(pool, &d.ret));
2900            sig.push(' ');
2901            let mname =
2902                jdc_core::rename::field_display(&class.name, &m.name, &m.desc).unwrap_or(&*m.name);
2903            sig.push_str(&java_ident(mname));
2904        }
2905        sig.push('(');
2906        // A promoted enum ctor's dex descriptor carries the compiler-
2907        // synthesized `(String name, int ordinal)` prefix — JLS forbids
2908        // declaring those (they are implicit in the `A(args)` constant
2909        // declarations the promotion emits, and strip_enum_ctor_super
2910        // already removed the `super(name, ordinal, ..)` delegation).
2911        // Skip the pair in the signature, or every constant declaration
2912        // fails javac arity ("无法将枚举…构造器…应用到给定类型"). Only
2913        // when the body never reads the two params — code that genuinely
2914        // uses them keeps the declared form.
2915        // Robust hotpatch guard strip MUST run before the trace-param
2916        // analysis: the guard's isSupport(..) call is the ctor's only
2917        // reader of the (String, int) params for plain robust enums, and
2918        // its self-static read is what JLS 8.9.2 forbids.
2919        if enum_promoted && is_init {
2920            if let Some(b) = body.as_mut() {
2921                strip_robust_enum_ctor_guard(&mut b.body);
2922            }
2923        }
2924        // Fallback enum base ctor: store the (String, int) trace params
2925        // into the synthetic $ddcName/$ddcOrdinal fields that back the
2926        // synthesized name()/ordinal()/valueOf() (synth_enum_members).
2927        // Non-final fields: constant-subclass ctors reach this store
2928        // through their super(..) chain, and a `final` would demand
2929        // definite assignment on every ctor path.
2930        if is_init
2931            && !enum_promoted
2932            && class.is_enum()
2933            && enum_synth_active(class)
2934            && matches!(d.args.first(), Some(JavaType::Object(sx)) if sx.as_ref() == "java/lang/String")
2935            && matches!(d.args.get(1), Some(JavaType::Int))
2936        {
2937            if let Some(b) = body.as_mut() {
2938                // A ctor that DELEGATES first (the -IA synthetic bridge,
2939                // `this(str, p2)`) must stay delegation-first — the
2940                // delegated-to ctor performs the stores.
2941                let starts_with_delegation = match &b.body {
2942                    Stmt::Block(vs) => vs.first().is_some_and(crate::passes::is_bare_ctor_call),
2943                    other => crate::passes::is_bare_ctor_call(other),
2944                };
2945                let ps: Vec<u32> = b
2946                    .vt
2947                    .vars
2948                    .iter()
2949                    .filter(|v| v.is_param && v.name != "this")
2950                    .take(2)
2951                    .map(|v| v.id)
2952                    .collect();
2953                if ps.len() == 2 && !starts_with_delegation {
2954                    let mk_store = |var: u32, fname: &str, fty: JavaType| {
2955                        Stmt::ExprStmt(Expr::Assign {
2956                            target: Box::new(Expr::Field {
2957                                owner: Some(Box::new(Expr::This)),
2958                                cls: std::sync::Arc::from(class.name.as_str()),
2959                                name: std::sync::Arc::from(fname),
2960                                ty: jdc_core::ir::expr::TypeRef::J(fty.clone()),
2961                                is_static: false,
2962                            }),
2963                            op: jdc_core::ir::expr::AssignOp::Plain,
2964                            value: Box::new(Expr::Local {
2965                                var,
2966                                ty: jdc_core::ir::expr::TypeRef::J(fty),
2967                            }),
2968                        })
2969                    };
2970                    let mut inj = vec![
2971                        mk_store(ps[0], ENUM_NAME_FIELD, JavaType::Object("java/lang/String".into())),
2972                        mk_store(ps[1], ENUM_ORD_FIELD, JavaType::Int),
2973                    ];
2974                    if let Stmt::Block(vs) = &mut b.body {
2975                        let mut rest = std::mem::take(vs);
2976                        inj.append(&mut rest);
2977                        *vs = inj;
2978                    } else {
2979                        let prev = std::mem::replace(&mut b.body, Stmt::Block(Vec::new()));
2980                        inj.push(prev);
2981                        b.body = Stmt::Block(inj);
2982                    }
2983                }
2984            }
2985        }
2986        let mut arg0 = 0;
2987        if enum_promoted
2988            && is_init
2989            && matches!(d.args.first(), Some(JavaType::Object(s)) if s.as_ref() == "java/lang/String")
2990            && matches!(d.args.get(1), Some(JavaType::Int))
2991        {
2992            // The Kotlin default-arg bridge ctor reads name/ordinal in
2993            // its `this(str, p2, ..)` delegation only — the LEADING pair
2994            // of a this()-delegation drops together with the params, so
2995            // those reads do not block the strip (the constants' extra
2996            // args match the bridge's user params, not the 2-param user
2997            // ctor).
2998            let synthetic: Vec<u32> = body
2999                .as_ref()
3000                .map(|b| {
3001                    b.vt
3002                        .vars
3003                        .iter()
3004                        .filter(|v| v.is_param && v.name != "this")
3005                        .take(2)
3006                        .map(|v| v.id)
3007                        .collect()
3008                })
3009                .unwrap_or_default();
3010            // The delegation-leading extension only applies to the
3011            // Kotlin default-arg BRIDGE: its descriptor ends with
3012            // kotlin/jvm/internal/DefaultConstructorMarker. A REAL user
3013            // ctor whose first two params are (String, int) and merely
3014            // forwards them must keep its signature (weixin +2.9k when
3015            // ungated).
3016            // Marker-name-free bridge shape: (.., int mask, marker).
3017            // R8 renames DefaultConstructorMarker with the rest of the
3018            // stdlib (weixin: kotlin/jvm/internal/i, an empty abstract
3019            // class), so gate on the package + the int-mask/refs pair
3020            // instead of the literal name — a real user enum ctor with
3021            // (int, kotlin/jvm/internal/*) tail params does not occur.
3022            let n_args = d.args.len();
3023            let is_bridge = n_args >= 2
3024                && matches!(&d.args[n_args - 1], JavaType::Object(ref m)
3025                    if m.starts_with("kotlin/jvm/internal/"))
3026                && matches!(d.args[n_args - 2], JavaType::Int);
3027            // Meituan Robust instrumented ctors PACK the trace params
3028            // into dispatch arrays (`v3[0] = str; v3[1] = new
3029            // Integer(p2); PatchProxy.isSupport(v3, ..)`) — value reads
3030            // that refuse the strip, and a true enum cannot DECLARE
3031            // (String, int) params (JLS 8.9.1), so the whole class fell
3032            // back (illegal `Enum.valueOf` + no ordinal()). Rewrite pure
3033            // VALUE reads to the equivalent `name()`/`ordinal()` calls —
3034            // Enum's fields are set by the implicit super before the
3035            // body runs, so the pack sees identical values. Delegation
3036            // LEAD reads (this(str, p2) of Kotlin/-IA bridges) must keep
3037            // their Local shape for the lead-drop below, so skip any
3038            // ctor that has them; written params are never replaceable.
3039            if synthetic.len() == 2 && !is_bridge {
3040                if let Some(b) = body.as_ref() {
3041                    let (p0, p1) = (synthetic[0], synthetic[1]);
3042                    let mut lead_reads = 0usize;
3043                    {
3044                        let cls_name = class.name.as_str();
3045                        let mut c = b.body.clone();
3046                        crate::passes::walk_stmt_exprs(&mut c, &mut |e| {
3047                            if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3048                                if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3049                                    for a in args.iter().take(2) {
3050                                        if let Expr::Local { var: v, .. } = a {
3051                                            if *v == p0 || *v == p1 {
3052                                                lead_reads += 1;
3053                                            }
3054                                        }
3055                                    }
3056                                }
3057                            }
3058                        });
3059                    }
3060                    let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
3061                    let reads = uses.get(&p0).copied().unwrap_or(0)
3062                        + uses.get(&p1).copied().unwrap_or(0);
3063                    let written = crate::passes::local_is_written(&b.body, p0)
3064                        || crate::passes::local_is_written(&b.body, p1);
3065                    if lead_reads == 0 && reads > 0 && !written {
3066                        let mk = |nm: &str, ret: JavaType| Expr::Method {
3067                            owner: Some(Box::new(Expr::This)),
3068                            cls: std::sync::Arc::from("java/lang/Enum"),
3069                            name: std::sync::Arc::from(nm),
3070                            desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
3071                                ret,
3072                                args: Vec::new(),
3073                            }),
3074                            args: Vec::new(),
3075                            is_static: false,
3076                            is_interface: false,
3077                            is_special: false,
3078                            is_super: false,
3079                            is_dynamic: false,
3080                            type_args: Vec::new(),
3081                        };
3082                        let name_call =
3083                            mk("name", JavaType::Object("java/lang/String".into()));
3084                        let ord_call = mk("ordinal", JavaType::Int);
3085                        if let Some(b) = body.as_mut() {
3086                            crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
3087                                crate::passes::deep_rewrite(e, &mut |x| {
3088                                    if let Expr::Local { var, .. } = x {
3089                                        if *var == p0 {
3090                                            *x = name_call.clone();
3091                                        } else if *var == p1 {
3092                                            *x = ord_call.clone();
3093                                        }
3094                                    }
3095                                });
3096                            });
3097                        }
3098                    }
3099                }
3100            }
3101            let mut lead = [0usize, 0usize];
3102            let refs_ok = body.as_ref().is_some_and(|b| {
3103                let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
3104                let mut c = b.body.clone();
3105                let cls_name = class.name.as_str();
3106                crate::passes::walk_stmt_exprs(&mut c, &mut |e| {
3107                    if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3108                        if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3109                            for (k, a) in args.iter().enumerate() {
3110                                if let Expr::Local { var: v, .. } = a {
3111                                    if let Some(pi) = synthetic.iter().position(|p| p == v) {
3112                                        if k == pi && k < 2 {
3113                                            lead[pi] += 1;
3114                                        }
3115                                    }
3116                                }
3117                            }
3118                        }
3119                    }
3120                });
3121                synthetic.iter().enumerate().all(|(pi, id)| {
3122                    let total = uses.get(id).copied().unwrap_or(0);
3123                    if is_bridge {
3124                        total == lead[pi]
3125                    } else {
3126                        total == 0
3127                    }
3128                })
3129            });
3130            if refs_ok {
3131                arg0 = 2;
3132                // Drop the leading (name, ordinal) args of the this()
3133                // delegations.
3134                if let (Some(b), true) = (body.as_mut(), is_bridge) {
3135                    let cls_name = class.name.as_str();
3136                    let syn = synthetic;
3137                    crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
3138                        if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3139                            if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3140                                let mut drop_n = 0;
3141                                for a in args.iter().take(2) {
3142                                    if let Expr::Local { var: v, .. } = a {
3143                                        if syn.contains(v) && drop_n == v - syn[0] {
3144                                            drop_n += 1;
3145                                            continue;
3146                                        }
3147                                    }
3148                                    break;
3149                                }
3150                                for _ in 0..drop_n {
3151                                    args.remove(0);
3152                                }
3153                            }
3154                        }
3155                    });
3156                }
3157            }
3158        }
3159        let n = d.args.len();
3160        let varargs = a & ACC_VARARGS != 0 && n > 0;
3161        let arg0 = arg0 + inner_arg0;
3162        for (i, arg) in d.args.iter().enumerate().skip(arg0) {
3163            if i > arg0 {
3164                sig.push_str(", ");
3165            }
3166            let name = param_names
3167                .get(i)
3168                .cloned()
3169                .unwrap_or_else(|| format!("p{}", i));
3170            if varargs && i + 1 == n {
3171                if let JavaType::Array(inner) = arg {
3172                    sig.push_str(&type_name(pool, inner));
3173                    sig.push_str("...");
3174                } else {
3175                    sig.push_str(&type_name(pool, arg));
3176                }
3177            } else {
3178                sig.push_str(&type_name(pool, arg));
3179            }
3180            sig.push(' ');
3181            // Param names come from dex debug info — obfuscated apps
3182            // name them `_` (reserved since Java 9) or after keywords.
3183            sig.push_str(&java_ident(&name));
3184        }
3185        sig.push(')');
3186    }
3187
3188    if is_clinit {
3189        let Some(b) = body else { return Ok(false) };
3190        // Direct render: the printer starts at the method's ABSOLUTE
3191        // indent and appends into the same buffer that carries the
3192        // header — no intermediate body string, no per-line re-indent
3193        // pass (two full copies of every method body saved).
3194        out.push_str(&ind);
3195        out.push_str("static {\n");
3196        let hdr = out.len();
3197        let printer = Printer::new(ctx, &b.vt)
3198            .with_indent(depth + 1)
3199            .with_output(std::mem::take(out));
3200        let t_print = std::time::Instant::now();
3201        let mut rendered = printer.into_string(&b.body);
3202        crate::method::phase_hit(3, t_print);
3203        if rendered[hdr..].trim().is_empty() {
3204            rendered.truncate(hdr);
3205        }
3206        rendered.push_str(&ind);
3207        rendered.push_str("}\n");
3208        *out = rendered;
3209        return Ok(true);
3210    }
3211    if a & (ACC_ABSTRACT | ACC_NATIVE) != 0 || body.is_none() {
3212        out.push_str(&ind);
3213        out.push_str(&sig);
3214        out.push_str(";\n");
3215        return Ok(true);
3216    }
3217    let Some(b) = body else { return Ok(false) };
3218
3219    // Direct render at the absolute indent level (see the clinit path).
3220    let mut printer = Printer::new(ctx, &b.vt).with_indent(depth + 1);
3221    match &b.desc.ret {
3222        JavaType::Boolean => {
3223            printer = printer.with_ret_bool(true);
3224        }
3225        JavaType::Char => {
3226            printer = printer.with_ret_char(true);
3227        }
3228        JavaType::Byte => {
3229            printer = printer.with_ret_narrow(true, false);
3230        }
3231        JavaType::Short => {
3232            printer = printer.with_ret_narrow(false, true);
3233        }
3234        _ => {}
3235    }
3236    out.push_str(&ind);
3237    out.push_str(&sig);
3238    out.push_str(" {\n");
3239    let hdr = out.len();
3240    let printer = printer.with_output(std::mem::take(out));
3241    let t_print = std::time::Instant::now();
3242    let mut rendered = printer.into_string(&b.body);
3243    crate::method::phase_hit(3, t_print);
3244    if rendered[hdr..].trim().is_empty() {
3245        rendered.truncate(hdr);
3246    }
3247    rendered.push_str(&ind);
3248    rendered.push_str("}\n");
3249    *out = rendered;
3250    Ok(true)
3251}
3252
3253// ---------------------------------------------------------------------------
3254// Naming helpers
3255// ---------------------------------------------------------------------------
3256
3257fn indent(depth: usize) -> String {
3258    "    ".repeat(depth)
3259}
3260
3261/// `$`-separated nesting rendered with dots — but ONLY when every
3262/// segment is a clean Java identifier (a genuine member class chain).
3263/// Anonymous (`Outer$1`), Kotlin synthetic (`Version$bigInteger$2`,
3264/// `...$$inlined$collect$1`) and local-class tails are NOT member
3265/// classes Java can name; the whole name stays flat with `$` (ddc emits
3266/// them as their own top-level files).
3267/// Kotlin emits method names like `invokeSuspend$lambda-0` — `-` (and
3268/// any other non-identifier character) is not legal Java. Deterministic
3269/// mapping, applied identically at declaration and call sites.
3270pub(crate) fn java_ident(name: &str) -> std::borrow::Cow<'_, str> {
3271    // env::var_os is an environ lock+scan — java_ident runs per IDENTIFIER
3272    // (millions per APK), where it profiled as __NSGetEnviron.
3273    static DBG_IDENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3274    if *DBG_IDENT.get_or_init(|| std::env::var_os("DDC_DBG_IDENT").is_some()) {
3275        eprintln!("[ident] {name:?}");
3276    }
3277    let clean = name
3278        .chars()
3279        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
3280    // Kotlin names fields/methods `default` (Companion.default) — no Java
3281    // program can declare or reference a keyword; the identical mapping
3282    // lives in jdc-core's call-site sanitizer.
3283    let keyword = matches!(
3284        name,
3285        "abstract"
3286            | "assert"
3287            | "boolean"
3288            | "break"
3289            | "byte"
3290            | "case"
3291            | "catch"
3292            | "char"
3293            | "class"
3294            | "const"
3295            | "continue"
3296            | "default"
3297            | "do"
3298            | "double"
3299            | "else"
3300            | "enum"
3301            | "extends"
3302            | "final"
3303            | "finally"
3304            | "float"
3305            | "for"
3306            | "goto"
3307            | "if"
3308            | "implements"
3309            | "import"
3310            | "instanceof"
3311            | "int"
3312            | "interface"
3313            | "long"
3314            | "native"
3315            | "new"
3316            | "package"
3317            | "private"
3318            | "protected"
3319            | "public"
3320            | "return"
3321            | "short"
3322            | "static"
3323            | "strictfp"
3324            | "super"
3325            | "switch"
3326            | "synchronized"
3327            | "this"
3328            | "throw"
3329            | "throws"
3330            | "transient"
3331            | "try"
3332            | "void"
3333            | "volatile"
3334            | "while"
3335            | "true"
3336            | "false"
3337            | "null"
3338            // `_` is a reserved identifier since Java 9 (Alipay's
3339            // instant-run fields are named `_`) — the `_<name>` mapping
3340            // turns it into `__`, matching jdc-core's call sites.
3341            | "_"
3342    );
3343    // A simple name may not START with a digit either (WhatsApp nests
3344    // `X/0Xx`): the declaration site and every reference (jdc-core's
3345    // sanitize_source_name) prefix the same underscore.
3346    let digit_start = name.chars().next().is_some_and(|c| c.is_ascii_digit());
3347    if clean && !keyword && !digit_start {
3348        std::borrow::Cow::Borrowed(name)
3349    } else if keyword || digit_start {
3350        std::borrow::Cow::Owned(format!("_{name}"))
3351    } else {
3352        // Non-ASCII single chars (Alipay names a field `支`) map to a
3353        // lone `_` — itself reserved since Java 9. Escape it.
3354        let mapped: String = name
3355            .chars()
3356            .map(|c| {
3357                if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
3358                    c
3359                } else {
3360                    '_'
3361                }
3362            })
3363            .collect();
3364        if mapped == "_" {
3365            std::borrow::Cow::Owned("__".to_string())
3366        } else {
3367            std::borrow::Cow::Owned(mapped)
3368        }
3369    }
3370}
3371
3372fn split_name(internal: &str) -> (String, String) {
3373    match internal.rfind('/') {
3374        Some(i) => (internal[..i].to_string(), internal[i + 1..].to_string()),
3375        None => (String::new(), internal.to_string()),
3376    }
3377}
3378
3379/// Dotted source form of an internal name.
3380pub fn dotted(internal: &str) -> String {
3381    let mut out = internal.replace('/', ".");
3382    // `$` → `.` only when the following segment can start a Java
3383    // identifier (anonymous/synthetic tails stay `$`).
3384    let mut i = 0;
3385    while let Some(p) = out[i..].find('$') {
3386        let at = i + p;
3387        // A LEADING `$` (ProGuard keeps `$Gson$Types`) is part of the
3388        // source name — dotting it produced a leading `.Gson.Types`.
3389        let head_ok = at > 0
3390            && out[..at]
3391                .chars()
3392                .next_back()
3393                .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
3394        let tail_ok = out[at + 1..]
3395            .chars()
3396            .next()
3397            .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
3398        if tail_ok && head_ok {
3399            out.replace_range(at..at + 1, ".");
3400            i = at + 1;
3401        } else {
3402            i = at + 1;
3403        }
3404    }
3405    // Every `.`-segment must start an identifier: WhatsApp's `X/0Hl`
3406    // reached `extends` with the digit start unmapped.
3407    sanitize_fq(&out)
3408}
3409
3410fn join_dotted(pool: &DexPool, names: &[String]) -> String {
3411    names
3412        .iter()
3413        .map(|n| print_class_name(pool, n))
3414        .collect::<Vec<_>>()
3415        .join(", ")
3416}
3417
3418/// A printable type name (arrays render with `[]` suffixes). Nested class
3419/// names dot their `$` when the outer chain is known to the pool.
3420/// Split a concatenated method-descriptor ARGUMENT string
3421/// (`"Ljava/lang/String;I[Ljava/lang/Object;"`) into per-parameter
3422/// `JavaType`s. Used by the framework-ancestor ctor bridge, where the
3423/// referenced sig comes from the method-id table (no pool ancestor
3424/// method to read a parsed descriptor from).
3425fn split_arg_descs(sig: &str) -> Vec<JavaType> {
3426    let mut out = Vec::new();
3427    let b = sig.as_bytes();
3428    let mut i = 0usize;
3429    while i < b.len() {
3430        let start = i;
3431        while i < b.len() && b[i] == b'[' {
3432            i += 1;
3433        }
3434        if i < b.len() {
3435            if b[i] == b'L' {
3436                while i < b.len() && b[i] != b';' {
3437                    i += 1;
3438                }
3439                if i < b.len() {
3440                    i += 1; // consume ';'
3441                }
3442            } else {
3443                i += 1; // primitive single char
3444            }
3445        }
3446        if i > start {
3447            out.push(desc_type(&sig[start..i]));
3448        } else {
3449            break; // malformed: avoid an infinite loop
3450        }
3451    }
3452    out
3453}
3454
3455pub fn type_name(pool: &DexPool, t: &JavaType) -> String {
3456    match t {
3457        JavaType::Void => "void".into(),
3458        JavaType::Object(n) => print_class_name(pool, n),
3459        JavaType::Array(inner) => format!("{}[]", type_name(pool, inner)),
3460        other => other.to_java(false),
3461    }
3462}
3463
3464/// `com/foo/Outer$Inner` → `com.foo.Outer.Inner` — each `$` dots only when
3465/// its left side names a known class (literal `$` top-level names survive).
3466// ---------------------------------------------------------------------------
3467// The per-class import map (JLS 6.4.2 obscuring repair).
3468//
3469// A referenced FQN whose FIRST segment equals an in-scope class simple
3470// name binds the qualifier to the CLASS, not the package — `class j3
3471// implements j3.g` is cyclic inheritance and collapses javac's
3472// attribution for the whole package (the Object-cascade root, weixin
3473// 84 files + contagion). The repair: emit `import j3.g;` and render
3474// the SIMPLE name in every type position. The set is populated per
3475// class from a metadata pre-scan (supertypes, field types, method
3476// signature descriptors) and read by print_class_name and the
3477// DexCtx::obscured_simple implementation (which jdc-core's shorten /
3478// java_type_name consult).
3479struct ObscureState {
3480    /// The ROOT class being rendered (its simple name is the obscuring
3481    /// in-scope name).
3482    class: String,
3483    /// The root class transitively extends android.view.View: it
3484    /// INHERITS the static Property fields View.X/Y/Z (API 31+), and
3485    /// an inherited member variable obscures a package's first segment
3486    /// in expression position (JLS 6.4.2) — every `X.foo` qualified
3487    /// ref inside the file would bind to the field (WhatsApp ×30,171:
3488    /// 431 custom-View files against its obfuscated package X). Those
3489    /// refs route through the import layer like own-simple obscuring.
3490    view_shadow: bool,
3491    /// Metadata pre-scan results: internal → simple render.
3492    map: jdc_core::FxHashMap<String, String>,
3493    /// Expression-level obscured refs DISCOVERED during the body render
3494    /// (their imports emit at assembly time).
3495    recorded: jdc_core::FxHashSet<String>,
3496    /// Simple names of the CURRENT PACKAGE's own classes: importing a
3497    /// name that collides would SHADOW every same-package use of it in
3498    /// this file (JLS 7.5.1) — those refs stay qualified (obscured,
3499    /// erroring) rather than corrupt.
3500    blocked: jdc_core::FxHashSet<String>,
3501    /// RENAMED display tails of same-package classes (zero-copy view
3502    /// into the process-wide cache): same blocking role as `blocked`
3503    /// for refs that render under the rename registry.
3504    blocked_renamed: Option<&'static jdc_core::FxHashSet<String>>,
3505}
3506
3507thread_local! {
3508    static OBSCURE: std::cell::RefCell<Option<ObscureState>> =
3509        const { std::cell::RefCell::new(None) };
3510}
3511
3512/// Install the per-class render state (call at class render entry).
3513pub(crate) fn set_obscured_state(
3514    class: String,
3515    map: jdc_core::FxHashMap<String, String>,
3516    blocked: jdc_core::FxHashSet<String>,
3517    blocked_renamed: Option<&'static jdc_core::FxHashSet<String>>,
3518    view_shadow: bool,
3519) {
3520    OBSCURE.with(|m| {
3521        *m.borrow_mut() = Some(ObscureState {
3522            class,
3523            view_shadow,
3524            map,
3525            recorded: jdc_core::FxHashSet::default(),
3526            blocked,
3527            blocked_renamed,
3528        });
3529    });
3530}
3531
3532/// Simple names this class's file renders through the import layer
3533/// (metadata map values + dynamically recorded nested tails). A method
3534/// local with one of these names captures the imported simple name in
3535/// expression position (locals beat single-type imports — `h.e` binds
3536/// a local int `h`, 无法取消引用int); deshadow_locals renames them.
3537/// Empty when no render state is installed (progressive paths).
3538pub(crate) fn obscured_simples_snapshot() -> jdc_core::FxHashSet<String> {
3539    OBSCURE.with(|m| {
3540        let mut out = jdc_core::FxHashSet::default();
3541        let Ok(st) = m.try_borrow() else { return out };
3542        let Some(st) = st.as_ref() else { return out };
3543        for simple in st.map.values() {
3544            out.insert(simple.clone());
3545        }
3546        for internal in &st.recorded {
3547            let renamed = crate::apply_class_rename(internal);
3548            if let Some(tail) = renamed.rsplit(['/', '$']).next() {
3549                if !tail.is_empty() {
3550                    out.insert(tail.to_string());
3551                }
3552            }
3553        }
3554        out
3555    })
3556}
3557
3558/// Take the recorded expression-level refs and clear the state.
3559pub(crate) fn take_recorded_and_clear() -> jdc_core::FxHashSet<String> {
3560    OBSCURE.with(|m| {
3561        m.borrow_mut()
3562            .take()
3563            .map(|st| st.recorded)
3564            .unwrap_or_default()
3565    })
3566}
3567
3568// ---- cross-package access widening ----------------------------------
3569// ART tolerates cross-package access to non-public classes/members that
3570// R8/d8 preserved; Java source cannot express it ("是在不可访问的类或
3571// 接口中定义的", "k在kotlinx.coroutines中不是公共的", "unknownFields 在
3572// l6 中是 protected"). The census scan records every cross-package
3573// reference; the installer keeps the non-public targets and the
3574// declaration renders widen them to public.
3575
3576struct WidenState {
3577    classes: jdc_core::FxHashSet<String>,
3578    methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>>,
3579    fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>,
3580    /// (owner, field name) finals written outside the declaring class's
3581    /// ctor/clinit — rendered WITHOUT `final` (see refscan census).
3582    unfinal: jdc_core::FxHashSet<(String, String)>,
3583}
3584
3585static WIDEN: std::sync::OnceLock<WidenState> = std::sync::OnceLock::new();
3586
3587pub(crate) fn install_access_widening(pool: &crate::DexPool) {
3588    let t0 = std::time::Instant::now();
3589    let raw = crate::refscan::access_widening_scan(pool.dexes());
3590    let mut classes: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3591    for c in &raw.classes {
3592        if let Some(pc) = pool.get_if_materialized(c) {
3593            if pc.access & crate::access::ACC_PUBLIC == 0 {
3594                classes.insert(c.clone());
3595            }
3596        }
3597    }
3598    let mut methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>> =
3599        jdc_core::FxHashMap::default();
3600    for (owner, set) in &raw.methods {
3601        for (name, desc) in set {
3602            if name == "<clinit>" {
3603                continue; // never referenced
3604            }
3605            // A dex ref of an INHERITED member may name any subclass as
3606            // owner (ART resolves up the chain), while the declaration —
3607            // and its access modifier — lives on an ancestor. Register
3608            // the widening on the DECLARING class or the rendered decl
3609            // stays protected ("在 X 中是 protected 访问控制").
3610            let mut cur: Option<&str> = Some(owner.as_str());
3611            let mut hops = 0u32;
3612            while let Some(cname) = cur {
3613                hops += 1;
3614                if hops > 64 {
3615                    break;
3616                }
3617                let Some(pc) = pool.get_if_materialized(cname) else {
3618                    break;
3619                };
3620                // Enum ctors are source-level private-only: widening is
3621                // a javac "modifier not allowed here".
3622                if name == "<init>" && pc.access & crate::access::ACC_ENUM != 0 {
3623                    break;
3624                }
3625                match pc.all_methods()
3626                    .find(|m| &*m.name == name.as_str() && &*m.desc == desc.as_str())
3627                {
3628                    Some(m) if m.access & crate::access::ACC_PUBLIC == 0 => {
3629                        methods
3630                            .entry(cname.to_string())
3631                            .or_default()
3632                            .insert((name.clone(), desc.clone()));
3633                        break;
3634                    }
3635                    Some(_) => break, // already public
3636                    None => cur = pc.super_name.as_deref(),
3637                }
3638            }
3639        }
3640    }
3641    // Override-closure DOWN the hierarchy: a widened (public) super
3642    // method whose subclass override stays protected/package is
3643    // "无法覆盖...更低的访问权限" (lark +823). Every transitive
3644    // subclass declaring the same (name, desc) non-public widens too.
3645    {
3646        let mut subs: jdc_core::FxHashMap<String, Vec<String>> =
3647            jdc_core::FxHashMap::default();
3648        for name in &pool.order {
3649            let Some(pc) = pool.get_if_materialized(name) else {
3650                continue;
3651            };
3652            if let Some(sup) = &pc.super_name {
3653                subs.entry(sup.clone()).or_default().push(name.clone());
3654            }
3655            for i in &pc.interfaces {
3656                subs.entry(i.clone()).or_default().push(name.clone());
3657            }
3658        }
3659        let seeds: Vec<(String, String, String)> = methods
3660            .iter()
3661            .flat_map(|(o, set)| {
3662                set.iter().map(|(n, d)| (o.clone(), n.clone(), d.clone()))
3663            })
3664            .collect();
3665        for (owner, mname, mdesc) in seeds {
3666            let mut queue: Vec<String> = match subs.get(&owner) {
3667                Some(v) => v.clone(),
3668                None => continue,
3669            };
3670            let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3671            while let Some(c) = queue.pop() {
3672                if !seen.insert(c.clone()) {
3673                    continue;
3674                }
3675                let Some(pc) = pool.get_if_materialized(&c) else {
3676                    continue;
3677                };
3678                let declares_nonpub = pc.all_methods().any(|m| {
3679                    &*m.name == mname.as_str()
3680                        && &*m.desc == mdesc.as_str()
3681                        && m.access & crate::access::ACC_PUBLIC == 0
3682                });
3683                if declares_nonpub {
3684                    methods
3685                        .entry(c.clone())
3686                        .or_default()
3687                        .insert((mname.clone(), mdesc.clone()));
3688                }
3689                if let Some(v) = subs.get(&c) {
3690                    queue.extend(v.iter().cloned());
3691                }
3692            }
3693        }
3694        // Subclass `super(..)` targets. A class C extends S; C's ctor
3695        // renders `super(..)` against S's ctor. When R8 INLINES S's
3696        // trivial ctor, C's dex ctor calls `Object.<init>` directly (not
3697        // `S.<init>`), so the bytecode census above never sees the S.<init>
3698        // reference and a PRIVATE S ctor stays private — the rendered
3699        // `super()` then fails ("R() 在 R 中是 private 访问控制", lark ×122:
3700        // pl.droidsonroids.gif.R extends com.ss.android.lark.R, whose ctor
3701        // R8 inlined to Object.<init>). Widen every private ctor of a
3702        // class that HAS subclasses so the implicit super() links. Enums
3703        // are excluded (their ctors are source-level private-only).
3704        for sup in subs.keys() {
3705            let Some(pc) = pool.get_if_materialized(sup) else {
3706                continue;
3707            };
3708            if pc.access & crate::access::ACC_ENUM != 0 {
3709                continue;
3710            }
3711            for m in pc.all_methods() {
3712                if &*m.name == "<init>" && m.access & crate::access::ACC_PRIVATE != 0 {
3713                    methods
3714                        .entry(sup.clone())
3715                        .or_default()
3716                        .insert((m.name.to_string(), m.desc.to_string()));
3717                }
3718            }
3719        }
3720    }
3721    let mut fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
3722        jdc_core::FxHashMap::default();
3723    for (owner, set) in &raw.fields {
3724        for name in set {
3725            // Same inherited-owner walk as methods: the protected field
3726            // lives on the ancestor that DECLARES it (weibo
3727            // mModuleContext/mData ×470 — refs named the module
3728            // subclass, the own-fields-only filter never widened the
3729            // declaring base).
3730            let mut cur: Option<&str> = Some(owner.as_str());
3731            let mut hops = 0u32;
3732            while let Some(cname) = cur {
3733                hops += 1;
3734                if hops > 64 {
3735                    break;
3736                }
3737                let Some(pc) = pool.get_if_materialized(cname) else {
3738                    break;
3739                };
3740                match pc.static_fields
3741                    .iter()
3742                    .chain(pc.instance_fields.iter())
3743                    .find(|f| f.name == name.as_str())
3744                {
3745                    Some(f) if f.access & crate::access::ACC_PUBLIC == 0 => {
3746                        fields
3747                            .entry(cname.to_string())
3748                            .or_default()
3749                            .insert(name.clone());
3750                        break;
3751                    }
3752                    Some(_) => break, // already public
3753                    None => cur = pc.super_name.as_deref(),
3754                }
3755            }
3756        }
3757    }
3758    if std::env::var("DDC_STATS").is_ok() {
3759        eprintln!(
3760            "[renames] access widening: classes={} method-owners={} field-owners={} scan={:?}",
3761            classes.len(),
3762            methods.len(),
3763            fields.len(),
3764            t0.elapsed()
3765        );
3766    }
3767
3768    let unfinal: jdc_core::FxHashSet<(String, String)> =
3769        raw.unfinal_fields.into_iter().collect();
3770    let _ = WIDEN.set(WidenState { classes, methods, fields, unfinal });
3771}
3772
3773#[inline]
3774pub(crate) fn widen_class(internal: &str) -> bool {
3775    WIDEN.get().is_some_and(|w| w.classes.contains(internal))
3776}
3777
3778#[inline]
3779pub(crate) fn widen_method(owner: &str, name: &str, desc: &str) -> bool {
3780    WIDEN.get().is_some_and(|w| {
3781        w.methods
3782            .get(owner)
3783            .is_some_and(|s| s.contains(&(name.to_string(), desc.to_string())))
3784    })
3785}
3786
3787#[inline]
3788pub(crate) fn widen_field(owner: &str, name: &str) -> bool {
3789    WIDEN.get().is_some_and(|w| {
3790        w.fields.get(owner).is_some_and(|s| s.contains(name))
3791    })
3792}
3793
3794/// True when the census saw this final field written outside the
3795/// declaring class's `<init>`/`<clinit>` — the `final` modifier must
3796/// not render ("无法为 final 变量 v 分配值" at every foreign write).
3797#[inline]
3798pub(crate) fn unfinal_field(owner: &str, name: &str) -> bool {
3799    WIDEN.get().is_some_and(|w| {
3800        w.unfinal.contains(&(owner.to_string(), name.to_string()))
3801    })
3802}
3803
3804/// Render-time lookup: the metadata map first; otherwise an
3805/// expression-level ref whose first segment equals the root class's
3806/// simple name — record it (its import emits at assembly) and render
3807/// the simple name NOW.
3808pub(crate) fn obscured_render_pub(internal: &str) -> Option<String> {
3809    OBSCURE.with(|m| {
3810        let mut st = m.borrow_mut();
3811        let st = st.as_mut()?;
3812        if let Some(simple) = st.map.get(internal) {
3813            return Some(simple.clone());
3814        }
3815        let first = internal.split('/').next().unwrap_or("");
3816        // The DISPLAY own simple: when the root class itself was renamed
3817        // (case-collision `widget/d` → `widget/d2`), no in-scope type
3818        // bears the raw name any more — the package's first segment is
3819        // NOT obscured and the qualified render is the correct one
3820        // (reqable d2.java: importing d.h let the class's own boolean
3821        // field h shadow it — 无法在调用超类型构造器之前引用h ×23).
3822        let own_renamed = crate::apply_class_rename(&st.class);
3823        let own = own_renamed.rsplit(['/', '$']).next().unwrap_or("");
3824        let seg_obscured =
3825            first == own || (st.view_shadow && matches!(first, "X" | "Y" | "Z"));
3826        if seg_obscured && internal.split('/').count() >= 2 {
3827            // A NESTED internal's in-scope simple name is its `$` tail
3828            // (`x/a$b` imports/renders as `b`) — the slash-tail left the
3829            // `$` in the render (`t2$a` flat against a nested emission).
3830            // The tail must come from the RENAMED display: case-collision
3831            // renames (`X/00i` → `X/_00i_2`) live in the class registry —
3832            // the raw tail rendered `_00i` against the declaration's
3833            // `_00i_2` (WhatsApp cannot-find ×27k after the View-shadow
3834            // escape opened this path at scale).
3835            let renamed = crate::apply_class_rename(internal);
3836            let simple = renamed
3837                .rsplit(['/', '$'])
3838                .next()
3839                .unwrap_or("")
3840                .to_string();
3841            if !simple.is_empty()
3842                && !st.blocked.contains(&simple)
3843                && !st.blocked_renamed.is_some_and(|e| e.contains(&simple))
3844            {
3845                st.recorded.insert(internal.to_string());
3846                return Some(simple);
3847            }
3848        }
3849        None
3850    })
3851}
3852
3853/// True when `class` transitively extends android.view.View (pool
3854/// chain, then a framework-boundary prefix check: any android/widget,
3855/// android/view, android/webkit or inputmethodservice ancestor is a
3856/// View descendant and carries the inherited View.X/Y/Z statics).
3857fn is_view_subclass(pool: &DexPool, class: &PoolClass) -> bool {
3858    let mut cur = class.super_name.clone();
3859    let mut hops = 0;
3860    while let Some(s) = cur {
3861        hops += 1;
3862        if hops > 64 {
3863            break;
3864        }
3865        if s == "android/view/View" {
3866            return true;
3867        }
3868        match pool.get(&s) {
3869            Some(sc) => cur = sc.super_name.clone(),
3870            None => {
3871                return s.starts_with("android/widget/")
3872                    || s.starts_with("android/view/")
3873                    || s.starts_with("android/webkit/")
3874                    || s.starts_with("android/inputmethodservice/");
3875            }
3876        }
3877    }
3878    false
3879}
3880
3881/// The pre-scan: internal names referenced by the class's metadata
3882/// (supertypes, field types, method descriptors) whose first segment
3883/// equals the class's own simple name and which exist in the pool —
3884/// these get imports and simple-name renders.
3885fn compute_obscured_renders(
3886    pool: &DexPool,
3887    class: &PoolClass,
3888    view_shadow: bool,
3889) -> jdc_core::FxHashMap<String, String> {
3890    let own_renamed = crate::apply_class_rename(&class.name);
3891    let own_simple = own_renamed.rsplit(['/', '$']).next().unwrap_or("");
3892    if own_simple.is_empty() {
3893        return jdc_core::FxHashMap::default();
3894    }
3895    let mut refs: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3896    for sup in class.interfaces.iter() {
3897        refs.insert(sup.clone());
3898    }
3899    if let Some(sup) = &class.super_name {
3900        refs.insert(sup.clone());
3901    }
3902    for f in class.static_fields.iter().chain(class.instance_fields.iter()) {
3903        if let JavaType::Object(n) = crate::desc_type(&f.desc) {
3904            refs.insert(n.to_string());
3905        }
3906    }
3907    for m in class.all_methods() {
3908        if let Some(d) = m.parsed_desc() {
3909            for a in d.args.iter() {
3910                if let JavaType::Object(n) = a {
3911                    refs.insert(n.to_string());
3912                }
3913            }
3914            if let JavaType::Object(n) = &d.ret {
3915                refs.insert(n.to_string());
3916            }
3917        }
3918    }
3919    let mut out: jdc_core::FxHashMap<String, String> = jdc_core::FxHashMap::default();
3920    for r in refs {
3921        // First segment == the class's own simple name: the qualified
3922        // render would be obscured. The name must exist in the pool for
3923        // the import to resolve. The KEY stays internal (renders look
3924        // refs up by internal name), but the import line and the
3925        // simple name use the RENAMED display — a collision-renamed
3926        // class renders under its display name and an import of the
3927        // internal name does not resolve.
3928        let first = r.split('/').next().unwrap_or("");
3929        let seg_obscured =
3930            first == own_simple || (view_shadow && matches!(first, "X" | "Y" | "Z"));
3931        if seg_obscured && r.split('/').count() >= 2 && pool.get(&r).is_some() {
3932            // Own-family refs (the class itself, its nested members)
3933            // render through member scope — an import would be redundant
3934            // (self-import) or shadow a same-package sibling named like
3935            // the tail (`import t.t2.a` hijacks every bare `a` that
3936            // meant sibling class t.a — lark t/t2).
3937            if r == class.name || r.starts_with(&format!("{}$", class.name)) {
3938                continue;
3939            }
3940            let display = crate::apply_class_rename(&r);
3941            let simple = display
3942                .rsplit(['/', '$'])
3943                .next()
3944                .unwrap_or("")
3945                .to_string();
3946            if !simple.is_empty() {
3947                out.insert(r.clone(), simple);
3948            }
3949        }
3950    }
3951    out
3952}
3953
3954pub fn print_class_name(pool: &DexPool, internal: &str) -> String {
3955    if let Some(simple) = obscured_render_pub(internal) {
3956        return sanitize_ref(&simple);
3957    }
3958    let cow = crate::apply_class_rename(internal);
3959    let internal: &str = &cow;
3960    // Flat EMISSION UNITS: a digit-tail member (anonymous / d8-lambda
3961    // shape, `Outer$lruCache$1`) is emitted as its own top-level file
3962    // whose simple name keeps every `$` — the `$` boundaries inside
3963    // that unit are part of the NAME, not nesting. References must use
3964    // the flat unit name; the generic loop below dotted the first
3965    // boundary (`LruCacheKt.lruCache$1`) against the declaration
3966    // `class LruCacheKt$lruCache$1` — every use of the type failed and
3967    // javac's attribution for the whole file collapsed (3627 pure-
3968    // cascade files on weibo).
3969    if internal.contains('$') && pool.get(internal).is_some() {
3970        if let Some(root) = emission_root(pool, internal) {
3971            if root.contains('$') {
3972                let below = &internal[root.len()..];
3973                if below.is_empty() {
3974                    // The unit itself: the flat name IS the reference.
3975                    return sanitize_ref(&internal.replace('/', "."));
3976                }
3977                let segs: Vec<&str> = below[1..].split('$').collect();
3978                if segs.iter().any(|s| s.is_empty()) {
3979                    // R8 tails like `ThreadMsg$$$` cannot be dotted at
3980                    // all — keep the whole name flat.
3981                    return sanitize_ref(&internal.replace('/', "."));
3982                }
3983                let mut out = root.replace('/', ".");
3984                for s in segs {
3985                    out.push('.');
3986                    out.push_str(s);
3987                }
3988                return sanitize_ref(&out);
3989            }
3990        }
3991    }
3992    let mut out = String::new();
3993    // `known` must test the ACCUMULATED internal prefix, not the bare
3994    // inter-`$` segment: the per-segment shape checked `pool.get("a")`
3995    // for the second level of `s5/o$a$b`, missed, and rendered the
3996    // undeclarable reference `s5.o.a$b` (找不到符号) for every nested-
3997    // nested type.
3998    let mut off = 0usize;
3999    loop {
4000        let rest = &internal[off..];
4001        match rest.find('$') {
4002            Some(i) => {
4003                let seg = &rest[..i];
4004                let prefix = &internal[..off + i];
4005                let known = pool.get(prefix).is_some()
4006                    || jdc_core::rename::is_renamed_display(prefix)
4007                    // The FULL name is not a pool class: this `$` cannot
4008                    // be a literal name (pool literal classes — an app's
4009                    // own `View$OnUnhandledKeyEventListener` — keep their
4010                    // `$` here AND at their declaration), so it can only
4011                    // be an external framework nesting boundary
4012                    // (`View$OnClickListener` → `.OnClickListener`).
4013                    || !pool.get(internal).is_some();
4014                // The `$` may only become a nesting dot when the tail
4015                // segment STARTS a Java identifier: R8's desugared-
4016                // library names carry `$` inside PACKAGE paths
4017                // (`j$/util/...` dotted into `j..util`) and suffixes
4018                // like `Collection$-EL` or anonymous `RequestId$1`
4019                // cannot be dotted under any reading.
4020                let tail_ok = rest[i + 1..]
4021                    .chars()
4022                    .next()
4023                    .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
4024                out.push_str(&seg.replace('/', "."));
4025                out.push_str(if known && tail_ok { "." } else { "$" });
4026                off += i + 1;
4027            }
4028            None => {
4029                out.push_str(&rest.replace('/', "."));
4030                return sanitize_ref(&out);
4031            }
4032        }
4033    }
4034}
4035
4036/// The top-level EMISSION UNIT ancestor of a pool class: walking the
4037/// `$`-chain upward (outer_of), the first ancestor that is itself
4038/// emitted as its own compilation unit — a digit-tail rest marks a
4039/// flat boundary (see top_level_classes). Clean members render inline
4040/// in their outer, so only the unit's own name may carry `$`.
4041fn emission_root(pool: &DexPool, internal: &str) -> Option<String> {
4042    let mut cur = internal.to_string();
4043    loop {
4044        let outer = pool.outer_of(&cur)?;
4045        let rest = cur
4046            .strip_prefix(outer)
4047            .and_then(|t| t.strip_prefix('$'))
4048            .unwrap_or("");
4049        if !crate::clean_member_tail(rest) {
4050            return Some(cur);
4051        }
4052        cur = outer.to_string();
4053    }
4054}
4055
4056/// Class-file names may contain characters Java source identifiers
4057/// cannot (`Collection$-EL`); the deterministic mapping matches the
4058/// declaration sites (java_ident).
4059/// Every `.`-segment of a fully-qualified name must start a Java
4060/// identifier: obfuscators emit `package do;` and `..badge.new..` paths.
4061///
4062/// PUBLIC on purpose: this is the single source of truth for the
4063/// declaration↔file-name mapping. The CLI writer used to keep its own
4064/// copy (`sanitize_file_seg`) which had drifted — it lacked the lone-`_`
4065/// escape below — so `l.֡` declared `class __` inside a file named
4066/// `_.java`. Callers reach it through `sanitize_seg`/`sanitize_internal`.
4067///
4068/// INJECTIVE on the characters, which is the property the writer needs.
4069/// The old mapping folded every non-identifier character to `_`, so
4070/// `l.᩻ܶ`, `l.᩻ۡ` and `l.֫᩷` all became `l.__`: on bin.mt.plus 22,636
4071/// distinct classes in package `l` collapsed onto three file names, the
4072/// writer's EEXIST branch overwrote them, and 22,633 sources were
4073/// destroyed with exit code 0. `_u<hex>` keeps them distinct, is
4074/// self-describing (it encodes the original code point) and costs the
4075/// decompiler nothing — the alternative, minting 22,636 registry
4076/// renames, makes every type reference allocate and took this sample
4077/// from 5.2 s to 26.8 s.
4078///
4079/// The only residual collisions are *lookalikes*: a literal class named
4080/// `_u1a7b` beside the class `᩻` (same code point), or the pre-existing
4081/// keyword form `_do` beside `do`. Those are detected and repaired
4082/// deterministically by `lossy_sanitize_renames`, which is why that pass
4083/// still exists.
4084pub fn sanitize_fq(dotted: &str) -> String {
4085    dotted
4086        .split('.')
4087        .map(sanitize_fq_seg)
4088        .collect::<Vec<_>>()
4089        .join(".")
4090}
4091
4092/// One `.`-segment: escape, then repair identifier-position problems.
4093fn sanitize_fq_seg(seg: &str) -> String {
4094    // Ident-safe ASCII passes through unchanged (the overwhelmingly
4095    // common case, and the only one on the borrow-fast path); every
4096    // other character — non-ASCII, and ASCII punctuation such as the
4097    // `-` in `Collection$-EL` — becomes `_u<hex>`.
4098    let plain = seg
4099        .chars()
4100        .all(|c| c.is_ascii() && (c.is_ascii_alphanumeric() || c == '_' || c == '$'));
4101    let mut out = if plain {
4102        seg.to_string()
4103    } else {
4104        use std::fmt::Write as _;
4105        let mut s = String::with_capacity(seg.len() + 8);
4106        for c in seg.chars() {
4107            if c.is_ascii() && (c.is_ascii_alphanumeric() || c == '_' || c == '$') {
4108                s.push(c);
4109            } else {
4110                s.push_str("_u");
4111                let _ = write!(s, "{:x}", c as u32);
4112            }
4113        }
4114        s
4115    };
4116    // `out` is pure ASCII from here: the identifier-position repairs.
4117    if is_java_keyword_name(&out)
4118        || is_restricted_type_name(&out)
4119        || out.chars().next().is_some_and(|c| c.is_ascii_digit())
4120    {
4121        out.insert(0, '_');
4122    }
4123    // A lone `_` is a reserved IDENTIFIER since Java 9.
4124    if out == "_" {
4125        out = "__".to_string();
4126    }
4127    out
4128}
4129
4130/// Restricted contextual TYPE names — legal as member/local names
4131/// (rt.jar compiles `var` locals), illegal in class declarations and
4132/// type references. Consulted only on CLASS-name paths.
4133pub(crate) fn is_restricted_type_name(s: &str) -> bool {
4134    matches!(s, "var" | "yield" | "record" | "sealed" | "permits")
4135}
4136
4137fn is_java_keyword_name(s: &str) -> bool {
4138    matches!(
4139        s,
4140        "abstract"
4141            | "assert"
4142            | "boolean"
4143            | "break"
4144            | "byte"
4145            | "case"
4146            | "catch"
4147            | "char"
4148            | "class"
4149            | "const"
4150            | "continue"
4151            | "default"
4152            | "do"
4153            | "double"
4154            | "else"
4155            | "enum"
4156            | "extends"
4157            | "final"
4158            | "finally"
4159            | "float"
4160            | "for"
4161            | "goto"
4162            | "if"
4163            | "implements"
4164            | "import"
4165            | "instanceof"
4166            | "int"
4167            | "interface"
4168            | "long"
4169            | "native"
4170            | "new"
4171            | "package"
4172            | "private"
4173            | "protected"
4174            | "public"
4175            | "return"
4176            | "short"
4177            | "static"
4178            | "strictfp"
4179            | "super"
4180            | "switch"
4181            | "synchronized"
4182            | "this"
4183            | "throw"
4184            | "throws"
4185            | "transient"
4186            | "try"
4187            | "void"
4188            | "volatile"
4189            | "while"
4190            | "true"
4191            | "false"
4192            | "null"
4193            // `_` is a reserved identifier since Java 9 (Alipay's
4194            // instant-run fields are named `_`) — the `_<name>` mapping
4195            // turns it into `__`, matching jdc-core's call sites.
4196            | "_"
4197    )
4198}
4199
4200fn sanitize_ref(name: &str) -> String {
4201    sanitize_fq(name)
4202}
4203
4204/// Dotted source form of an internal name.
4205pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
4206    print_class_name(pool, internal)
4207}
4208
4209/// Unused import silencer.
4210#[allow(dead_code)]
4211fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
4212    let _ = java_type_to_generic;
4213}
4214
4215#[cfg(test)]
4216mod sanitize_tests {
4217    use super::sanitize_fq;
4218
4219    /// The property the file writer depends on: distinct class names must
4220    /// reach distinct output paths. The lossy predecessor of this mapping
4221    /// folded every non-identifier character to `_`, so 22,636 classes of
4222    /// `bin.mt.plus` (names built from Thai/Yi/Syriac code points) became
4223    /// three (`l.__`, `l.___`, `l._`) and all but three were overwritten.
4224    #[test]
4225    fn distinct_obfuscated_names_stay_distinct() {
4226        // The three names that collapsed onto `l/__.java` in the field.
4227        let names = ["l.᩻ܶ", "l.᩻ۡ", "l.֫᩷", "l.֡", "l.᩻᩶ۛ"];
4228        let mut mapped: Vec<String> = names.iter().map(|n| sanitize_fq(n)).collect();
4229        let before = mapped.len();
4230        mapped.sort();
4231        mapped.dedup();
4232        assert_eq!(mapped.len(), before, "sanitizer folded distinct names: {mapped:?}");
4233        assert!(
4234            mapped.iter().all(|m| m.is_ascii()),
4235            "sanitizer must stay ASCII: {mapped:?}"
4236        );
4237        // Self-describing: the escape carries the original code point.
4238        assert_eq!(sanitize_fq("l.᩻ܶ"), "l._u1a7b_u736");
4239    }
4240
4241    /// Every output must be a legal Java identifier segment: no leading
4242    /// digit, no keyword, not the lone `_` reserved since Java 9.
4243    #[test]
4244    fn output_is_a_legal_identifier() {
4245        for (input, want) in [
4246            ("do", "_do"),
4247            ("_", "__"),
4248            ("0Xx", "_0Xx"),
4249            ("Collection$-EL", "Collection$_u2dEL"),
4250            ("᩻", "_u1a7b"),
4251            ("a", "a"),
4252            ("A$B", "A$B"),
4253            ("x_y", "x_y"),
4254        ] {
4255            assert_eq!(sanitize_fq(input), want, "input {input:?}");
4256        }
4257    }
4258
4259    /// Documented residual: ident-safe ASCII passes through unchanged, so a
4260    /// literal `_u1a7b` collides with the single character U+1A7B. The
4261    /// collision is real — which is why `lossy_sanitize_renames` still owns
4262    /// a repair pass — and this test pins the shape the guard must catch.
4263    #[test]
4264    fn lookalike_collision_is_known_and_bounded() {
4265        assert_eq!(sanitize_fq("_u1a7b"), sanitize_fq("᩻"));
4266    }
4267}