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 shadow = inherited_field_shadows(pool, class);
154    let mut obscured_map = compute_obscured_renders(pool, class, &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        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/// Ctor argument signatures to mirror on an empty-shell class whose
1075/// ancestor chain ends at a Throwable-family FRAMEWORK class — read
1076/// from the embedded API database (the hand-written 26-name table it
1077/// replaced could not cover app-specific chains and hard-coded the
1078/// sig set instead of the real one). Private ctors are skipped: the
1079/// bridge `super(..)` would not compile against them (Throwable's
1080/// 4-arg suppression ctor is private).
1081fn throwable_ctor_sigs(fw: &str) -> Option<Vec<String>> {
1082    if !crate::fwdb::is_subtype(fw, "java/lang/Throwable") {
1083        return None;
1084    }
1085    let mut out: Vec<String> = Vec::new();
1086    crate::fwdb::for_each_method(fw, |d, f| {
1087        if d.starts_with("<init>(")
1088            && f & crate::fwdb::MF_STATIC == 0
1089            && f & crate::fwdb::MF_PRIVATE == 0
1090            && f & crate::fwdb::MF_UNKNOWN == 0
1091        {
1092            let hi = d.find(')').unwrap_or(d.len());
1093            let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1094            out.push(d[lo..hi].to_string());
1095        }
1096    });
1097    out.sort();
1098    out.dedup();
1099    Some(out)
1100}
1101
1102fn synth_missing_interface_stubs(
1103    pool: &DexPool,
1104    class: &PoolClass,
1105    out: &mut String,
1106    depth: usize,
1107    emitted_any: &mut bool,
1108) {
1109    use crate::access::*;
1110    // Concrete classes only: interfaces / abstract classes / annotations
1111    // legitimately leave methods unimplemented.
1112    if class.access & (ACC_INTERFACE | ACC_ABSTRACT | ACC_ANNOTATION) != 0 {
1113        return;
1114    }
1115    fn argsig(d: &str) -> &str {
1116        let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1117        let hi = d.find(')').unwrap_or(d.len());
1118        &d[lo..hi]
1119    }
1120    // Framework interface method lists come from the embedded API
1121    // database (fwdb) — see the lookup below.
1122
1123    // Transitive interface closure — seeded from the WHOLE superclass
1124    // chain, not just the class's own `implements`: weixin rd extends
1125    // abstract k0 implements ey2.a, R8 stripped rd's onAttach, and the
1126    // old own-interfaces-only seed saw an empty requirement (×51
1127    // weixin a.onAttach family). Pool interfaces contribute their
1128    // method lists; framework interfaces consult the table above.
1129    // (name, argsig) -> the unique declaring desc, or None when two
1130    // ancestors declare the same erasure with DIFFERENT descriptors —
1131    // a Java-inexpressible diamond where any synthesized stub would
1132    // itself fail "无法覆盖/无法实现" (weibo +1016 regression when the
1133    // first-wins desc was stubbed against a conflicting sibling).
1134    let mut required: jdc_core::FxHashMap<(String, String), Option<(String, String)>> =
1135        jdc_core::FxHashMap::default();
1136    let mut defaults: jdc_core::FxHashSet<(String, String)> = jdc_core::FxHashSet::default();
1137    let mut stack: Vec<String> = Vec::new();
1138    {
1139        let mut cur: Option<&PoolClass> = Some(class);
1140        let mut hops = 0;
1141        while let Some(c) = cur {
1142            hops += 1;
1143            if hops > 64 {
1144                break;
1145            }
1146            stack.extend(c.interfaces.iter().cloned());
1147            let mut hit_fw = false;
1148            cur = c.super_name.as_ref().and_then(|s| {
1149                if s == "java/lang/Object" {
1150                    None
1151                } else {
1152                    let pc = pool.get(s);
1153                    if pc.is_none() {
1154                        hit_fw = true;
1155                    }
1156                    pc
1157                }
1158            });
1159            if hit_fw {
1160                // A FRAMEWORK superclass contributes methods the pool
1161                // walk cannot see: the class may already provide (or be
1162                // barred from overriding — Context.getString is final)
1163                // any requirement, and a stub here collided ×669 weibo.
1164                // No stubs past the framework boundary.
1165                return;
1166            }
1167        }
1168    }
1169    let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
1170    while let Some(iname) = stack.pop() {
1171        if !seen.insert(iname.clone()) {
1172            continue;
1173        }
1174        let Some(ic) = pool.get(&iname) else {
1175            // Framework interface: the embedded API database provides
1176            // its abstract instance methods with real access flags
1177            // (abstract, not static, not default) — the hand-written
1178            // 13-interface table this replaced covered only classics.
1179            if crate::fwdb::class_flags(&iname) & crate::fwdb::CF_INTERFACE != 0 {
1180                crate::fwdb::for_each_method(&iname, |d, f| {
1181                    if f & crate::fwdb::MF_ABSTRACT == 0
1182                        || f & crate::fwdb::MF_STATIC != 0
1183                        || f & crate::fwdb::MF_UNKNOWN != 0
1184                        || d.starts_with("<init>")
1185                    {
1186                        return;
1187                    }
1188                    let hi = d.find('(').unwrap_or(d.len());
1189                    let mn = d[..hi].to_string();
1190                    let md = d[hi..].to_string();
1191                    let key = (mn.clone(), argsig(&md).to_string());
1192                    match required.entry(key) {
1193                        std::collections::hash_map::Entry::Vacant(v) => {
1194                            v.insert(Some((mn, md)));
1195                        }
1196                        std::collections::hash_map::Entry::Occupied(mut o) => {
1197                            if o.get().as_ref().is_some_and(|(_, d0)| *d0 != md) {
1198                                o.insert(None);
1199                            }
1200                        }
1201                    }
1202                });
1203            }
1204            continue;
1205        };
1206        for m in ic.all_methods() {
1207            if m.is_static() || &*m.name == "<clinit>" || &*m.name == "<init>" {
1208                continue;
1209            }
1210            let key = (m.name.to_string(), argsig(&m.desc).to_string());
1211            if m.code_off == 0 {
1212                let mdesc = m.desc.to_string();
1213                match required.entry(key) {
1214                    std::collections::hash_map::Entry::Vacant(v) => {
1215                        v.insert(Some((m.name.to_string(), mdesc)));
1216                    }
1217                    std::collections::hash_map::Entry::Occupied(mut o) => {
1218                        if o.get().as_ref().is_some_and(|(_, d)| *d != mdesc) {
1219                            o.insert(None);
1220                        }
1221                    }
1222                }
1223            } else {
1224                defaults.insert(key);
1225            }
1226        }
1227        stack.extend(ic.interfaces.iter().cloned());
1228    }
1229    if required.is_empty() {
1230        return;
1231    }
1232    // Methods provided by the class + its concrete superclass chain, plus
1233    // the java/lang/Object publics that satisfy common requirements.
1234    let mut provided: jdc_core::FxHashSet<(String, String)> = jdc_core::FxHashSet::default();
1235    provided.insert(("equals".into(), "Ljava/lang/Object;".into()));
1236    provided.insert(("hashCode".into(), String::new()));
1237    provided.insert(("toString".into(), String::new()));
1238    // (name, desc) pairs whose erasure twin exists elsewhere in the
1239    // chain with a DIFFERENT desc — stub-hostile shapes (see below).
1240    let mut chain_sigs: jdc_core::FxHashMap<(String, String), jdc_core::FxHashSet<String>> =
1241        jdc_core::FxHashMap::default();
1242    let mut cur: Option<&PoolClass> = Some(class);
1243    while let Some(c) = cur {
1244        for m in c.all_methods() {
1245            if !m.is_static() && &*m.name != "<init>" && &*m.name != "<clinit>" {
1246                chain_sigs
1247                    .entry((m.name.to_string(), argsig(&m.desc).to_string()))
1248                    .or_default()
1249                    .insert(m.desc.to_string());
1250            }
1251        }
1252        for m in c.all_methods() {
1253            // A method PROVIDES the implementation if it is not abstract:
1254            // bytecode (code_off != 0) OR native (JNI body, code_off == 0
1255            // but ACC_NATIVE — `…ToNative` methods satisfy their interface
1256            // and must not be stubbed twice).
1257            if m.access & ACC_ABSTRACT == 0 && !m.is_static() {
1258                // A RENAMED method no longer provides its raw (name,
1259                // argsig): impostor overrides (return type satisfying no
1260                // ancestor declaration) and erasure-clash losers render
1261                // under their registry display, and the ancestor
1262                // requirement falls to the stub below — matching ART,
1263                // where the interface proto never resolved either.
1264                let renamed = jdc_core::rename::field_display(&c.name, &m.name, &m.desc)
1265                    .is_some();
1266                if !renamed {
1267                    provided.insert((m.name.to_string(), argsig(&m.desc).to_string()));
1268                }
1269            }
1270        }
1271        cur = c.super_name.as_ref().and_then(|s| {
1272            if s == "java/lang/Object" {
1273                None
1274            } else {
1275                pool.get(s)
1276            }
1277        });
1278    }
1279    let mut chain_conflict: jdc_core::FxHashSet<(String, String)> =
1280        jdc_core::FxHashSet::default();
1281    for ((name, _argsig), descs) in chain_sigs.iter() {
1282        if descs.len() > 1 {
1283            for d in descs {
1284                chain_conflict.insert((name.clone(), d.clone()));
1285            }
1286        }
1287    }
1288    let mut missing: Vec<(String, String)> = Vec::new();
1289    for (key, m) in &required {
1290        let Some(m) = m else { continue }; // conflicting ancestor descs
1291        if defaults.contains(key) || provided.contains(key) {
1292            continue;
1293        }
1294        // Kotlin bridge variant (return kotlin/Unit beside the real
1295        // boolean/object twin a framework interface declares): stubbing
1296        // it renders `java.lang.Void addAll(Collection)` that neither
1297        // implements the twin nor serves its callers — the poisoned
1298        // return type cascades through every call site (weibo +703
1299        // regression). The honest 未覆盖 error for the twin stays.
1300        if m.1.ends_with(")Lkotlin/Unit;") {
1301            continue;
1302        }
1303        // A chain method sharing the erasure but NOT the descriptor
1304        // (abstract super with a different return, an impostor the
1305        // rename pass left because it satisfies another ancestor):
1306        // the stub would clash with it — skip; the class keeps the
1307        // single honest 未覆盖 error instead of gaining a 无法覆盖 one.
1308        if chain_conflict.contains(&(m.0.clone(), m.1.clone())) {
1309            continue;
1310        }
1311        missing.push(m.clone());
1312    }
1313    missing.sort();
1314    let ind = "    ".repeat(depth + 1);
1315    for (mname, mdesc) in missing {
1316        let lo = mdesc.find('(').map(|i| i + 1).unwrap_or(0);
1317        let hi = mdesc.find(')').unwrap_or(mdesc.len());
1318        let ret_s = &mdesc[hi + 1..];
1319        let args_v = split_arg_descs(&mdesc[lo..hi]);
1320        let d = ParsedStub { ret: crate::desc_type(ret_s), args: args_v };
1321        if *emitted_any {
1322            out.push('\n');
1323        }
1324        out.push_str(&ind);
1325        out.push_str("public ");
1326        out.push_str(&type_name(pool, &d.ret));
1327        out.push(' ');
1328        out.push_str(&java_ident(&mname));
1329        out.push('(');
1330        for (i, a) in d.args.iter().enumerate() {
1331            if i > 0 {
1332                out.push_str(", ");
1333            }
1334            out.push_str(&type_name(pool, a));
1335            out.push_str(&format!(" p{}", i + 1));
1336        }
1337        out.push_str(") {\n");
1338        out.push_str(&ind);
1339        out.push_str("    throw new AbstractMethodError();\n");
1340        out.push_str(&ind);
1341        out.push_str("}\n");
1342        *emitted_any = true;
1343    }
1344}
1345
1346/// Synthetic storage fields backing name()/ordinal() on a FALLBACK enum
1347/// (a plain class cannot inherit java.lang.Enum's). The enum ctor's
1348/// (String, int) trace params feed them (injected in emit_method).
1349const ENUM_NAME_FIELD: &str = "$ddcName";
1350const ENUM_ORD_FIELD: &str = "$ddcOrdinal";
1351
1352/// True when the class renders as a fallback enum BASE: ACC_ENUM,
1353/// restoration failed (enum_consts None — caller-gated), super is the
1354/// Enum root. Constant subclasses inherit the synthesized members.
1355fn fallback_enum_base(class: &PoolClass) -> bool {
1356    class.is_enum()
1357        && class
1358            .super_name
1359            .as_ref()
1360            .map(|s| s == "java/lang/Enum" || s == "java/lang/Object")
1361            .unwrap_or(true)
1362}
1363
1364/// Full gate for the synthetic Enum surface: fallback base WITH the
1365/// trace ctor and no `$ddc*` field-name clash. The ctor injection in
1366/// emit_method, the member synthesis and the dex-valueOf suppression
1367/// must all agree, or fields/ctor/renders desynchronize.
1368fn enum_synth_active(class: &PoolClass) -> bool {
1369    fallback_enum_base(class)
1370        && enum_trace_ctor(class)
1371        && !class
1372            .static_fields
1373            .iter()
1374            .chain(class.instance_fields.iter())
1375            .any(|f| f.name == ENUM_NAME_FIELD || f.name == ENUM_ORD_FIELD)
1376}
1377
1378/// Does the class have the compiler-generated `(String, int, ..)` enum
1379/// ctor whose leading params feed the synthetic name/ordinal fields?
1380fn enum_trace_ctor(class: &PoolClass) -> bool {
1381    class.all_methods().any(|m| {
1382        &*m.name == "<init>"
1383            && m.parsed_desc()
1384                .map(|d| {
1385                    matches!(d.args.first(), Some(JavaType::Object(s))
1386                        if s.as_ref() == "java/lang/String")
1387                        && matches!(d.args.get(1), Some(JavaType::Int))
1388                })
1389                .unwrap_or(false)
1390    })
1391}
1392
1393/// Synthesize the java.lang.Enum member surface a FALLBACK enum base
1394/// lacks (it renders as a plain class — `extends Enum` is illegal in
1395/// source): storage fields `$ddcName`/`$ddcOrdinal` (fed by the ctor
1396/// trace-param injection in emit_method), `name()`, `ordinal()`,
1397/// `valueOf(String)` and `compareTo`, plus `values()` when R8 stripped
1398/// the dex copy. Kills the illegal dex `Enum.valueOf(X.class, ..)`
1399/// ("无法将类 Enum<E>中的方法 valueOf应用到给定类型" — weixin ×1,307,
1400/// weibo ×477) and makes ordinal() EXACT (the interim values()-scan
1401/// misread enums whose $VALUES carries register-reuse duplicates,
1402/// protobuf ub). Purely additive: the dex never declares these members
1403/// on the enum class (they are Enum inheritances), so no collision —
1404/// except the dex valueOf, suppressed at the method loop.
1405fn synth_enum_members(
1406    pool: &DexPool,
1407    class: &PoolClass,
1408    out: &mut String,
1409    depth: usize,
1410    emitted_any: &mut bool,
1411) {
1412    use crate::access::*;
1413    if !fallback_enum_base(class) {
1414        return;
1415    }
1416    let self_ty = JavaType::Object(class.name.as_str().into());
1417    let self_name = type_name(pool, &self_ty);
1418    let self_arr = JavaType::Array(Box::new(self_ty));
1419    let arr_name = type_name(pool, &self_arr);
1420    let traced = enum_synth_active(class);
1421    let has_values = class.all_methods().any(|m| {
1422        &*m.name == "values"
1423            && m.access & ACC_STATIC != 0
1424            && m.parsed_desc()
1425                .map(|d| d.ret == self_arr && d.args.is_empty())
1426                .unwrap_or(false)
1427    });
1428    let ind = "    ".repeat(depth + 1);
1429    let push_line = |out: &mut String, l: &str| {
1430        out.push_str(&ind);
1431        out.push_str(l);
1432        out.push('\n');
1433    };
1434    // values(): from the $VALUES array field when the dex copy is gone.
1435    if !has_values {
1436        let want = format!("[L{};", class.name);
1437        if let Some(vf) = class.static_fields.iter().find(|f| f.desc == want) {
1438            if *emitted_any {
1439                out.push('\n');
1440            }
1441            *emitted_any = true;
1442            push_line(out, &format!("public static {} values() {{", arr_name));
1443            push_line(
1444                out,
1445                &format!("    return ({}) {}.clone();", arr_name, java_ident(&vf.name)),
1446            );
1447            push_line(out, "}");
1448        }
1449    }
1450    if !traced {
1451        return; // no (String,int) ctor: fields would stay default — keep
1452                // the scan-based ordinal only (below) and the dex valueOf.
1453    }
1454    if *emitted_any {
1455        out.push('\n');
1456    }
1457    *emitted_any = true;
1458    push_line(out, &format!("private java.lang.String {};", ENUM_NAME_FIELD));
1459    push_line(out, &format!("private int {};", ENUM_ORD_FIELD));
1460    out.push('\n');
1461    push_line(out, &format!(
1462        "public java.lang.String name() {{\n{}    return this.{};\n{}}}",
1463        ind, ENUM_NAME_FIELD, ind
1464    ));
1465    out.push('\n');
1466    push_line(out, &format!(
1467        "public int ordinal() {{\n{}    return this.{};\n{}}}",
1468        ind, ENUM_ORD_FIELD, ind
1469    ));
1470    out.push('\n');
1471    push_line(out, &format!(
1472        "public int compareTo({0} o) {{\n{1}    return this.{2} - o.{2};\n{1}}}",
1473        self_name, ind, ENUM_ORD_FIELD
1474    ));
1475    out.push('\n');
1476    push_line(out, &format!(
1477        "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}}}",
1478        self_name, ind
1479    ));
1480}
1481
1482/// Scan-based ordinal() fallback for fallback enums WITHOUT the trace
1483/// ctor (rare): position in values(). Kept separate so synth_enum_members
1484/// can bail after values() synthesis.
1485fn synth_enum_ordinal_scan(
1486    pool: &DexPool,
1487    class: &PoolClass,
1488    out: &mut String,
1489    depth: usize,
1490    emitted_any: &mut bool,
1491) {
1492    if !fallback_enum_base(class) || enum_trace_ctor(class) {
1493        return;
1494    }
1495    if class
1496        .all_methods()
1497        .any(|m| &*m.name == "ordinal" && &*m.desc == "()I")
1498    {
1499        return;
1500    }
1501    let self_arr = JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into())));
1502    let arr_name = type_name(pool, &self_arr);
1503    let ind = "    ".repeat(depth + 1);
1504    if *emitted_any {
1505        out.push('\n');
1506    }
1507    *emitted_any = true;
1508    out.push_str(&ind);
1509    out.push_str("public int ordinal() {\n");
1510    out.push_str(&ind);
1511    out.push_str(&format!("    {} vs = values();\n", arr_name));
1512    out.push_str(&ind);
1513    out.push_str("    for (int i = 0; i < vs.length; i++) {\n");
1514    out.push_str(&ind);
1515    out.push_str("        if (vs[i] == this) {\n");
1516    out.push_str(&ind);
1517    out.push_str("            return i;\n");
1518    out.push_str(&ind);
1519    out.push_str("        }\n");
1520    out.push_str(&ind);
1521    out.push_str("    }\n");
1522    out.push_str(&ind);
1523    out.push_str("    return 0;\n");
1524    out.push_str(&ind);
1525    out.push_str("}\n");
1526}
1527
1528/// Strip the Meituan Robust hotpatch guard from a RESTORED enum ctor:
1529/// `Object[] v3 = {name, ordinal}; Class[] v4 = ..; boolean s =
1530/// PatchProxy.isSupport(v3, this, changeQuickRedirect, ..); if (s) {
1531/// PatchProxy.accessDispatch(..); return; }`. The guard reads the enum's
1532/// own static field — bytecode-legal (Robust instruments after javac),
1533/// source-illegal (JLS 8.9.2, both simple AND qualified forms). It is
1534/// dead code for decompiled output (no patch is ever loaded). Removal is
1535/// surgical: statements containing a PatchProxy call, the locals feeding
1536/// its arguments, their defs and element writes — a fixpoint over the
1537/// top-level list. collect_enum_constants' dex gate already refused
1538/// restoration when self-static reads go beyond the redirect field.
1539fn strip_robust_enum_ctor_guard(body: &mut Stmt) {
1540    let is_patchy = |e: &Expr| {
1541        matches!(e, Expr::Method { cls, name, .. }
1542            if cls.contains("PatchProxy")
1543                && (&**name == "isSupport"
1544                    || &**name == "accessDispatch"
1545                    || &**name == "proxy"
1546                    || &**name == "isSupportClinit"
1547                    || &**name == "accessDispatchClinit"))
1548    };
1549    let Stmt::Block(vs) = body else { return };
1550    if !vs.iter().any(|st| {
1551        let mut h = false;
1552        crate::passes::visit_all_exprs(st, &mut |x| {
1553            if is_patchy(x) {
1554                h = true;
1555            }
1556        });
1557        h
1558    }) {
1559        return;
1560    }
1561    let mut dead: jdc_core::FxHashSet<u32> = jdc_core::FxHashSet::default();
1562    // Locals read inside the PatchProxy call args of a statement (the
1563    // pack arrays) — dead once the statement goes.
1564    let patchy_arg_locals = |st: &Stmt, out: &mut jdc_core::FxHashSet<u32>| {
1565        crate::passes::visit_all_exprs(st, &mut |x| {
1566            if let Expr::Method { args, .. } = x {
1567                if is_patchy(x) {
1568                    for a in args {
1569                        let mut aa = a.clone();
1570                        crate::passes::deep_rewrite(&mut aa, &mut |y| {
1571                            if let Expr::Local { var, .. } = y {
1572                                out.insert(*var);
1573                            }
1574                        });
1575                    }
1576                }
1577            }
1578        });
1579    };
1580    for _ in 0..4 {
1581        let before = vs.len();
1582        let mut i = 0usize;
1583        while i < vs.len() {
1584            let action = {
1585                let st = &vs[i];
1586                match st {
1587                    Stmt::LocalDef { var, .. } if dead.contains(var) => Some((*var, false)),
1588                    Stmt::LocalDef { var, init: Some(e), .. } => {
1589                        let mut has_patchy = false;
1590                        let mut ec = e.clone();
1591                        crate::passes::deep_rewrite(&mut ec, &mut |x| {
1592                            if is_patchy(x) {
1593                                has_patchy = true;
1594                            }
1595                        });
1596                        if has_patchy {
1597                            Some((*var, true))
1598                        } else {
1599                            None
1600                        }
1601                    }
1602                    Stmt::If { .. } => {
1603                        let mut h = false;
1604                        crate::passes::visit_all_exprs(st, &mut |x| {
1605                            if is_patchy(x) {
1606                                h = true;
1607                            }
1608                        });
1609                        if h {
1610                            Some((u32::MAX, true))
1611                        } else {
1612                            None
1613                        }
1614                    }
1615                    // `v3[0] = str;` / `v3 = ..;` writes to a dead pack.
1616                    Stmt::ExprStmt(Expr::Assign { target, .. }) => {
1617                        let mut tgt_dead = false;
1618                        let mut tc = target.clone();
1619                        crate::passes::deep_rewrite(&mut tc, &mut |x| {
1620                            if let Expr::Local { var, .. } = x {
1621                                if dead.contains(var) {
1622                                    tgt_dead = true;
1623                                }
1624                            }
1625                        });
1626                        if tgt_dead {
1627                            Some((u32::MAX, false))
1628                        } else {
1629                            None
1630                        }
1631                    }
1632                    _ => None,
1633                }
1634            };
1635            if let Some((var, collect_args)) = action {
1636                if collect_args {
1637                    patchy_arg_locals(&vs[i], &mut dead);
1638                }
1639                if var != u32::MAX {
1640                    dead.insert(var);
1641                }
1642                vs.remove(i);
1643            } else {
1644                i += 1;
1645            }
1646        }
1647        if vs.len() == before {
1648            break;
1649        }
1650    }
1651}
1652
1653#[allow(clippy::only_used_in_recursion)]
1654fn emit_class_body(
1655    pool: &DexPool,
1656    class: &PoolClass,
1657    ctx: &DexCtx<'_>,
1658    opts: &ClassOptions,
1659    out: &mut String,
1660    depth: usize,
1661) -> anyhow::Result<()> {
1662    let ind = indent(depth);
1663    // The class's own header must use the RENAMED display name — the
1664    // ctor path (is_init) already renames; a header declaring `class a`
1665    // while the file/ctor say `a_2` leaves the ctor looking like a
1666    // method with no return type (MinisApp's Y2.a vs y2.a package
1667    // case-collision: 2,518 javac parse errors).
1668    let cname = crate::apply_class_rename(&class.name);
1669    let (_, simple) = split_name(&cname);
1670    // A class emitted as its OWN top-level file (depth 0) must declare a
1671    // flat `$` name — `class Outer.Inner` is not declarable at file
1672    // scope. An INLINE nested member (depth > 0) declares its own
1673    // segment inside the parent's body.
1674    let simple = if depth == 0 {
1675        simple.to_string()
1676    } else {
1677        // R8 names can END in `$`: an empty last `$`-segment would
1678        // render `class  {` — keep the whole simple name.
1679        let seg = simple.rsplit('$').next().unwrap_or(&simple);
1680        if seg.is_empty() {
1681            simple.to_string()
1682        } else {
1683            seg.to_string()
1684        }
1685    };
1686    // A class NAMED `var`-style (taobao ships `tb.var`) cannot be
1687    // declared — restricted contextual type names escape here and in
1688    // the ctor, file name, and every type reference.
1689    let simple = if is_restricted_type_name(&simple) {
1690        format!("_{simple}")
1691    } else {
1692        simple
1693    };
1694    let is_iface = class.is_interface();
1695    let is_enum = class.is_enum();
1696
1697    let mut head = String::new();
1698    let a = class.access;
1699    if a & ACC_PUBLIC != 0 || widen_class(&class.name) {
1700        head.push_str("public ");
1701    }
1702    // Inline nested members need their `static` (interfaces/annotations
1703    // are implicitly static; a missing `static` on a member class makes
1704    // every `new Report(...)` site an "outer instance required" error).
1705    if depth > 0 && !is_iface && ctx.nested_is_static(&class.name) {
1706        head.push_str("static ");
1707    }
1708    if a & ACC_FINAL != 0 && !is_enum {
1709        head.push_str("final ");
1710    }
1711    if a & ACC_ABSTRACT != 0 && !is_iface {
1712        head.push_str("abstract ");
1713    }
1714    if a & ACC_ANNOTATION != 0 {
1715        head.push('@');
1716    }
1717    let mut enum_consts: Option<(Vec<EnumConst>, crate::method::MethodBody)> = if is_enum {
1718        collect_enum_constants(pool, class, ctx)
1719    } else {
1720        None
1721    };
1722    if let Some(ecs) = &enum_consts {
1723        // True `enum` declaration: constants render in the header, the
1724        // desugared boilerplate (const fields, their <clinit> inits —
1725        // stripped by strip_enum_const_inits — and the ACC_ENUM flags)
1726        // disappears. R8-renamed values()/valueOf() stay (they do not
1727        // collide with the compiler-generated ones); javac-named ones
1728        // are skipped at the method loop below.
1729        head.push_str("enum ");
1730        head.push_str(&sanitize_ref(&simple));
1731        let _ = ecs;
1732    } else if is_enum {
1733        // An enum with constant-specific bodies carries ACC_ABSTRACT —
1734        // `abstract final` is an illegal modifier combination; abstract
1735        // (already pushed above) suppresses the hardcoded final.
1736        let abstract_ = a & ACC_ABSTRACT != 0;
1737        head.push_str(if abstract_ {
1738            "/* enum */ class "
1739        } else {
1740            "/* enum */ final class "
1741        });
1742        head.push_str(&sanitize_ref(&simple));
1743    } else if is_iface {
1744        head.push_str("interface ");
1745        head.push_str(&sanitize_ref(&simple));
1746    } else {
1747        head.push_str("class ");
1748        head.push_str(&sanitize_ref(&simple));
1749    }
1750    // The obscured-super import was emitted at the package line; the
1751    // clause must render the SIMPLE name (the qualified form binds to
1752    // the class itself even with the import present).
1753    let render_super = |sup: &String| -> String { print_class_name(pool, sup) };
1754    if is_iface {
1755        // A @interface cannot declare extends at all (JLS 9.6): the dex
1756        // interface table lists java/lang/annotation/Annotation for
1757        // every annotation type — rendering it produced "对于
1758        // @interfaces, 不允许 'extends'" (weibo 1100).
1759        let is_annot = a & ACC_ANNOTATION != 0;
1760        if !class.interfaces.is_empty() && !is_annot {
1761            head.push_str(" extends ");
1762            head.push_str(&join_dotted(pool, &class.interfaces));
1763        }
1764    } else {
1765        if let Some(sup) = &class.super_name {
1766            // Suppress `extends` for TRUE `enum` declarations (they
1767            // implicitly extend java.lang.Enum) and for the enum BASE in
1768            // fallback form (super IS java/lang/Enum — `class X extends
1769            // Enum` is illegal). But an ENUM CONSTANT SUBCLASS rendered
1770            // in fallback (`/* enum */ class j$1`, ACC_ENUM, super = the
1771            // enum base) MUST keep `extends j`: without it the constant
1772            // loses its subtype relation and every `field = new j$1(..)`
1773            // fails ("j$1无法转换为j", weibo jsoup TokeniserState ×172).
1774            let suppress_extends =
1775                is_enum && (enum_consts.is_some() || sup == "java/lang/Enum");
1776            if sup != "java/lang/Object" && !suppress_extends {
1777                head.push_str(" extends ");
1778                head.push_str(&render_super(sup));
1779            }
1780        }
1781        if !class.interfaces.is_empty() {
1782            head.push_str(if is_iface {
1783                " extends "
1784            } else {
1785                " implements "
1786            });
1787            head.push_str(&join_dotted(pool, &class.interfaces));
1788        }
1789    }
1790
1791    out.push_str(&ind);
1792    out.push_str(&head);
1793    out.push_str(" {\n");
1794
1795    // True-enum constant list: the constants lead the body, ahead of
1796    // any remaining fields.
1797    if let Some((ecs, _)) = &enum_consts {
1798        for (i, ec) in ecs.iter().enumerate() {
1799            out.push_str(&indent(depth + 1));
1800            out.push_str(&java_ident(&ec.name));
1801            if !ec.extra_args.is_empty() {
1802                let mut line = String::from("(");
1803                render_enum_args(ctx, pool, &ec.extra_args, &mut line);
1804                line.push(')');
1805                out.push_str(&line);
1806            }
1807            if i + 1 < ecs.len() {
1808                out.push_str(",\n");
1809            } else {
1810                out.push_str(";\n");
1811            }
1812        }
1813        out.push('\n');
1814    }
1815
1816    // Fields.
1817    // A static final whose dex-encoded static value is OVERWRITTEN by
1818    // the clinit must render as a blank final: the dex encoded array
1819    // value is just the class-load default (ART lets <clinit> reassign
1820    // static finals), but Java forbids touching an initialized final
1821    // ("无法为 static final 变量 $stable 分配值" — Compose `$stable = 0`
1822    // + clinit `= 8`; WalletBaseUI HARDCODE_* = 0 + clinit computed
1823    // value; the original null-valued shape: Kotlin `object` INSTANCE,
1824    // okio SegmentPool). Scan the clinit's raw SPuts once.
1825    let clinit_sputs: Option<jdc_core::FxHashSet<(std::sync::Arc<str>, std::sync::Arc<str>)>> =
1826        // Gate: only classes with a VALUED final static can hit the
1827        // conflict — skip the clinit decode otherwise (decoding it for
1828        // every class cost seconds on 98k-class corpora).
1829        (|| {
1830            let has_valued_final = class.static_fields.iter().enumerate().any(|(i, f)| {
1831                f.access & crate::access::ACC_FINAL != 0
1832                    && class.static_values.get(i).is_some()
1833            });
1834            if !has_valued_final {
1835                return None;
1836            }
1837            let m = class.all_methods().find(|m| &*m.name == "<clinit>")?;
1838            let dex = pool.dex(m.dex_idx)?;
1839            let ci = dex.code_at(m.code_off)?;
1840            let mut set: jdc_core::FxHashSet<(
1841                std::sync::Arc<str>,
1842                std::sync::Arc<str>,
1843            )> = jdc_core::FxHashSet::default();
1844            for ins in &ci.insns {
1845                if let InsnKind::SPut { field_idx, .. } = ins.kind {
1846                    let fid = dex.field(field_idx);
1847                    // clinit only writes own-class fields here; match
1848                    // (name, type) against the field list below.
1849                    set.insert((
1850                        std::sync::Arc::from(dex.string(fid.name_idx)),
1851                        std::sync::Arc::from(dex.type_name(fid.type_idx)),
1852                    ));
1853                }
1854            }
1855            Some(set)
1856        })();
1857    let mut field_emitted = false;
1858    for (i, f) in class.static_fields.iter().enumerate() {
1859        // Enum constant fields became the header list above.
1860        if enum_consts.is_some() && f.access & crate::access::ACC_ENUM != 0 {
1861            continue;
1862        }
1863        if field_emitted || !class.instance_fields.is_empty() {
1864            out.push('\n');
1865        }
1866        field_emitted = true;
1867        // A blank-final: static final, clinit-assigned. Interfaces are
1868        // excluded — their fields are implicitly `public static final`
1869        // and MUST carry an initializer.
1870        let blank_final = f.access & crate::access::ACC_FINAL != 0
1871            && !class.is_interface()
1872            && clinit_sputs
1873                .as_ref()
1874                .is_some_and(|s| {
1875                    s.contains(&(
1876                        std::sync::Arc::from(f.name.as_str()),
1877                        std::sync::Arc::from(f.desc.as_str()),
1878                    ))
1879                });
1880        emit_field(
1881            pool,
1882            f,
1883            if blank_final {
1884                None
1885            } else {
1886                class.static_values.get(i)
1887            },
1888            &class.name,
1889            out,
1890            depth + 1,
1891            true,
1892            class.is_interface(),
1893        );
1894    }
1895    if !class.instance_fields.is_empty() && !class.static_fields.is_empty() {
1896        out.push('\n');
1897    }
1898    for (i, f) in class.instance_fields.iter().enumerate() {
1899        if i > 0 {
1900            out.push('\n');
1901        }
1902        emit_field(pool, f, None, &class.name, out, depth + 1, false, false);
1903    }
1904
1905    // Methods.
1906    // Duplicated (name, parameter-types) pairs in one class are javac
1907    // "method is already defined" errors (weibo: 11.5k hits). Source
1908    // cannot declare them, so exactly one survives: covariant bridges
1909    // (`Object get(int)` beside `ByteString get(int)` — the compiler
1910    // GENERATES bridges), plus R8 output that lost the bridge flag
1911    // (okio Buffer.clone, interface getView re-declarations). A
1912    // non-bridge method outranks a bridge for the same signature
1913    // (bridge bodies are delegation stubs); first occurrence otherwise.
1914    // Never drop a signature outright — the last method standing for a
1915    // key is always rendered.
1916    // Keyed on the DISPLAY name: a member-renamed method (return-type-
1917    // only collisions on isolated classes) no longer collides with its
1918    // sibling — both render, each caller resolving through the registry.
1919    fn sig_key<'m>(cls: &str, renames_on: bool, m: &'m PoolMethod) -> (&'m str, &'m str) {
1920        let d: &'m str = &m.desc;
1921        let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1922        let hi = d.find(')').unwrap_or(d.len());
1923        let n = if renames_on {
1924            jdc_core::rename::field_display(cls, &m.name, d).unwrap_or(&*m.name)
1925        } else {
1926            &*m.name
1927        };
1928        (n, &d[lo..hi])
1929    }
1930    let renames_on = jdc_core::rename::member_rename_active();
1931    let suppress_dex_valueof = class.is_enum()
1932        && enum_consts.is_none()
1933        && enum_synth_active(class);
1934    let methods: Vec<&PoolMethod> = class.all_methods().collect();
1935    let mut claim: jdc_core::FxHashMap<(&str, &str), usize> = jdc_core::FxHashMap::default();
1936    for (i, m) in methods.iter().enumerate() {
1937        let key = sig_key(&class.name, renames_on, m);
1938        match claim.get(&key) {
1939            Some(&j)
1940                if m.access & crate::access::ACC_BRIDGE == 0
1941                    && methods[j].access & crate::access::ACC_BRIDGE != 0 =>
1942            {
1943                claim.insert(key, i);
1944            }
1945            None => {
1946                claim.insert(key, i);
1947            }
1948            _ => {}
1949        }
1950    }
1951    // NOTE: a same-signature bridge is already retired by the claim map
1952    // above. An ERASURE-shaped bridge (SAM/variance: params are the
1953    // Object-erased types of a sibling's) must NOT be skipped: with raw
1954    // (non-generic) interface rendering the bridge is exactly what
1955    // satisfies the interface — skipping it turned every Kotlin lambda
1956    // class into "不是抽象的, 并且未覆盖…invoke(Object,Object)" (reqable
1957    // +17), while the "对invoke的引用不明确" it appeared to fix was a
1958    // missing-kotlin-classpath cascade, not a ddc bug.
1959    let mut emitted_any = !class.static_fields.is_empty() || !class.instance_fields.is_empty();
1960    for (i, m) in methods.iter().enumerate() {
1961        if &*m.name == "<clinit>" {
1962            continue; // rendered after the fields
1963        }
1964        if claim.get(&sig_key(&class.name, renames_on, m)) != Some(&i) {
1965            continue;
1966        }
1967        if bridge_shadowed_by_ancestor(pool, class, m) {
1968            continue;
1969        }
1970        // Fallback enum base: the dex valueOf body is
1971        // `Enum.valueOf(X.class, str)` — illegal once X is a plain class
1972        // (T is not bounded by Enum). Suppress it; synth_enum_members
1973        // renders the values()-scan replacement.
1974        if suppress_dex_valueof
1975            && m.is_static()
1976            && &*m.name == "valueOf"
1977            && m.parsed_desc().map(|d| {
1978                d.args.len() == 1
1979                    && matches!(d.args[0], JavaType::Object(ref sn)
1980                        if sn.as_ref() == "java/lang/String")
1981                    && d.ret == JavaType::Object(class.name.as_str().into())
1982            }).unwrap_or(false)
1983        {
1984            continue;
1985        }
1986        // True-enum rendering: javac auto-generates values()/valueOf()
1987        // — the original-named ones must not re-declare (R8-renamed
1988        // copies stay, they do not collide).
1989        if let Some((ecs, _)) = &enum_consts {
1990            let d = m.parsed_desc();
1991            let self_arr = d.as_ref().map(|d| d.ret == JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into()))));
1992            let self_ret = d.as_ref().map(|d| d.ret == JavaType::Object(class.name.as_str().into()));
1993            let no_args = d.as_ref().map(|d| d.args.is_empty()).unwrap_or(false);
1994            let one_str = d
1995                .as_ref()
1996                .map(|d| d.args.len() == 1 && matches!(d.args[0], JavaType::Object(ref n) if n.as_ref() == "java/lang/String"))
1997                .unwrap_or(false);
1998            let static_ = m.is_static();
1999            if static_ && no_args && self_arr == Some(true) && &*m.name == "values" {
2000                continue;
2001            }
2002            if static_ && one_str && self_ret == Some(true) && &*m.name == "valueOf" {
2003                continue;
2004            }
2005            // Enum constructors must be private in source form.
2006            let m_owned: Option<PoolMethod> = if &*m.name == "<init>" {
2007                let mut c = (*m).clone();
2008                c.access = (c.access & !(crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED | crate::access::ACC_PRIVATE)) | crate::access::ACC_PRIVATE;
2009                Some(c)
2010            } else {
2011                None
2012            };
2013            let m_ref: &PoolMethod = m_owned.as_ref().unwrap_or(m);
2014            let mark = out.len();
2015            if emitted_any {
2016                out.push('\n');
2017            }
2018            if emit_method(pool, class, ctx, m_ref, depth + 1, true, out)? {
2019                emitted_any = true;
2020            } else {
2021                out.truncate(mark);
2022            }
2023            let _ = ecs;
2024            continue;
2025        }
2026        let mark = out.len();
2027        if emitted_any {
2028            out.push('\n');
2029        }
2030        if emit_method(pool, class, ctx, m, depth + 1, enum_consts.is_some(), out)? {
2031            emitted_any = true;
2032        } else {
2033            out.truncate(mark);
2034        }
2035    }
2036    // Inherited-ctor bridges: dex method refs resolve through the
2037    // hierarchy, so `new C(args)` against a class C with NO declared
2038    // <init> legally targets the SUPERCLASS ctor (weixin tenpay
2039    // `new m(map)` — m declares nothing, i.<init>(HashMap) does). Java
2040    // has no inherited constructors: without a bridge, C's implicit
2041    // default ctor is the only one and every arg-carrying construction
2042    // fails ("无法将类 m中的构造器 m应用到给定类型; 需要: 没有参数").
2043    // Mirror the nearest ctor-declaring ancestor's public/protected
2044    // ctors as thin `super(..)` delegations.
2045    if !class.is_interface()
2046        && enum_consts.is_none()
2047        && !class.all_methods().any(|m| &*m.name == "<init>")
2048    {
2049        if let Some(mut sup) = class.super_name.clone() {
2050            loop {
2051                if sup == "java/lang/Object" {
2052                    break;
2053                }
2054                let Some(sc) = pool.get(&sup) else { break };
2055                let ctors: Vec<&PoolMethod> = sc
2056                    .all_methods()
2057                    .filter(|m| &*m.name == "<init>")
2058                    .collect();
2059                if !ctors.is_empty() {
2060                    for sm in ctors {
2061                        if sm.access & crate::access::ACC_PRIVATE != 0
2062                            || (sm.access
2063                                & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
2064                                == 0)
2065                        {
2066                            continue; // private/package-private: no legal bridge
2067                        }
2068                        let Some(d) = sm.parsed_desc() else { continue };
2069                        let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
2070                            "public "
2071                        } else {
2072                            "protected "
2073                        };
2074                        if emitted_any {
2075                            out.push('\n');
2076                        }
2077                        out.push_str(&format!("    {}", "    ".repeat(depth)));
2078                        out.push_str(mods);
2079                        out.push_str(&sanitize_ref(&simple));
2080                        out.push('(');
2081                        let mut names = Vec::with_capacity(d.args.len());
2082                        for (i, a) in d.args.iter().enumerate() {
2083                            if i > 0 {
2084                                out.push_str(", ");
2085                            }
2086                            out.push_str(&type_name(pool, a));
2087                            let nm = format!("p{}", i + 1);
2088                            out.push(' ');
2089                            out.push_str(&nm);
2090                            names.push(nm);
2091                        }
2092                        out.push_str(") {\n");
2093                        out.push_str(&format!("    {}    super({});\n", "    ".repeat(depth), names.join(", ")));
2094                        out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2095                        emitted_any = true;
2096                    }
2097                    break;
2098                }
2099                match &sc.super_name {
2100                    Some(n) => sup = n.clone(),
2101                    None => break,
2102                }
2103            }
2104        }
2105    }
2106
2107    // Referenced-but-undeclared inherited-ctor bridges. The block above
2108    // covers a class with NO declared ctor; this covers a class that
2109    // DECLARES ctors but is missing one that dex callers reach THROUGH
2110    // it. Dex resolves a `<init>` method-ref up the superclass chain, so
2111    // a subclass `super(2, recv, owner, name, sig, flags)` may target
2112    // `m.<init>(int,Object,Class,String,String,int)` that `m`
2113    // (FunctionReferenceImpl) does NOT declare — its super `l` does.
2114    // Java has no inherited ctors, so without a bridge on `m` every such
2115    // subclass fails ("无法将类 m中的构造器 m应用到给定类型"). Mirror each
2116    // referenced sig C lacks from the nearest ancestor declaring it, as a
2117    // thin `super(..)` delegation. Ref-gated (only sigs actually called
2118    // through C) so ordinary classes gain nothing.
2119    if !class.is_interface()
2120        && enum_consts.is_none()
2121        && class.all_methods().any(|m| &*m.name == "<init>")
2122    {
2123        if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
2124            fn arg_sig(d: &str) -> &str {
2125                let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2126                let hi = d.find(')').unwrap_or(d.len());
2127                &d[lo..hi]
2128            }
2129            let declared: jdc_core::FxHashSet<&str> = class
2130                .all_methods()
2131                .filter(|m| &*m.name == "<init>")
2132                .map(|m| arg_sig(&m.desc))
2133                .collect();
2134            let mut missing: Vec<&String> = refs
2135                .iter()
2136                .filter(|s| !declared.contains(s.as_str()))
2137                .collect();
2138            missing.sort();
2139            for sig in missing {
2140                // Nearest ancestor declaring a public/protected <init>
2141                // with this exact arg signature.
2142                let mut sup = class.super_name.clone();
2143                let mut found: Option<&PoolMethod> = None;
2144                while let Some(s) = sup {
2145                    if s == "java/lang/Object" {
2146                        break;
2147                    }
2148                    let Some(sc) = pool.get(&s) else { break };
2149                    if let Some(sm) = sc.all_methods().find(|m| {
2150                        &*m.name == "<init>"
2151                            && arg_sig(&m.desc) == sig.as_str()
2152                            && m.access & crate::access::ACC_PRIVATE == 0
2153                            && m.access
2154                                & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
2155                                != 0
2156                    }) {
2157                        found = Some(sm);
2158                        break;
2159                    }
2160                    sup = sc.super_name.clone();
2161                }
2162                let Some(sm) = found else {
2163                    // The walk terminated at java/lang/Object — which
2164                    // always declares <init>()V, so an empty-sig ref
2165                    // through this class resolved there dex-wise. The
2166                    // Java implicit default ctor is gone (the class
2167                    // declares other ctors); mirror it as an explicit
2168                    // public bridge (AgentMenuTask$Request →
2169                    // AppBrandProxyUIProcessTask$ProcessRequest:
2170                    // "需要: Parcel 找到: 没有参数", weixin 205).
2171                    if sig.is_empty() {
2172                        if emitted_any {
2173                            out.push('\n');
2174                        }
2175                        out.push_str(&format!("    {}", "    ".repeat(depth)));
2176                        out.push_str("public ");
2177                        out.push_str(&sanitize_ref(&simple));
2178                        out.push_str("() {\n");
2179                        out.push_str(&format!(
2180                            "    {}    super();\n",
2181                            "    ".repeat(depth)
2182                        ));
2183                        out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2184                        emitted_any = true;
2185                    }
2186                    continue;
2187                };
2188                let Some(d) = sm.parsed_desc() else { continue };
2189                let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
2190                    "public "
2191                } else {
2192                    "protected "
2193                };
2194                if emitted_any {
2195                    out.push('\n');
2196                }
2197                out.push_str(&format!("    {}", "    ".repeat(depth)));
2198                out.push_str(mods);
2199                out.push_str(&sanitize_ref(&simple));
2200                out.push('(');
2201                let mut names = Vec::with_capacity(d.args.len());
2202                for (i, a) in d.args.iter().enumerate() {
2203                    if i > 0 {
2204                        out.push_str(", ");
2205                    }
2206                    out.push_str(&type_name(pool, a));
2207                    let nm = format!("p{}", i + 1);
2208                    out.push(' ');
2209                    out.push_str(&nm);
2210                    names.push(nm);
2211                }
2212                out.push_str(") {\n");
2213                out.push_str(&format!(
2214                    "    {}    super({});\n",
2215                    "    ".repeat(depth),
2216                    names.join(", ")
2217                ));
2218                out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2219                emitted_any = true;
2220            }
2221        }
2222    }
2223
2224    // Framework-ancestor ctor bridge. The two blocks above mirror POOL
2225    // ancestors; both stop at a framework (off-pool) superclass. A class
2226    // whose DIRECT superclass is a framework type (in android.jar) that
2227    // declares a ctor the class lacks also needs a bridge — `super(args)`
2228    // resolves against the jar. Only the DIRECT-super-is-framework case is
2229    // safe: an intermediate pool ancestor with no matching ctor would
2230    // break the `super()` chain (that ancestor needs its own bridge, which
2231    // its own refs may not trigger). Covers weixin g3 extends
2232    // org.xml.sax.SAXException (`new g3(String)` → SAXException(String)),
2233    // the custom-exception / Parcelable family the pool walk can't reach.
2234    if !class.is_interface() && enum_consts.is_none() {
2235        let direct_fw = class
2236            .super_name
2237            .as_ref()
2238            .map(|s| s != "java/lang/Object" && pool.get(s).is_none())
2239            .unwrap_or(false);
2240        if direct_fw {
2241            if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
2242                fn arg_sig(d: &str) -> &str {
2243                    let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
2244                    let hi = d.find(')').unwrap_or(d.len());
2245                    &d[lo..hi]
2246                }
2247                let declared: jdc_core::FxHashSet<&str> = class
2248                    .all_methods()
2249                    .filter(|m| &*m.name == "<init>")
2250                    .map(|m| arg_sig(&m.desc))
2251                    .collect();
2252                let mut missing: Vec<&String> = refs
2253                    .iter()
2254                    .filter(|s| !declared.contains(s.as_str()))
2255                    .collect();
2256                missing.sort();
2257                for sig in missing {
2258                    // Never bridge the empty sig here: a no-ctor class
2259                    // gets Java's implicit default, and a class WITH
2260                    // ctors is covered by the referenced-but-undeclared
2261                    // block's Object-walk tail (emitting in both was
2262                    // `已在类 b0中定义了构造器 b0()` ×6 weixin).
2263                    if sig.is_empty() {
2264                        continue;
2265                    }
2266                    let args = split_arg_descs(sig);
2267                    if emitted_any {
2268                        out.push('\n');
2269                    }
2270                    out.push_str(&format!("    {}", "    ".repeat(depth)));
2271                    out.push_str("public ");
2272                    out.push_str(&sanitize_ref(&simple));
2273                    out.push('(');
2274                    let mut names = Vec::with_capacity(args.len());
2275                    for (i, a) in args.iter().enumerate() {
2276                        if i > 0 {
2277                            out.push_str(", ");
2278                        }
2279                        out.push_str(&type_name(pool, a));
2280                        let nm = format!("p{}", i + 1);
2281                        out.push(' ');
2282                        out.push_str(&nm);
2283                        names.push(nm);
2284                    }
2285                    out.push_str(") {\n");
2286                    out.push_str(&format!(
2287                        "    {}    super({});\n",
2288                        "    ".repeat(depth),
2289                        names.join(", ")
2290                    ));
2291                    out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2292                    emitted_any = true;
2293                }
2294            }
2295        }
2296    }
2297
2298    // Throwable-family ctor bridge for empty-shell exception classes
2299    // (see throwable_ctor_sigs). Walks the pool chain to the nearest
2300    // FRAMEWORK ancestor, stopping at any pool ancestor that DECLARES
2301    // ctors (the mirror block above covers that shape) — so chains of
2302    // empty shells (C extends P extends Error) each get the bridges and
2303    // every super(..) resolves against its parent's bridge.
2304    if !class.is_interface()
2305        && enum_consts.is_none()
2306        && !class.all_methods().any(|m| &*m.name == "<init>")
2307    {
2308        let mut fw_anc: Option<&str> = None;
2309        if let Some(sup) = class.super_name.as_deref() {
2310            let mut cur = sup;
2311            let mut hops = 0;
2312            loop {
2313                hops += 1;
2314                if hops > 32 || cur == "java/lang/Object" {
2315                    break;
2316                }
2317                match pool.get(cur) {
2318                    Some(sc) => {
2319                        if sc.all_methods().any(|m| &*m.name == "<init>") {
2320                            break;
2321                        }
2322                        match sc.super_name.as_deref() {
2323                            Some(n) => cur = n,
2324                            None => break,
2325                        }
2326                    }
2327                    None => {
2328                        fw_anc = Some(cur);
2329                        break;
2330                    }
2331                }
2332            }
2333        }
2334        if let Some(sigs) = fw_anc.and_then(throwable_ctor_sigs) {
2335            // The framework-ancestor bridge above already emitted every
2336            // sig the dex method-ref table attributes to this class —
2337            // emitting the table copy too was `已在类 z中定义了构造器
2338            // z(String)` (+60 weixin dup-def). The empty sig is exempt
2339            // there, so the table keeps it.
2340            let ref_sigs = pool.ctor_ref_sigs(&class.name);
2341            for sig in &sigs {
2342                if !sig.is_empty()
2343                    && ref_sigs.is_some_and(|rs| rs.contains(sig))
2344                {
2345                    continue;
2346                }
2347                let args = split_arg_descs(sig);
2348                if emitted_any {
2349                    out.push('\n');
2350                }
2351                out.push_str(&format!("    {}", "    ".repeat(depth)));
2352                out.push_str("public ");
2353                out.push_str(&sanitize_ref(&simple));
2354                out.push('(');
2355                let mut names = Vec::with_capacity(args.len());
2356                for (i, a) in args.iter().enumerate() {
2357                    if i > 0 {
2358                        out.push_str(", ");
2359                    }
2360                    out.push_str(&type_name(pool, a));
2361                    let nm = format!("p{}", i + 1);
2362                    out.push(' ');
2363                    out.push_str(&nm);
2364                    names.push(nm);
2365                }
2366                out.push_str(") {\n");
2367                out.push_str(&format!(
2368                    "    {}    super({});\n",
2369                    "    ".repeat(depth),
2370                    names.join(", ")
2371                ));
2372                out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
2373                emitted_any = true;
2374            }
2375        }
2376    }
2377
2378    // Static resolver helpers extracted from branched-delegation ctors
2379    // (passes::extract_branched_delegation_helper) — the computation the
2380    // Java first-statement rule cannot host inline.
2381    for h in crate::passes::take_ctor_helpers(&class.name) {
2382        if emitted_any {
2383            out.push('\n');
2384        }
2385        out.push_str(&indent(depth + 1));
2386        out.push_str("private static ");
2387        out.push_str(&type_name(pool, &h.ret));
2388        out.push(' ');
2389        out.push_str(&h.name);
2390        out.push('(');
2391        for (i, (ty, nm)) in h.params.iter().enumerate() {
2392            if i > 0 {
2393                out.push_str(", ");
2394            }
2395            out.push_str(&type_name(pool, ty));
2396            out.push(' ');
2397            out.push_str(&java_ident(nm));
2398        }
2399        out.push_str(") {\n");
2400        let p = Printer::new(ctx, &h.vt);
2401        let text = p.with_indent(depth + 2).into_string(&h.body);
2402        for line in text.lines() {
2403            if line.trim().is_empty() {
2404                continue;
2405            }
2406            out.push_str(line);
2407            out.push('\n');
2408        }
2409        out.push_str(&indent(depth + 1));
2410        out.push_str("}\n");
2411        emitted_any = true;
2412    }
2413
2414    // Missing-abstract-method stubs (R8 tree-shook an interface method a
2415    // concrete class no longer implements; javac rejects the incomplete
2416    // class). Synthesized last so they sit after the real members.
2417    synth_missing_interface_stubs(pool, class, out, depth, &mut emitted_any);
2418
2419    // Fallback enum base: recover the Enum member surface the
2420    // plain-class rendering lacks (name/ordinal/valueOf/compareTo +
2421    // synthetic storage fields fed by the ctor injection).
2422    if class.is_enum() && enum_consts.is_none() {
2423        synth_enum_members(pool, class, out, depth, &mut emitted_any);
2424        synth_enum_ordinal_scan(pool, class, out, depth, &mut emitted_any);
2425    }
2426
2427    // Static initializer. INTERFACES cannot carry a `static { }` block in
2428    // Java — their clinit only assigns constants, which static_values (or
2429    // the `= null` default) already render as field initializers; skip
2430    // the block entirely.
2431    let skip_clinit = class.is_interface();
2432    if let Some((_, clinit_body)) = enum_consts.take() {
2433        // True-enum <clinit>: the constant assignments are already gone;
2434        // render the remainder (the $VALUES array build) directly.
2435        // Skip an empty remainder (all statements were constant inits).
2436        let empty = match &clinit_body.body {
2437            Stmt::Block(v) => v.iter().all(|s| matches!(s, Stmt::Block(b) if b.is_empty())),
2438            _ => false,
2439        };
2440        if !empty {
2441            if emitted_any {
2442                out.push('\n');
2443            }
2444            out.push_str(&indent(depth + 1));
2445            out.push_str("static {\n");
2446            let p = Printer::new(ctx, &clinit_body.vt);
2447            let text = p.with_indent(depth + 2).into_string(&clinit_body.body);
2448            for line in text.lines() {
2449                if line.trim().is_empty() {
2450                    continue;
2451                }
2452                out.push_str(line);
2453                out.push('\n');
2454            }
2455            out.push_str(&indent(depth + 1));
2456            out.push_str("}\n");
2457            emitted_any = true;
2458        }
2459    } else if let Some(clinit) = (!skip_clinit)
2460        .then(|| class.all_methods().find(|m| &*m.name == "<clinit>"))
2461        .flatten()
2462    {
2463        let mark = out.len();
2464        if emitted_any {
2465            out.push('\n');
2466        }
2467        if emit_method(pool, class, ctx, clinit, depth + 1, false, out)? {
2468            emitted_any = true;
2469        } else {
2470            out.truncate(mark);
2471        }
2472    }
2473
2474    // Nested member classes (clean `$` tails only; anonymous/local/lambda
2475    // classes are emitted as their own top-level files by the driver).
2476    for nested in nested_members(pool, class, ctx) {
2477        if emitted_any {
2478            out.push('\n');
2479        }
2480        let nested_ctx = DexCtx::new(pool, nested);
2481        out.push('\n');
2482        emit_class_body(pool, nested, &nested_ctx, opts, out, depth + 1)?;
2483        emitted_any = true;
2484    }
2485
2486    out.push_str(&ind);
2487    out.push_str("}\n");
2488    Ok(())
2489}
2490
2491/// Member classes of `class` (direct children by the `$` chain / member
2492/// annotations), excluding anonymous / local / lambda shapes.
2493fn nested_members<'a>(
2494    pool: &'a DexPool,
2495    class: &'a PoolClass,
2496    ctx: &DexCtx<'_>,
2497) -> Vec<&'a PoolClass> {
2498    let mut out = Vec::new();
2499    for name in pool.children_of(&class.name) {
2500        let Some(pc) = pool.get(name) else { continue };
2501        // Only a REAL `outer$tail` name is an inline member: the
2502        // children index also carries annotation-derived outers
2503        // (EnclosingClass) with no naming relationship to the child
2504        // (obfuscated apps pair a 1-char name with a long enclosing
2505        // descriptor) — the blind slice panicked there (alipay
2506        // `a.a.a.a.c`, exposed once children_of actually returned data).
2507        let Some(rest) = name
2508            .strip_prefix(class.name.as_str())
2509            .and_then(|t| t.strip_prefix('$'))
2510        else {
2511            continue;
2512        };
2513        // The SAME predicate top_level_classes uses for the standalone
2514        // decision: a non-clean member tail ($ExternalSynthetic flat
2515        // units, hyphen markers, digit-led anonymous/local shapes) is
2516        // emitted as its own file — inlining it here too DOUBLE-EMITS
2517        // the class, and the nested copy renders corrupt (weibo:
2518        // 12,152 nested `ExternalSyntheticLambdaN` chimeras across
2519        // 4,316 files; Recorder's nested copy mixed a sibling's
2520        // captures — `int f$0` + Builder.setSource — 无法取消引用int).
2521        if !crate::clean_member_tail(rest) {
2522            continue;
2523        }
2524        if ctx.find_outer(name).as_deref() != Some(class.name.as_str()) {
2525            continue;
2526        }
2527        out.push(pc);
2528    }
2529    out
2530}
2531
2532// Eight parameters are all load-bearing (pool, field, static value,
2533// owner class, output, depth, staticness, interface-init requirement);
2534// bundling them into a struct would obscure each call site.
2535#[allow(clippy::too_many_arguments)]
2536fn emit_field(
2537    pool: &DexPool,
2538    f: &crate::PoolField,
2539    init: Option<&StaticValue>,
2540    f_class: &str,
2541    out: &mut String,
2542    depth: usize,
2543    is_static: bool,
2544    require_init: bool,
2545) {
2546    let ind = indent(depth);
2547    let mut line = String::new();
2548    let a = f.access;
2549    if a & ACC_PUBLIC != 0 || widen_field(f_class, &f.name) {
2550        line.push_str("public ");
2551    } else if a & ACC_PRIVATE != 0 {
2552        line.push_str("private ");
2553    } else if a & ACC_PROTECTED != 0 {
2554        line.push_str("protected ");
2555    }
2556    if is_static {
2557        line.push_str("static ");
2558    }
2559    if a & ACC_FINAL != 0 && !unfinal_field(f_class, &f.name) {
2560        line.push_str("final ");
2561    }
2562    if a & ACC_SYNTHETIC != 0 {
2563        line.push_str("/* synthetic */ ");
2564    }
2565    if a & ACC_TRANSIENT_HINT != 0 {
2566        line.push_str("transient ");
2567    }
2568    if a & ACC_VOLATILE_HINT != 0 {
2569        line.push_str("volatile ");
2570    }
2571    let ty = type_name(pool, &desc_type(&f.desc));
2572    line.push_str(&ty);
2573    line.push(' ');
2574    let fname = jdc_core::rename::field_display(f_class, &f.name, &f.desc).unwrap_or(&*f.name);
2575    line.push_str(&java_ident(fname));
2576    let mut rendered = None;
2577    if let Some(v) = init {
2578        rendered = render_static_value(pool, v, f_class);
2579    }
2580    if rendered.is_none() && require_init {
2581        // Interface fields MUST have an initializer in Java; the dex may
2582        // not carry a static_values entry for a compile-time-constant the
2583        // compiler folded away. Keep it compilable.
2584        let default = match desc_type(&f.desc) {
2585            JavaType::Boolean => "false",
2586            JavaType::Byte | JavaType::Short | JavaType::Char | JavaType::Int => "0",
2587            JavaType::Long => "0L",
2588            JavaType::Float => "0.0F",
2589            JavaType::Double => "0.0",
2590            _ => "null",
2591        };
2592        rendered = Some(default.to_string());
2593    }
2594    if let Some(text) = rendered {
2595        // A long field holding an `Int(i64)` static value needs the `L`
2596        // suffix: without it the literal is an int and overflows
2597        // (`long d = -6343169151696340687` failed javac).
2598        let text = if matches!(desc_type(&f.desc), JavaType::Long)
2599            && text.chars().all(|c| c.is_ascii_digit() || c == '-')
2600        {
2601            format!("{text}L")
2602        } else {
2603            text
2604        };
2605        line.push_str(" = ");
2606        line.push_str(&text);
2607    }
2608    line.push(';');
2609    out.push_str(&ind);
2610    out.push_str(&line);
2611    out.push('\n');
2612}
2613
2614// DEX field access_flags bits 0x40/0x80 are bridge/varargs for METHODS and
2615// volatile(0x40)/transient(0x80) for fields.
2616pub const ACC_VOLATILE_HINT: u32 = 0x40;
2617pub const ACC_TRANSIENT_HINT: u32 = 0x80;
2618
2619fn render_static_value(pool: &DexPool, v: &StaticValue, owner: &str) -> Option<String> {
2620    Some(match v {
2621        StaticValue::Int(i) => i.to_string(),
2622        StaticValue::Float(f) => format_float(*f as f64, true),
2623        StaticValue::Double(d) => format_float(*d, false),
2624        StaticValue::Str(s) => format!("\"{}\"", escape_string(s)),
2625        StaticValue::Type(t) => format!("{}.class", dotted(t)),
2626        StaticValue::Boolean(b) => b.to_string(),
2627        StaticValue::Null => "null".into(),
2628        StaticValue::Field(cls, name, desc) => {
2629            // The rename registry owns the display name (obscuring-field
2630            // renames key on (owner, name, desc)) — a const field ref must
2631            // render the SAME name as the declaration.
2632            let n: std::borrow::Cow<str> = match jdc_core::rename::field_display(cls, name, desc) {
2633                Some(d) => std::borrow::Cow::Borrowed(d),
2634                None => java_ident(name),
2635            };
2636            if cls == owner {
2637                n.into_owned()
2638            } else {
2639                format!("{}.{}", print_class_name(pool, cls), n)
2640            }
2641        }
2642        StaticValue::Other => return None,
2643    })
2644}
2645
2646/// Render one method straight into the class buffer `out`. Returns
2647/// whether anything was written (skips: no descriptor, deferred to a
2648/// monitored thread). The old shape returned a per-method String that
2649/// the caller copied in — one extra full copy of every method body.
2650/// `enum_promoted`: the class rendered as a true `enum` declaration
2651/// (constants in the header), which changes what a ctor signature may
2652/// declare.
2653/// d8's covariant-return bridges: `Context getContext() { return
2654/// super.getContext(); }` shadowing MMActivity's covariant
2655/// `AppCompatActivity getContext()`. Source cannot declare the
2656/// wider-return override, and the rendered bridge HIJACKS every
2657/// `this.getContext()` in the file to the wide type (weixin
2658/// FTSBaseVoiceSearchUI's `AppCompatActivity context =
2659/// this.getContext();` — Context无法转换为AppCompatActivity ×74). The
2660/// inherited covariant method satisfies source callers natively and
2661/// javac regenerates binary bridges on demand — skip the render.
2662///
2663/// TWO gates, both learned the hard way (the ungated ACC_BRIDGE +
2664/// ancestor-name version skipped erasure/interface-satisfier bridges
2665/// and cost lark +29k — the claim-map's raw-interface lesson at whole-
2666/// hierarchy scale):
2667/// 1. INSTRUCTION SHAPE: the body must be exactly invoke-super to a
2668///    same-named method + optional move-result + return (≤3 insns).
2669///    Erasure bridges call THIS with casts; interface satisfiers are
2670///    real bodies — both keep rendering.
2671/// 2. POOL ancestor (super chain, exact name+args, non-private)
2672///    declares the covariant source. Framework ancestors are not
2673///    consulted (fwdb carries no arg descs — a name-only match could
2674///    skip a real overload); such bridges keep rendering.
2675fn bridge_shadowed_by_ancestor(pool: &DexPool, class: &PoolClass, m: &PoolMethod) -> bool {
2676    if m.access & crate::access::ACC_BRIDGE == 0 || &*m.name == "<init>" || m.code_off == 0 {
2677        return false;
2678    }
2679    let Some(d) = m.parsed_desc() else {
2680        return false;
2681    };
2682    // Gate 1: invoke-super-same-name + move-result*/return* tail only.
2683    let Some(dex) = pool.dex(m.dex_idx) else {
2684        return false;
2685    };
2686    let dex = &*dex;
2687    let Some(code) = dex.code_insns_bytes_at(m.code_off) else {
2688        return false;
2689    };
2690    let mut head_ok = false;
2691    let mut bad_tail = false;
2692    let mut cnt = 0usize;
2693    let mut first = true;
2694    ddc_dex::insn::scan_instructions(code, &mut |op, pc, bytes| {
2695        cnt += 1;
2696        if first {
2697            first = false;
2698            if op == 0x6f || op == 0x75 {
2699                let unit = bytes
2700                    .get(2 * (pc + 1)..2 * (pc + 1) + 2)
2701                    .map(|b| u16::from_le_bytes([b[0], b[1]]) as u32)
2702                    .unwrap_or(u32::MAX);
2703                if unit != u32::MAX {
2704                    let mr = dex.method(unit);
2705                    head_ok = dex.string(mr.name_idx) == &*m.name;
2706                }
2707            }
2708        } else if !matches!(op, 0x0a | 0x0b | 0x0c | 0x0e | 0x0f | 0x10 | 0x11) {
2709            bad_tail = true;
2710        }
2711    });
2712    if !head_ok || bad_tail || !(2..=3).contains(&cnt) {
2713        return false;
2714    }
2715    // Gate 2: pool super chain declares name+args.
2716    let mut cur: Option<String> = class.super_name.clone();
2717    let mut hops = 0u32;
2718    while let Some(s) = cur {
2719        hops += 1;
2720        if hops > 64 {
2721            return false;
2722        }
2723        let Some(pc) = pool.get_if_materialized(&s) else {
2724            return false; // unresolvable ancestor — keep the bridge
2725        };
2726        if pc.all_methods().any(|am| {
2727            am.name == m.name
2728                && am.access & crate::access::ACC_PRIVATE == 0
2729                && am.parsed_desc().is_some_and(|ad| ad.args == d.args)
2730        }) {
2731            return true;
2732        }
2733        cur = pc.super_name.clone();
2734    }
2735    false
2736}
2737
2738fn emit_method(
2739    pool: &DexPool,
2740    class: &PoolClass,
2741    ctx: &DexCtx<'_>,
2742    m: &PoolMethod,
2743    depth: usize,
2744    enum_promoted: bool,
2745    out: &mut String,
2746) -> anyhow::Result<bool> {
2747    let ind = indent(depth);
2748    let desc = m.parsed_desc();
2749
2750    // Signature.
2751    let mut sig = String::with_capacity(192);
2752    let a = m.access;
2753    if a & ACC_PUBLIC != 0 || widen_method(&class.name, &m.name, &m.desc) {
2754        sig.push_str("public ");
2755    } else if a & ACC_PRIVATE != 0 {
2756        // A fallback enum BASE (`/* enum */ class`, not a promoted
2757        // `enum`) renders its constant-specific-body subclasses as
2758        // SEPARATE same-package classes (`j$6 extends j`); their ctors
2759        // call `super(name, ordinal, ..)`, which a PRIVATE base ctor
2760        // forbids ("j(String,int) has private access"). Widen the base
2761        // ctor to package-private so the subclass links. The base is
2762        // abstract → no external instantiation risk. Only the BASE
2763        // (super is Enum/Object), not the constant subclass itself.
2764        let fallback_enum_base_ctor = class.is_enum()
2765            && !enum_promoted
2766            && &*m.name == "<init>"
2767            && class
2768                .super_name
2769                .as_ref()
2770                .map(|s| s == "java/lang/Enum" || s == "java/lang/Object")
2771                .unwrap_or(true);
2772        if !fallback_enum_base_ctor {
2773            sig.push_str("private ");
2774        }
2775    } else if a & ACC_PROTECTED != 0 {
2776        sig.push_str("protected ");
2777    }
2778    let is_clinit = &*m.name == "<clinit>";
2779    let is_init = &*m.name == "<init>";
2780    if a & ACC_STATIC != 0 || is_clinit {
2781        sig.push_str("static ");
2782    }
2783    if a & ACC_FINAL != 0 {
2784        sig.push_str("final ");
2785    }
2786    // `abstract synchronized` is an illegal combination — obfuscated
2787    // builds mark abstract bridges synchronized (WhatsApp
2788    // SQLiteOpenHelper).
2789    if a & (ACC_SYNCHRONIZED | ACC_DECLARED_SYNCHRONIZED) != 0 && a & ACC_ABSTRACT == 0 {
2790        sig.push_str("synchronized ");
2791    }
2792    if a & ACC_NATIVE != 0 {
2793        sig.push_str("native ");
2794    }
2795    if a & ACC_ABSTRACT != 0 {
2796        sig.push_str("abstract ");
2797    }
2798    if a & ACC_SYNTHETIC != 0 {
2799        sig.push_str("/* synthetic */ ");
2800    }
2801
2802    // Body (needed for parameter names even for abstract methods).
2803    let mut body = decompile_method(pool, class, m).ok().flatten();
2804    // An interface method WITH a body is a `default` method (JLS 9.4.3)
2805    // unless static/private — dex carries no `default` flag, so the
2806    // plain form rendered an abstract signature with a body and javac
2807    // rejected every one ("接口抽象方法不能带有主体", weixin 1138). A
2808    // default REQUIRES a body: only push it once the body is confirmed
2809    // (a failed decompile renders a body-less declaration).
2810    if class.is_interface()
2811        && a & (ACC_STATIC | ACC_ABSTRACT | ACC_NATIVE | ACC_PRIVATE | ACC_ANNOTATION) == 0
2812        && body.is_some()
2813    {
2814        sig.insert_str(0, "default ");
2815    }
2816    let param_names: Vec<String> = body
2817        .as_ref()
2818        .map(|b| {
2819            let mut ps: Vec<(u16, String)> =
2820                b.vt.vars
2821                    .iter()
2822                    .filter(|v| v.is_param && v.name != "this")
2823                    .map(|v| (v.slot, v.name.clone()))
2824                    .collect();
2825            ps.sort_by_key(|(s, _)| *s);
2826            ps.into_iter().map(|(_, n)| n).collect()
2827        })
2828        .unwrap_or_else(|| {
2829            (0..desc.as_ref().map(|d| d.args.len()).unwrap_or(0))
2830                .map(|i| format!("p{}", i + 1))
2831                .collect()
2832        });
2833
2834    // Non-static member-inner ctor: the synthetic outer instance rides
2835    // as args[0] (typed as the direct enclosing class). Every emission
2836    // site already passes it implicitly — qualified `outer.new Inner(..)`,
2837    // `this.new Inner(..)` from inside the outer, `super(..)` after
2838    // skip_outer_arg — so the signature drops the param and the body's
2839    // references become `Outer.this`. Without this, every construction
2840    // site fails javac arity ("无法将类…构造器…应用到给定类型" — the
2841    // guava androidx inner-class families, d8 capture lambdas).
2842    let mut inner_arg0 = 0usize;
2843    if is_init {
2844        let tail = class.name.rsplit('$').next().unwrap_or("");
2845        let digit_simple = !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit());
2846        if let Some(d) = desc.as_ref() {
2847            if let Some(JavaType::Object(outer)) = d.args.first() {
2848                // The direct enclosing class: nesting annotation when
2849                // present, else the `$`-chain parent (find_outer_name —
2850                // d8 lambdas are `Outer$$ExternalSyntheticLambdaN`, where
2851                // a plain rsplit leaves a trailing `$`). It must agree
2852                // with the this$0 type for the rewrite to fire.
2853                let enclosing: Option<String> = class
2854                    .nesting
2855                    .enclosing_class
2856                    .clone()
2857                    .or_else(|| crate::find_outer_name(pool, &class.name));
2858                let direct = enclosing.as_deref() == Some(outer.as_ref());
2859                if direct && !digit_simple && ctx.class_has_this0(&class.name) {
2860                    let param0 = body.as_ref().and_then(|b| {
2861                        b.vt.vars
2862                            .iter()
2863                            .find(|v| v.is_param && v.name != "this")
2864                            .map(|v| v.id)
2865                    });
2866                    let written = match (&body, param0) {
2867                        (Some(b), Some(p0)) => crate::passes::local_is_written(&b.body, p0),
2868                        _ => false,
2869                    };
2870                    if let (Some(p0), false) = (param0, written) {
2871                        if let Some(b) = body.as_mut() {
2872                            // this()-delegations to the SAME class drop the
2873                            // outer arg (all of its ctors share the strip);
2874                            // a super target joins when it is an inner of
2875                            // the same enclosing family.
2876                            let mut eligible: Vec<String> = vec![class.name.clone()];
2877                            if let Some(sup) = &class.super_name {
2878                                let sup_enclosing =
2879                                    crate::find_outer_name(pool, sup);
2880                                if sup_enclosing.as_deref() == Some(outer.as_ref())
2881                                    && ctx.class_has_this0(sup)
2882                                {
2883                                    eligible.push(sup.clone());
2884                                }
2885                            }
2886                            let renamed = crate::apply_class_rename(outer);
2887                            let display = dotted(renamed.as_ref());
2888                            let ty =
2889                                jdc_core::ir::expr::TypeRef::J(JavaType::Object(outer.clone()));
2890                            crate::passes::rewrite_inner_ctor_outer_param(
2891                                &mut b.body,
2892                                p0,
2893                                &display,
2894                                &ty,
2895                                &eligible,
2896                            );
2897                            inner_arg0 = 1;
2898                        }
2899                    }
2900                }
2901            }
2902        }
2903    }
2904
2905    if is_clinit {
2906        // `static { ... }` — the caller strips the method name/params.
2907    } else {
2908        let Some(d) = &desc else { return Ok(false) };
2909        if is_init {
2910            // The ctor name must equal the DECLARED class name of its
2911            // file: flat `$` at depth 0 (own file), own segment when
2912            // inlined in the parent at depth > 0. Case-renamed classes
2913            // use their display name.
2914            let dname = crate::apply_class_rename(&class.name);
2915            let (_, simple) = split_name(&dname);
2916            // emit_method's depth is the METHOD indent = class depth + 1:
2917            // own-file classes (depth 0 header → method depth 1) need the
2918            // flat `$` ctor name; inline nested members use their segment.
2919            let base = if depth <= 1 {
2920                simple.to_string()
2921            } else {
2922                // R8 names can END in `$` (`ThreadMsg$$$`): the last
2923                // `$`-segment is empty — keep the whole simple name.
2924                let seg = simple.rsplit('$').next().unwrap_or(&simple);
2925                if seg.is_empty() {
2926                    simple.to_string()
2927                } else {
2928                    seg.to_string()
2929                }
2930            };
2931            let name = if is_restricted_type_name(&base) {
2932                format!("_{base}")
2933            } else {
2934                base
2935            };
2936            // The ctor name must equal the DECLARED class name — the same
2937            // injective `_u<hex>` sanitizer the header uses (java_ident's
2938            // lossy `-`→`_` fold left `class _u2dDeprecatedOkio` with a
2939            // `private _DeprecatedOkio()` ctor: "方法声明无效; 需要返回
2940            // 类型", which as a PARSE error also masks every semantic
2941            // error in the whole javac run).
2942            sig.push_str(&sanitize_ref(&name));
2943        } else {
2944            sig.push_str(&type_name(pool, &d.ret));
2945            sig.push(' ');
2946            let mname =
2947                jdc_core::rename::field_display(&class.name, &m.name, &m.desc).unwrap_or(&*m.name);
2948            sig.push_str(&java_ident(mname));
2949        }
2950        sig.push('(');
2951        // A promoted enum ctor's dex descriptor carries the compiler-
2952        // synthesized `(String name, int ordinal)` prefix — JLS forbids
2953        // declaring those (they are implicit in the `A(args)` constant
2954        // declarations the promotion emits, and strip_enum_ctor_super
2955        // already removed the `super(name, ordinal, ..)` delegation).
2956        // Skip the pair in the signature, or every constant declaration
2957        // fails javac arity ("无法将枚举…构造器…应用到给定类型"). Only
2958        // when the body never reads the two params — code that genuinely
2959        // uses them keeps the declared form.
2960        // Robust hotpatch guard strip MUST run before the trace-param
2961        // analysis: the guard's isSupport(..) call is the ctor's only
2962        // reader of the (String, int) params for plain robust enums, and
2963        // its self-static read is what JLS 8.9.2 forbids.
2964        if enum_promoted && is_init {
2965            if let Some(b) = body.as_mut() {
2966                strip_robust_enum_ctor_guard(&mut b.body);
2967            }
2968        }
2969        // Fallback enum base ctor: store the (String, int) trace params
2970        // into the synthetic $ddcName/$ddcOrdinal fields that back the
2971        // synthesized name()/ordinal()/valueOf() (synth_enum_members).
2972        // Non-final fields: constant-subclass ctors reach this store
2973        // through their super(..) chain, and a `final` would demand
2974        // definite assignment on every ctor path.
2975        if is_init
2976            && !enum_promoted
2977            && class.is_enum()
2978            && enum_synth_active(class)
2979            && matches!(d.args.first(), Some(JavaType::Object(sx)) if sx.as_ref() == "java/lang/String")
2980            && matches!(d.args.get(1), Some(JavaType::Int))
2981        {
2982            if let Some(b) = body.as_mut() {
2983                // A ctor that DELEGATES first (the -IA synthetic bridge,
2984                // `this(str, p2)`) must stay delegation-first — the
2985                // delegated-to ctor performs the stores.
2986                let starts_with_delegation = match &b.body {
2987                    Stmt::Block(vs) => vs.first().is_some_and(crate::passes::is_bare_ctor_call),
2988                    other => crate::passes::is_bare_ctor_call(other),
2989                };
2990                let ps: Vec<u32> = b
2991                    .vt
2992                    .vars
2993                    .iter()
2994                    .filter(|v| v.is_param && v.name != "this")
2995                    .take(2)
2996                    .map(|v| v.id)
2997                    .collect();
2998                if ps.len() == 2 && !starts_with_delegation {
2999                    let mk_store = |var: u32, fname: &str, fty: JavaType| {
3000                        Stmt::ExprStmt(Expr::Assign {
3001                            target: Box::new(Expr::Field {
3002                                owner: Some(Box::new(Expr::This)),
3003                                cls: std::sync::Arc::from(class.name.as_str()),
3004                                name: std::sync::Arc::from(fname),
3005                                ty: jdc_core::ir::expr::TypeRef::J(fty.clone()),
3006                                is_static: false,
3007                            }),
3008                            op: jdc_core::ir::expr::AssignOp::Plain,
3009                            value: Box::new(Expr::Local {
3010                                var,
3011                                ty: jdc_core::ir::expr::TypeRef::J(fty),
3012                            }),
3013                        })
3014                    };
3015                    let mut inj = vec![
3016                        mk_store(ps[0], ENUM_NAME_FIELD, JavaType::Object("java/lang/String".into())),
3017                        mk_store(ps[1], ENUM_ORD_FIELD, JavaType::Int),
3018                    ];
3019                    if let Stmt::Block(vs) = &mut b.body {
3020                        let mut rest = std::mem::take(vs);
3021                        inj.append(&mut rest);
3022                        *vs = inj;
3023                    } else {
3024                        let prev = std::mem::replace(&mut b.body, Stmt::Block(Vec::new()));
3025                        inj.push(prev);
3026                        b.body = Stmt::Block(inj);
3027                    }
3028                }
3029            }
3030        }
3031        let mut arg0 = 0;
3032        if enum_promoted
3033            && is_init
3034            && matches!(d.args.first(), Some(JavaType::Object(s)) if s.as_ref() == "java/lang/String")
3035            && matches!(d.args.get(1), Some(JavaType::Int))
3036        {
3037            // The Kotlin default-arg bridge ctor reads name/ordinal in
3038            // its `this(str, p2, ..)` delegation only — the LEADING pair
3039            // of a this()-delegation drops together with the params, so
3040            // those reads do not block the strip (the constants' extra
3041            // args match the bridge's user params, not the 2-param user
3042            // ctor).
3043            let synthetic: Vec<u32> = body
3044                .as_ref()
3045                .map(|b| {
3046                    b.vt
3047                        .vars
3048                        .iter()
3049                        .filter(|v| v.is_param && v.name != "this")
3050                        .take(2)
3051                        .map(|v| v.id)
3052                        .collect()
3053                })
3054                .unwrap_or_default();
3055            // The delegation-leading extension only applies to the
3056            // Kotlin default-arg BRIDGE: its descriptor ends with
3057            // kotlin/jvm/internal/DefaultConstructorMarker. A REAL user
3058            // ctor whose first two params are (String, int) and merely
3059            // forwards them must keep its signature (weixin +2.9k when
3060            // ungated).
3061            // Marker-name-free bridge shape: (.., int mask, marker).
3062            // R8 renames DefaultConstructorMarker with the rest of the
3063            // stdlib (weixin: kotlin/jvm/internal/i, an empty abstract
3064            // class), so gate on the package + the int-mask/refs pair
3065            // instead of the literal name — a real user enum ctor with
3066            // (int, kotlin/jvm/internal/*) tail params does not occur.
3067            let n_args = d.args.len();
3068            let is_bridge = n_args >= 2
3069                && matches!(&d.args[n_args - 1], JavaType::Object(ref m)
3070                    if m.starts_with("kotlin/jvm/internal/"))
3071                && matches!(d.args[n_args - 2], JavaType::Int);
3072            // Meituan Robust instrumented ctors PACK the trace params
3073            // into dispatch arrays (`v3[0] = str; v3[1] = new
3074            // Integer(p2); PatchProxy.isSupport(v3, ..)`) — value reads
3075            // that refuse the strip, and a true enum cannot DECLARE
3076            // (String, int) params (JLS 8.9.1), so the whole class fell
3077            // back (illegal `Enum.valueOf` + no ordinal()). Rewrite pure
3078            // VALUE reads to the equivalent `name()`/`ordinal()` calls —
3079            // Enum's fields are set by the implicit super before the
3080            // body runs, so the pack sees identical values. Delegation
3081            // LEAD reads (this(str, p2) of Kotlin/-IA bridges) must keep
3082            // their Local shape for the lead-drop below, so skip any
3083            // ctor that has them; written params are never replaceable.
3084            if synthetic.len() == 2 && !is_bridge {
3085                if let Some(b) = body.as_ref() {
3086                    let (p0, p1) = (synthetic[0], synthetic[1]);
3087                    let mut lead_reads = 0usize;
3088                    {
3089                        let cls_name = class.name.as_str();
3090                        let mut c = b.body.clone();
3091                        crate::passes::walk_stmt_exprs(&mut c, &mut |e| {
3092                            if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3093                                if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3094                                    for a in args.iter().take(2) {
3095                                        if let Expr::Local { var: v, .. } = a {
3096                                            if *v == p0 || *v == p1 {
3097                                                lead_reads += 1;
3098                                            }
3099                                        }
3100                                    }
3101                                }
3102                            }
3103                        });
3104                    }
3105                    let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
3106                    let reads = uses.get(&p0).copied().unwrap_or(0)
3107                        + uses.get(&p1).copied().unwrap_or(0);
3108                    let written = crate::passes::local_is_written(&b.body, p0)
3109                        || crate::passes::local_is_written(&b.body, p1);
3110                    if lead_reads == 0 && reads > 0 && !written {
3111                        let mk = |nm: &str, ret: JavaType| Expr::Method {
3112                            owner: Some(Box::new(Expr::This)),
3113                            cls: std::sync::Arc::from("java/lang/Enum"),
3114                            name: std::sync::Arc::from(nm),
3115                            desc: std::sync::Arc::new(jdc_core::types::MethodDescriptor {
3116                                ret,
3117                                args: Vec::new(),
3118                            }),
3119                            args: Vec::new(),
3120                            is_static: false,
3121                            is_interface: false,
3122                            is_special: false,
3123                            is_super: false,
3124                            is_dynamic: false,
3125                            type_args: Vec::new(),
3126                        };
3127                        let name_call =
3128                            mk("name", JavaType::Object("java/lang/String".into()));
3129                        let ord_call = mk("ordinal", JavaType::Int);
3130                        if let Some(b) = body.as_mut() {
3131                            crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
3132                                crate::passes::deep_rewrite(e, &mut |x| {
3133                                    if let Expr::Local { var, .. } = x {
3134                                        if *var == p0 {
3135                                            *x = name_call.clone();
3136                                        } else if *var == p1 {
3137                                            *x = ord_call.clone();
3138                                        }
3139                                    }
3140                                });
3141                            });
3142                        }
3143                    }
3144                }
3145            }
3146            let mut lead = [0usize, 0usize];
3147            let refs_ok = body.as_ref().is_some_and(|b| {
3148                let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
3149                let mut c = b.body.clone();
3150                let cls_name = class.name.as_str();
3151                crate::passes::walk_stmt_exprs(&mut c, &mut |e| {
3152                    if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3153                        if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3154                            for (k, a) in args.iter().enumerate() {
3155                                if let Expr::Local { var: v, .. } = a {
3156                                    if let Some(pi) = synthetic.iter().position(|p| p == v) {
3157                                        if k == pi && k < 2 {
3158                                            lead[pi] += 1;
3159                                        }
3160                                    }
3161                                }
3162                            }
3163                        }
3164                    }
3165                });
3166                synthetic.iter().enumerate().all(|(pi, id)| {
3167                    let total = uses.get(id).copied().unwrap_or(0);
3168                    if is_bridge {
3169                        total == lead[pi]
3170                    } else {
3171                        total == 0
3172                    }
3173                })
3174            });
3175            if refs_ok {
3176                arg0 = 2;
3177                // Drop the leading (name, ordinal) args of the this()
3178                // delegations.
3179                if let (Some(b), true) = (body.as_mut(), is_bridge) {
3180                    let cls_name = class.name.as_str();
3181                    let syn = synthetic;
3182                    crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
3183                        if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
3184                            if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
3185                                let mut drop_n = 0;
3186                                for a in args.iter().take(2) {
3187                                    if let Expr::Local { var: v, .. } = a {
3188                                        if syn.contains(v) && drop_n == v - syn[0] {
3189                                            drop_n += 1;
3190                                            continue;
3191                                        }
3192                                    }
3193                                    break;
3194                                }
3195                                for _ in 0..drop_n {
3196                                    args.remove(0);
3197                                }
3198                            }
3199                        }
3200                    });
3201                }
3202            }
3203        }
3204        let n = d.args.len();
3205        let varargs = a & ACC_VARARGS != 0 && n > 0;
3206        let arg0 = arg0 + inner_arg0;
3207        for (i, arg) in d.args.iter().enumerate().skip(arg0) {
3208            if i > arg0 {
3209                sig.push_str(", ");
3210            }
3211            let name = param_names
3212                .get(i)
3213                .cloned()
3214                .unwrap_or_else(|| format!("p{}", i));
3215            if varargs && i + 1 == n {
3216                if let JavaType::Array(inner) = arg {
3217                    sig.push_str(&type_name(pool, inner));
3218                    sig.push_str("...");
3219                } else {
3220                    sig.push_str(&type_name(pool, arg));
3221                }
3222            } else {
3223                sig.push_str(&type_name(pool, arg));
3224            }
3225            sig.push(' ');
3226            // Param names come from dex debug info — obfuscated apps
3227            // name them `_` (reserved since Java 9) or after keywords.
3228            sig.push_str(&java_ident(&name));
3229        }
3230        sig.push(')');
3231    }
3232
3233    if is_clinit {
3234        let Some(b) = body else { return Ok(false) };
3235        // Direct render: the printer starts at the method's ABSOLUTE
3236        // indent and appends into the same buffer that carries the
3237        // header — no intermediate body string, no per-line re-indent
3238        // pass (two full copies of every method body saved).
3239        out.push_str(&ind);
3240        out.push_str("static {\n");
3241        let hdr = out.len();
3242        let printer = Printer::new(ctx, &b.vt)
3243            .with_indent(depth + 1)
3244            .with_output(std::mem::take(out));
3245        let t_print = std::time::Instant::now();
3246        let mut rendered = printer.into_string(&b.body);
3247        crate::method::phase_hit(3, t_print);
3248        if rendered[hdr..].trim().is_empty() {
3249            rendered.truncate(hdr);
3250        }
3251        rendered.push_str(&ind);
3252        rendered.push_str("}\n");
3253        *out = rendered;
3254        return Ok(true);
3255    }
3256    if a & (ACC_ABSTRACT | ACC_NATIVE) != 0 || body.is_none() {
3257        out.push_str(&ind);
3258        out.push_str(&sig);
3259        out.push_str(";\n");
3260        return Ok(true);
3261    }
3262    let Some(b) = body else { return Ok(false) };
3263
3264    // Direct render at the absolute indent level (see the clinit path).
3265    let mut printer = Printer::new(ctx, &b.vt).with_indent(depth + 1);
3266    match &b.desc.ret {
3267        JavaType::Boolean => {
3268            printer = printer.with_ret_bool(true);
3269        }
3270        JavaType::Char => {
3271            printer = printer.with_ret_char(true);
3272        }
3273        JavaType::Byte => {
3274            printer = printer.with_ret_narrow(true, false);
3275        }
3276        JavaType::Short => {
3277            printer = printer.with_ret_narrow(false, true);
3278        }
3279        _ => {}
3280    }
3281    out.push_str(&ind);
3282    out.push_str(&sig);
3283    out.push_str(" {\n");
3284    let hdr = out.len();
3285    let printer = printer.with_output(std::mem::take(out));
3286    let t_print = std::time::Instant::now();
3287    let mut rendered = printer.into_string(&b.body);
3288    crate::method::phase_hit(3, t_print);
3289    if rendered[hdr..].trim().is_empty() {
3290        rendered.truncate(hdr);
3291    }
3292    rendered.push_str(&ind);
3293    rendered.push_str("}\n");
3294    *out = rendered;
3295    Ok(true)
3296}
3297
3298// ---------------------------------------------------------------------------
3299// Naming helpers
3300// ---------------------------------------------------------------------------
3301
3302fn indent(depth: usize) -> String {
3303    "    ".repeat(depth)
3304}
3305
3306/// `$`-separated nesting rendered with dots — but ONLY when every
3307/// segment is a clean Java identifier (a genuine member class chain).
3308/// Anonymous (`Outer$1`), Kotlin synthetic (`Version$bigInteger$2`,
3309/// `...$$inlined$collect$1`) and local-class tails are NOT member
3310/// classes Java can name; the whole name stays flat with `$` (ddc emits
3311/// them as their own top-level files).
3312/// Kotlin emits method names like `invokeSuspend$lambda-0` — `-` (and
3313/// any other non-identifier character) is not legal Java. Deterministic
3314/// mapping, applied identically at declaration and call sites.
3315pub(crate) fn java_ident(name: &str) -> std::borrow::Cow<'_, str> {
3316    // env::var_os is an environ lock+scan — java_ident runs per IDENTIFIER
3317    // (millions per APK), where it profiled as __NSGetEnviron.
3318    static DBG_IDENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3319    if *DBG_IDENT.get_or_init(|| std::env::var_os("DDC_DBG_IDENT").is_some()) {
3320        eprintln!("[ident] {name:?}");
3321    }
3322    let clean = name
3323        .chars()
3324        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
3325    // Kotlin names fields/methods `default` (Companion.default) — no Java
3326    // program can declare or reference a keyword; the identical mapping
3327    // lives in jdc-core's call-site sanitizer.
3328    let keyword = matches!(
3329        name,
3330        "abstract"
3331            | "assert"
3332            | "boolean"
3333            | "break"
3334            | "byte"
3335            | "case"
3336            | "catch"
3337            | "char"
3338            | "class"
3339            | "const"
3340            | "continue"
3341            | "default"
3342            | "do"
3343            | "double"
3344            | "else"
3345            | "enum"
3346            | "extends"
3347            | "final"
3348            | "finally"
3349            | "float"
3350            | "for"
3351            | "goto"
3352            | "if"
3353            | "implements"
3354            | "import"
3355            | "instanceof"
3356            | "int"
3357            | "interface"
3358            | "long"
3359            | "native"
3360            | "new"
3361            | "package"
3362            | "private"
3363            | "protected"
3364            | "public"
3365            | "return"
3366            | "short"
3367            | "static"
3368            | "strictfp"
3369            | "super"
3370            | "switch"
3371            | "synchronized"
3372            | "this"
3373            | "throw"
3374            | "throws"
3375            | "transient"
3376            | "try"
3377            | "void"
3378            | "volatile"
3379            | "while"
3380            | "true"
3381            | "false"
3382            | "null"
3383            // `_` is a reserved identifier since Java 9 (Alipay's
3384            // instant-run fields are named `_`) — the `_<name>` mapping
3385            // turns it into `__`, matching jdc-core's call sites.
3386            | "_"
3387    );
3388    // A simple name may not START with a digit either (WhatsApp nests
3389    // `X/0Xx`): the declaration site and every reference (jdc-core's
3390    // sanitize_source_name) prefix the same underscore.
3391    let digit_start = name.chars().next().is_some_and(|c| c.is_ascii_digit());
3392    if clean && !keyword && !digit_start {
3393        std::borrow::Cow::Borrowed(name)
3394    } else if keyword || digit_start {
3395        std::borrow::Cow::Owned(format!("_{name}"))
3396    } else {
3397        // Non-ASCII single chars (Alipay names a field `支`) map to a
3398        // lone `_` — itself reserved since Java 9. Escape it.
3399        let mapped: String = name
3400            .chars()
3401            .map(|c| {
3402                if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
3403                    c
3404                } else {
3405                    '_'
3406                }
3407            })
3408            .collect();
3409        if mapped == "_" {
3410            std::borrow::Cow::Owned("__".to_string())
3411        } else {
3412            std::borrow::Cow::Owned(mapped)
3413        }
3414    }
3415}
3416
3417fn split_name(internal: &str) -> (String, String) {
3418    match internal.rfind('/') {
3419        Some(i) => (internal[..i].to_string(), internal[i + 1..].to_string()),
3420        None => (String::new(), internal.to_string()),
3421    }
3422}
3423
3424/// Dotted source form of an internal name.
3425pub fn dotted(internal: &str) -> String {
3426    let mut out = internal.replace('/', ".");
3427    // `$` → `.` only when the following segment can start a Java
3428    // identifier (anonymous/synthetic tails stay `$`).
3429    let mut i = 0;
3430    while let Some(p) = out[i..].find('$') {
3431        let at = i + p;
3432        // A LEADING `$` (ProGuard keeps `$Gson$Types`) is part of the
3433        // source name — dotting it produced a leading `.Gson.Types`.
3434        let head_ok = at > 0
3435            && out[..at]
3436                .chars()
3437                .next_back()
3438                .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
3439        let tail_ok = out[at + 1..]
3440            .chars()
3441            .next()
3442            .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
3443        if tail_ok && head_ok {
3444            out.replace_range(at..at + 1, ".");
3445            i = at + 1;
3446        } else {
3447            i = at + 1;
3448        }
3449    }
3450    // Every `.`-segment must start an identifier: WhatsApp's `X/0Hl`
3451    // reached `extends` with the digit start unmapped.
3452    sanitize_fq(&out)
3453}
3454
3455fn join_dotted(pool: &DexPool, names: &[String]) -> String {
3456    names
3457        .iter()
3458        .map(|n| print_class_name(pool, n))
3459        .collect::<Vec<_>>()
3460        .join(", ")
3461}
3462
3463/// A printable type name (arrays render with `[]` suffixes). Nested class
3464/// names dot their `$` when the outer chain is known to the pool.
3465/// Split a concatenated method-descriptor ARGUMENT string
3466/// (`"Ljava/lang/String;I[Ljava/lang/Object;"`) into per-parameter
3467/// `JavaType`s. Used by the framework-ancestor ctor bridge, where the
3468/// referenced sig comes from the method-id table (no pool ancestor
3469/// method to read a parsed descriptor from).
3470pub(crate) fn split_arg_descs(sig: &str) -> Vec<JavaType> {
3471    let mut out = Vec::new();
3472    let b = sig.as_bytes();
3473    let mut i = 0usize;
3474    while i < b.len() {
3475        let start = i;
3476        while i < b.len() && b[i] == b'[' {
3477            i += 1;
3478        }
3479        if i < b.len() {
3480            if b[i] == b'L' {
3481                while i < b.len() && b[i] != b';' {
3482                    i += 1;
3483                }
3484                if i < b.len() {
3485                    i += 1; // consume ';'
3486                }
3487            } else {
3488                i += 1; // primitive single char
3489            }
3490        }
3491        if i > start {
3492            out.push(desc_type(&sig[start..i]));
3493        } else {
3494            break; // malformed: avoid an infinite loop
3495        }
3496    }
3497    out
3498}
3499
3500pub fn type_name(pool: &DexPool, t: &JavaType) -> String {
3501    match t {
3502        JavaType::Void => "void".into(),
3503        JavaType::Object(n) => print_class_name(pool, n),
3504        JavaType::Array(inner) => format!("{}[]", type_name(pool, inner)),
3505        other => other.to_java(false),
3506    }
3507}
3508
3509/// `com/foo/Outer$Inner` → `com.foo.Outer.Inner` — each `$` dots only when
3510/// its left side names a known class (literal `$` top-level names survive).
3511// ---------------------------------------------------------------------------
3512// The per-class import map (JLS 6.4.2 obscuring repair).
3513//
3514// A referenced FQN whose FIRST segment equals an in-scope class simple
3515// name binds the qualifier to the CLASS, not the package — `class j3
3516// implements j3.g` is cyclic inheritance and collapses javac's
3517// attribution for the whole package (the Object-cascade root, weixin
3518// 84 files + contagion). The repair: emit `import j3.g;` and render
3519// the SIMPLE name in every type position. The set is populated per
3520// class from a metadata pre-scan (supertypes, field types, method
3521// signature descriptors) and read by print_class_name and the
3522// DexCtx::obscured_simple implementation (which jdc-core's shorten /
3523// java_type_name consult).
3524struct ObscureState {
3525    /// The ROOT class being rendered (its simple name is the obscuring
3526    /// in-scope name).
3527    class: String,
3528    /// Field names in scope through the root class's inheritance chain
3529    /// (own + pool ancestors + framework ancestors' statics/instances +
3530    /// interface constants). An in-scope field obscures a package's
3531    /// first segment in expression position (JLS 6.4.2) — WhatsApp's
3532    /// View subclasses inherit the static Property X/Y/Z (API 31+) and
3533    /// its whole obfuscated codebase lives in package X (×30,171); the
3534    /// general set replaces the hardcoded X/Y/Z list. Obscured refs
3535    /// route through the import layer like own-simple obscuring.
3536    shadow: jdc_core::FxHashSet<String>,
3537    /// Metadata pre-scan results: internal → simple render.
3538    map: jdc_core::FxHashMap<String, String>,
3539    /// Expression-level obscured refs DISCOVERED during the body render
3540    /// (their imports emit at assembly time).
3541    recorded: jdc_core::FxHashSet<String>,
3542    /// Simple names of the CURRENT PACKAGE's own classes: importing a
3543    /// name that collides would SHADOW every same-package use of it in
3544    /// this file (JLS 7.5.1) — those refs stay qualified (obscured,
3545    /// erroring) rather than corrupt.
3546    blocked: jdc_core::FxHashSet<String>,
3547    /// RENAMED display tails of same-package classes (zero-copy view
3548    /// into the process-wide cache): same blocking role as `blocked`
3549    /// for refs that render under the rename registry.
3550    blocked_renamed: Option<&'static jdc_core::FxHashSet<String>>,
3551}
3552
3553thread_local! {
3554    static OBSCURE: std::cell::RefCell<Option<ObscureState>> =
3555        const { std::cell::RefCell::new(None) };
3556}
3557
3558/// Install the per-class render state (call at class render entry).
3559pub(crate) fn set_obscured_state(
3560    class: String,
3561    map: jdc_core::FxHashMap<String, String>,
3562    blocked: jdc_core::FxHashSet<String>,
3563    blocked_renamed: Option<&'static jdc_core::FxHashSet<String>>,
3564    shadow: jdc_core::FxHashSet<String>,
3565) {
3566    OBSCURE.with(|m| {
3567        *m.borrow_mut() = Some(ObscureState {
3568            class,
3569            shadow,
3570            map,
3571            recorded: jdc_core::FxHashSet::default(),
3572            blocked,
3573            blocked_renamed,
3574        });
3575    });
3576}
3577
3578/// Simple names this class's file renders through the import layer
3579/// (metadata map values + dynamically recorded nested tails). A method
3580/// local with one of these names captures the imported simple name in
3581/// expression position (locals beat single-type imports — `h.e` binds
3582/// a local int `h`, 无法取消引用int); deshadow_locals renames them.
3583/// Empty when no render state is installed (progressive paths).
3584pub(crate) fn obscured_simples_snapshot() -> jdc_core::FxHashSet<String> {
3585    OBSCURE.with(|m| {
3586        let mut out = jdc_core::FxHashSet::default();
3587        let Ok(st) = m.try_borrow() else { return out };
3588        let Some(st) = st.as_ref() else { return out };
3589        for simple in st.map.values() {
3590            out.insert(simple.clone());
3591        }
3592        for internal in &st.recorded {
3593            let renamed = crate::apply_class_rename(internal);
3594            if let Some(tail) = renamed.rsplit(['/', '$']).next() {
3595                if !tail.is_empty() {
3596                    out.insert(tail.to_string());
3597                }
3598            }
3599        }
3600        out
3601    })
3602}
3603
3604/// Take the recorded expression-level refs and clear the state.
3605pub(crate) fn take_recorded_and_clear() -> jdc_core::FxHashSet<String> {
3606    OBSCURE.with(|m| {
3607        m.borrow_mut()
3608            .take()
3609            .map(|st| st.recorded)
3610            .unwrap_or_default()
3611    })
3612}
3613
3614// ---- cross-package access widening ----------------------------------
3615// ART tolerates cross-package access to non-public classes/members that
3616// R8/d8 preserved; Java source cannot express it ("是在不可访问的类或
3617// 接口中定义的", "k在kotlinx.coroutines中不是公共的", "unknownFields 在
3618// l6 中是 protected"). The census scan records every cross-package
3619// reference; the installer keeps the non-public targets and the
3620// declaration renders widen them to public.
3621
3622struct WidenState {
3623    classes: jdc_core::FxHashSet<String>,
3624    methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>>,
3625    fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>,
3626    /// (owner, field name) finals written outside the declaring class's
3627    /// ctor/clinit — rendered WITHOUT `final` (see refscan census).
3628    unfinal: jdc_core::FxHashSet<(String, String)>,
3629}
3630
3631static WIDEN: std::sync::OnceLock<WidenState> = std::sync::OnceLock::new();
3632
3633pub(crate) fn install_access_widening(pool: &crate::DexPool) {
3634    let t0 = std::time::Instant::now();
3635    let raw = crate::refscan::access_widening_scan(pool.dexes());
3636    let mut classes: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3637    for c in &raw.classes {
3638        if let Some(pc) = pool.get_if_materialized(c) {
3639            if pc.access & crate::access::ACC_PUBLIC == 0 {
3640                classes.insert(c.clone());
3641            }
3642        }
3643    }
3644    let mut methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>> =
3645        jdc_core::FxHashMap::default();
3646    for (owner, set) in &raw.methods {
3647        for (name, desc) in set {
3648            if name == "<clinit>" {
3649                continue; // never referenced
3650            }
3651            // A dex ref of an INHERITED member may name any subclass as
3652            // owner (ART resolves up the chain), while the declaration —
3653            // and its access modifier — lives on an ancestor. Register
3654            // the widening on the DECLARING class or the rendered decl
3655            // stays protected ("在 X 中是 protected 访问控制").
3656            let mut cur: Option<&str> = Some(owner.as_str());
3657            let mut hops = 0u32;
3658            while let Some(cname) = cur {
3659                hops += 1;
3660                if hops > 64 {
3661                    break;
3662                }
3663                let Some(pc) = pool.get_if_materialized(cname) else {
3664                    break;
3665                };
3666                // Enum ctors are source-level private-only: widening is
3667                // a javac "modifier not allowed here".
3668                if name == "<init>" && pc.access & crate::access::ACC_ENUM != 0 {
3669                    break;
3670                }
3671                match pc.all_methods()
3672                    .find(|m| &*m.name == name.as_str() && &*m.desc == desc.as_str())
3673                {
3674                    Some(m) if m.access & crate::access::ACC_PUBLIC == 0 => {
3675                        methods
3676                            .entry(cname.to_string())
3677                            .or_default()
3678                            .insert((name.clone(), desc.clone()));
3679                        break;
3680                    }
3681                    Some(_) => break, // already public
3682                    None => cur = pc.super_name.as_deref(),
3683                }
3684            }
3685        }
3686    }
3687    // Override-closure DOWN the hierarchy: a widened (public) super
3688    // method whose subclass override stays protected/package is
3689    // "无法覆盖...更低的访问权限" (lark +823). Every transitive
3690    // subclass declaring the same (name, desc) non-public widens too.
3691    {
3692        let mut subs: jdc_core::FxHashMap<String, Vec<String>> =
3693            jdc_core::FxHashMap::default();
3694        for name in &pool.order {
3695            let Some(pc) = pool.get_if_materialized(name) else {
3696                continue;
3697            };
3698            if let Some(sup) = &pc.super_name {
3699                subs.entry(sup.clone()).or_default().push(name.clone());
3700            }
3701            for i in &pc.interfaces {
3702                subs.entry(i.clone()).or_default().push(name.clone());
3703            }
3704        }
3705        let seeds: Vec<(String, String, String)> = methods
3706            .iter()
3707            .flat_map(|(o, set)| {
3708                set.iter().map(|(n, d)| (o.clone(), n.clone(), d.clone()))
3709            })
3710            .collect();
3711        for (owner, mname, mdesc) in seeds {
3712            let mut queue: Vec<String> = match subs.get(&owner) {
3713                Some(v) => v.clone(),
3714                None => continue,
3715            };
3716            let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3717            while let Some(c) = queue.pop() {
3718                if !seen.insert(c.clone()) {
3719                    continue;
3720                }
3721                let Some(pc) = pool.get_if_materialized(&c) else {
3722                    continue;
3723                };
3724                let declares_nonpub = pc.all_methods().any(|m| {
3725                    &*m.name == mname.as_str()
3726                        && &*m.desc == mdesc.as_str()
3727                        && m.access & crate::access::ACC_PUBLIC == 0
3728                });
3729                if declares_nonpub {
3730                    methods
3731                        .entry(c.clone())
3732                        .or_default()
3733                        .insert((mname.clone(), mdesc.clone()));
3734                }
3735                if let Some(v) = subs.get(&c) {
3736                    queue.extend(v.iter().cloned());
3737                }
3738            }
3739        }
3740        // Subclass `super(..)` targets. A class C extends S; C's ctor
3741        // renders `super(..)` against S's ctor. When R8 INLINES S's
3742        // trivial ctor, C's dex ctor calls `Object.<init>` directly (not
3743        // `S.<init>`), so the bytecode census above never sees the S.<init>
3744        // reference and a PRIVATE S ctor stays private — the rendered
3745        // `super()` then fails ("R() 在 R 中是 private 访问控制", lark ×122:
3746        // pl.droidsonroids.gif.R extends com.ss.android.lark.R, whose ctor
3747        // R8 inlined to Object.<init>). Widen every private ctor of a
3748        // class that HAS subclasses so the implicit super() links. Enums
3749        // are excluded (their ctors are source-level private-only).
3750        for sup in subs.keys() {
3751            let Some(pc) = pool.get_if_materialized(sup) else {
3752                continue;
3753            };
3754            if pc.access & crate::access::ACC_ENUM != 0 {
3755                continue;
3756            }
3757            for m in pc.all_methods() {
3758                if &*m.name == "<init>" && m.access & crate::access::ACC_PRIVATE != 0 {
3759                    methods
3760                        .entry(sup.clone())
3761                        .or_default()
3762                        .insert((m.name.to_string(), m.desc.to_string()));
3763                }
3764            }
3765        }
3766    }
3767    let mut fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
3768        jdc_core::FxHashMap::default();
3769    for (owner, set) in &raw.fields {
3770        for name in set {
3771            // Same inherited-owner walk as methods: the protected field
3772            // lives on the ancestor that DECLARES it (weibo
3773            // mModuleContext/mData ×470 — refs named the module
3774            // subclass, the own-fields-only filter never widened the
3775            // declaring base).
3776            let mut cur: Option<&str> = Some(owner.as_str());
3777            let mut hops = 0u32;
3778            while let Some(cname) = cur {
3779                hops += 1;
3780                if hops > 64 {
3781                    break;
3782                }
3783                let Some(pc) = pool.get_if_materialized(cname) else {
3784                    break;
3785                };
3786                match pc.static_fields
3787                    .iter()
3788                    .chain(pc.instance_fields.iter())
3789                    .find(|f| f.name == name.as_str())
3790                {
3791                    Some(f) if f.access & crate::access::ACC_PUBLIC == 0 => {
3792                        fields
3793                            .entry(cname.to_string())
3794                            .or_default()
3795                            .insert(name.clone());
3796                        break;
3797                    }
3798                    Some(_) => break, // already public
3799                    None => cur = pc.super_name.as_deref(),
3800                }
3801            }
3802        }
3803    }
3804    if std::env::var("DDC_STATS").is_ok() {
3805        eprintln!(
3806            "[renames] access widening: classes={} method-owners={} field-owners={} scan={:?}",
3807            classes.len(),
3808            methods.len(),
3809            fields.len(),
3810            t0.elapsed()
3811        );
3812    }
3813
3814    let unfinal: jdc_core::FxHashSet<(String, String)> =
3815        raw.unfinal_fields.into_iter().collect();
3816    let _ = WIDEN.set(WidenState { classes, methods, fields, unfinal });
3817}
3818
3819#[inline]
3820pub(crate) fn widen_class(internal: &str) -> bool {
3821    WIDEN.get().is_some_and(|w| w.classes.contains(internal))
3822}
3823
3824#[inline]
3825pub(crate) fn widen_method(owner: &str, name: &str, desc: &str) -> bool {
3826    WIDEN.get().is_some_and(|w| {
3827        w.methods
3828            .get(owner)
3829            .is_some_and(|s| s.contains(&(name.to_string(), desc.to_string())))
3830    })
3831}
3832
3833#[inline]
3834pub(crate) fn widen_field(owner: &str, name: &str) -> bool {
3835    WIDEN.get().is_some_and(|w| {
3836        w.fields.get(owner).is_some_and(|s| s.contains(name))
3837    })
3838}
3839
3840/// True when the census saw this final field written outside the
3841/// declaring class's `<init>`/`<clinit>` — the `final` modifier must
3842/// not render ("无法为 final 变量 v 分配值" at every foreign write).
3843#[inline]
3844pub(crate) fn unfinal_field(owner: &str, name: &str) -> bool {
3845    WIDEN.get().is_some_and(|w| {
3846        w.unfinal.contains(&(owner.to_string(), name.to_string()))
3847    })
3848}
3849
3850/// Render-time lookup: the metadata map first; otherwise an
3851/// expression-level ref whose first segment equals the root class's
3852/// simple name — record it (its import emits at assembly) and render
3853/// the simple name NOW.
3854pub(crate) fn obscured_render_pub(internal: &str) -> Option<String> {
3855    OBSCURE.with(|m| {
3856        let mut st = m.borrow_mut();
3857        let st = st.as_mut()?;
3858        if let Some(simple) = st.map.get(internal) {
3859            return Some(simple.clone());
3860        }
3861        let first = internal.split('/').next().unwrap_or("");
3862        // The DISPLAY own simple: when the root class itself was renamed
3863        // (case-collision `widget/d` → `widget/d2`), no in-scope type
3864        // bears the raw name any more — the package's first segment is
3865        // NOT obscured and the qualified render is the correct one
3866        // (reqable d2.java: importing d.h let the class's own boolean
3867        // field h shadow it — 无法在调用超类型构造器之前引用h ×23).
3868        let own_renamed = crate::apply_class_rename(&st.class);
3869        let own = own_renamed.rsplit(['/', '$']).next().unwrap_or("");
3870        let seg_obscured = first == own || st.shadow.contains(first);
3871        if seg_obscured && internal.split('/').count() >= 2 {
3872            // A NESTED internal's in-scope simple name is its `$` tail
3873            // (`x/a$b` imports/renders as `b`) — the slash-tail left the
3874            // `$` in the render (`t2$a` flat against a nested emission).
3875            // The tail must come from the RENAMED display: case-collision
3876            // renames (`X/00i` → `X/_00i_2`) live in the class registry —
3877            // the raw tail rendered `_00i` against the declaration's
3878            // `_00i_2` (WhatsApp cannot-find ×27k after the View-shadow
3879            // escape opened this path at scale).
3880            let renamed = crate::apply_class_rename(internal);
3881            let simple = renamed
3882                .rsplit(['/', '$'])
3883                .next()
3884                .unwrap_or("")
3885                .to_string();
3886            if !simple.is_empty()
3887                && !st.blocked.contains(&simple)
3888                && !st.blocked_renamed.is_some_and(|e| e.contains(&simple))
3889            {
3890                st.recorded.insert(internal.to_string());
3891                return Some(simple);
3892            }
3893        }
3894        None
3895    })
3896}
3897
3898/// Field names in scope for `class` through inheritance: own fields,
3899/// pool ancestors (supers + interfaces, transitively), and framework
3900/// ancestors via the embedded API database (chain sets memoized
3901/// process-wide — every View subclass shares the View/ViewGroup/Object
3902/// field union, so the per-file cost is its own pool chain only).
3903fn inherited_field_shadows(
3904    pool: &DexPool,
3905    class: &PoolClass,
3906) -> jdc_core::FxHashSet<String> {
3907    fn fw_chain_fields(boundary: &str) -> std::sync::Arc<jdc_core::FxHashSet<String>> {
3908        use std::sync::{Mutex, OnceLock};
3909        static CACHE: OnceLock<Mutex<jdc_core::FxHashMap<String, std::sync::Arc<jdc_core::FxHashSet<String>>>>> =
3910            OnceLock::new();
3911        let cache = CACHE.get_or_init(|| Mutex::new(jdc_core::FxHashMap::default()));
3912        if let Some(hit) = cache.lock().unwrap_or_else(|e| e.into_inner()).get(boundary) {
3913            return hit.clone();
3914        }
3915        let mut set: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3916        let mut stack: Vec<&str> = vec![boundary];
3917        let mut seen: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
3918        let mut hops = 0usize;
3919        while let Some(c) = stack.pop() {
3920            hops += 1;
3921            if hops > 256 || !seen.insert(c) {
3922                continue;
3923            }
3924            crate::fwdb::for_each_field(c, |n, f| {
3925                // STATIC only: instance fields shadow qualified refs in
3926                // instance contexts but NOT in static ones, and the
3927                // render decision is context-free — the static subset
3928                // is the sound approximation (View.X/Y/Z are static;
3929                // including instance fields cost +33 battery).
3930                if f & crate::fwdb::MF_STATIC != 0 {
3931                    set.insert(n.to_string());
3932                }
3933            });
3934            if let Some(sup) = crate::fwdb::super_of(c) {
3935                stack.push(sup);
3936            }
3937            crate::fwdb::for_each_interface(c, |i| stack.push(i));
3938        }
3939        let arc = std::sync::Arc::new(set);
3940        cache
3941            .lock()
3942            .unwrap_or_else(|e| e.into_inner())
3943            .insert(boundary.to_string(), arc.clone());
3944        arc
3945    }
3946
3947    let mut out: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3948    let mut stack: Vec<String> = vec![class.name.to_string()];
3949    let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3950    let mut hops = 0usize;
3951    while let Some(cn) = stack.pop() {
3952        hops += 1;
3953        if hops > 512 || !seen.insert(cn.clone()) {
3954            continue;
3955        }
3956        let Some(pc) = pool.get(&cn) else {
3957            // Framework boundary: memoized chain set.
3958            out.extend(fw_chain_fields(&cn).iter().cloned());
3959            continue;
3960        };
3961        for f in pc.static_fields.iter() {
3962            out.insert(f.name.to_string());
3963        }
3964        if let Some(sup) = &pc.super_name {
3965            if sup != "java/lang/Object" {
3966                stack.push(sup.clone());
3967            } else {
3968                // Object itself lives in the DB (its fields shadow too —
3969                // none exist, but interface constants reached via
3970                // interfaces below still do).
3971            }
3972        } else {
3973            // Interface (no super): java/lang/Object contributes nothing.
3974        }
3975        stack.extend(pc.interfaces.iter().cloned());
3976    }
3977    out
3978}
3979
3980/// The pre-scan: internal names referenced by the class's metadata
3981/// (supertypes, field types, method descriptors) whose first segment
3982/// equals the class's own simple name and which exist in the pool —
3983/// these get imports and simple-name renders.
3984fn compute_obscured_renders(
3985    pool: &DexPool,
3986    class: &PoolClass,
3987    shadow: &jdc_core::FxHashSet<String>,
3988) -> jdc_core::FxHashMap<String, String> {
3989    let own_renamed = crate::apply_class_rename(&class.name);
3990    let own_simple = own_renamed.rsplit(['/', '$']).next().unwrap_or("");
3991    if own_simple.is_empty() {
3992        return jdc_core::FxHashMap::default();
3993    }
3994    let mut refs: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
3995    for sup in class.interfaces.iter() {
3996        refs.insert(sup.clone());
3997    }
3998    if let Some(sup) = &class.super_name {
3999        refs.insert(sup.clone());
4000    }
4001    for f in class.static_fields.iter().chain(class.instance_fields.iter()) {
4002        if let JavaType::Object(n) = crate::desc_type(&f.desc) {
4003            refs.insert(n.to_string());
4004        }
4005    }
4006    for m in class.all_methods() {
4007        if let Some(d) = m.parsed_desc() {
4008            for a in d.args.iter() {
4009                if let JavaType::Object(n) = a {
4010                    refs.insert(n.to_string());
4011                }
4012            }
4013            if let JavaType::Object(n) = &d.ret {
4014                refs.insert(n.to_string());
4015            }
4016        }
4017    }
4018    let mut out: jdc_core::FxHashMap<String, String> = jdc_core::FxHashMap::default();
4019    for r in refs {
4020        // First segment == the class's own simple name: the qualified
4021        // render would be obscured. The name must exist in the pool for
4022        // the import to resolve. The KEY stays internal (renders look
4023        // refs up by internal name), but the import line and the
4024        // simple name use the RENAMED display — a collision-renamed
4025        // class renders under its display name and an import of the
4026        // internal name does not resolve.
4027        let first = r.split('/').next().unwrap_or("");
4028        let seg_obscured = first == own_simple || shadow.contains(first);
4029        if seg_obscured && r.split('/').count() >= 2 && pool.get(&r).is_some() {
4030            // Own-family refs (the class itself, its nested members)
4031            // render through member scope — an import would be redundant
4032            // (self-import) or shadow a same-package sibling named like
4033            // the tail (`import t.t2.a` hijacks every bare `a` that
4034            // meant sibling class t.a — lark t/t2).
4035            if r == class.name || r.starts_with(&format!("{}$", class.name)) {
4036                continue;
4037            }
4038            let display = crate::apply_class_rename(&r);
4039            let simple = display
4040                .rsplit(['/', '$'])
4041                .next()
4042                .unwrap_or("")
4043                .to_string();
4044            if !simple.is_empty() {
4045                out.insert(r.clone(), simple);
4046            }
4047        }
4048    }
4049    out
4050}
4051
4052pub fn print_class_name(pool: &DexPool, internal: &str) -> String {
4053    if let Some(simple) = obscured_render_pub(internal) {
4054        return sanitize_ref(&simple);
4055    }
4056    let cow = crate::apply_class_rename(internal);
4057    let internal: &str = &cow;
4058    // Flat EMISSION UNITS: a digit-tail member (anonymous / d8-lambda
4059    // shape, `Outer$lruCache$1`) is emitted as its own top-level file
4060    // whose simple name keeps every `$` — the `$` boundaries inside
4061    // that unit are part of the NAME, not nesting. References must use
4062    // the flat unit name; the generic loop below dotted the first
4063    // boundary (`LruCacheKt.lruCache$1`) against the declaration
4064    // `class LruCacheKt$lruCache$1` — every use of the type failed and
4065    // javac's attribution for the whole file collapsed (3627 pure-
4066    // cascade files on weibo).
4067    if internal.contains('$') && pool.get(internal).is_some() {
4068        if let Some(root) = emission_root(pool, internal) {
4069            if root.contains('$') {
4070                let below = &internal[root.len()..];
4071                if below.is_empty() {
4072                    // The unit itself: the flat name IS the reference.
4073                    return sanitize_ref(&internal.replace('/', "."));
4074                }
4075                let segs: Vec<&str> = below[1..].split('$').collect();
4076                if segs.iter().any(|s| s.is_empty()) {
4077                    // R8 tails like `ThreadMsg$$$` cannot be dotted at
4078                    // all — keep the whole name flat.
4079                    return sanitize_ref(&internal.replace('/', "."));
4080                }
4081                let mut out = root.replace('/', ".");
4082                for s in segs {
4083                    out.push('.');
4084                    out.push_str(s);
4085                }
4086                return sanitize_ref(&out);
4087            }
4088        }
4089    }
4090    let mut out = String::new();
4091    // `known` must test the ACCUMULATED internal prefix, not the bare
4092    // inter-`$` segment: the per-segment shape checked `pool.get("a")`
4093    // for the second level of `s5/o$a$b`, missed, and rendered the
4094    // undeclarable reference `s5.o.a$b` (找不到符号) for every nested-
4095    // nested type.
4096    let mut off = 0usize;
4097    loop {
4098        let rest = &internal[off..];
4099        match rest.find('$') {
4100            Some(i) => {
4101                let seg = &rest[..i];
4102                let prefix = &internal[..off + i];
4103                let known = pool.get(prefix).is_some()
4104                    || jdc_core::rename::is_renamed_display(prefix)
4105                    // The FULL name is not a pool class: this `$` cannot
4106                    // be a literal name (pool literal classes — an app's
4107                    // own `View$OnUnhandledKeyEventListener` — keep their
4108                    // `$` here AND at their declaration), so it can only
4109                    // be an external framework nesting boundary
4110                    // (`View$OnClickListener` → `.OnClickListener`).
4111                    || !pool.get(internal).is_some();
4112                // The `$` may only become a nesting dot when the tail
4113                // segment STARTS a Java identifier: R8's desugared-
4114                // library names carry `$` inside PACKAGE paths
4115                // (`j$/util/...` dotted into `j..util`) and suffixes
4116                // like `Collection$-EL` or anonymous `RequestId$1`
4117                // cannot be dotted under any reading.
4118                //
4119                // Sanitizer-escaped tails DO dot for POOL classes: the
4120                // injective `_u<hex>` escape turns any non-ASCII (or
4121                // ASCII-punctuation) leading char into `_`-initial —
4122                // a valid identifier start — and the declaration side
4123                // renders the member that way (weibo's unicode-named
4124                // nested `a$Ꮺ`: decl `class a_u2dIA` inside a, ctor
4125                // params dotted `a.a_u2dIA`, but casts stayed flat
4126                // `a$a_u2dIA` — 找不到符号 类 ×~500). Digit and `$`
4127                // tails stay flat (emission-unit files keep the `$`;
4128                // framework binary names are unreachable either way).
4129                let tail_next = rest[i + 1..].chars().next();
4130                let tail_ok = match tail_next {
4131                    Some(c) if c.is_ascii_alphabetic() || c == '_' => true,
4132                    Some(c) if !c.is_ascii_digit() && c != '$' => {
4133                        pool.get(internal).is_some()
4134                    }
4135                    _ => false,
4136                };
4137                out.push_str(&seg.replace('/', "."));
4138                out.push_str(if known && tail_ok { "." } else { "$" });
4139                off += i + 1;
4140            }
4141            None => {
4142                out.push_str(&rest.replace('/', "."));
4143                return sanitize_ref(&out);
4144            }
4145        }
4146    }
4147}
4148
4149/// The top-level EMISSION UNIT ancestor of a pool class: walking the
4150/// `$`-chain upward (outer_of), the first ancestor that is itself
4151/// emitted as its own compilation unit — a digit-tail rest marks a
4152/// flat boundary (see top_level_classes). Clean members render inline
4153/// in their outer, so only the unit's own name may carry `$`.
4154fn emission_root(pool: &DexPool, internal: &str) -> Option<String> {
4155    let mut cur = internal.to_string();
4156    loop {
4157        let outer = pool.outer_of(&cur)?;
4158        let rest = cur
4159            .strip_prefix(outer)
4160            .and_then(|t| t.strip_prefix('$'))
4161            .unwrap_or("");
4162        if !crate::clean_member_tail(rest) {
4163            return Some(cur);
4164        }
4165        cur = outer.to_string();
4166    }
4167}
4168
4169/// Class-file names may contain characters Java source identifiers
4170/// cannot (`Collection$-EL`); the deterministic mapping matches the
4171/// declaration sites (java_ident).
4172/// Every `.`-segment of a fully-qualified name must start a Java
4173/// identifier: obfuscators emit `package do;` and `..badge.new..` paths.
4174///
4175/// PUBLIC on purpose: this is the single source of truth for the
4176/// declaration↔file-name mapping. The CLI writer used to keep its own
4177/// copy (`sanitize_file_seg`) which had drifted — it lacked the lone-`_`
4178/// escape below — so `l.֡` declared `class __` inside a file named
4179/// `_.java`. Callers reach it through `sanitize_seg`/`sanitize_internal`.
4180///
4181/// INJECTIVE on the characters, which is the property the writer needs.
4182/// The old mapping folded every non-identifier character to `_`, so
4183/// `l.᩻ܶ`, `l.᩻ۡ` and `l.֫᩷` all became `l.__`: on bin.mt.plus 22,636
4184/// distinct classes in package `l` collapsed onto three file names, the
4185/// writer's EEXIST branch overwrote them, and 22,633 sources were
4186/// destroyed with exit code 0. `_u<hex>` keeps them distinct, is
4187/// self-describing (it encodes the original code point) and costs the
4188/// decompiler nothing — the alternative, minting 22,636 registry
4189/// renames, makes every type reference allocate and took this sample
4190/// from 5.2 s to 26.8 s.
4191///
4192/// The only residual collisions are *lookalikes*: a literal class named
4193/// `_u1a7b` beside the class `᩻` (same code point), or the pre-existing
4194/// keyword form `_do` beside `do`. Those are detected and repaired
4195/// deterministically by `lossy_sanitize_renames`, which is why that pass
4196/// still exists.
4197pub fn sanitize_fq(dotted: &str) -> String {
4198    dotted
4199        .split('.')
4200        .map(sanitize_fq_seg)
4201        .collect::<Vec<_>>()
4202        .join(".")
4203}
4204
4205/// One `.`-segment: escape, then repair identifier-position problems.
4206fn sanitize_fq_seg(seg: &str) -> String {
4207    // Ident-safe ASCII passes through unchanged (the overwhelmingly
4208    // common case, and the only one on the borrow-fast path); every
4209    // other character — non-ASCII, and ASCII punctuation such as the
4210    // `-` in `Collection$-EL` — becomes `_u<hex>`.
4211    let plain = seg
4212        .chars()
4213        .all(|c| c.is_ascii() && (c.is_ascii_alphanumeric() || c == '_' || c == '$'));
4214    let mut out = if plain {
4215        seg.to_string()
4216    } else {
4217        use std::fmt::Write as _;
4218        let mut s = String::with_capacity(seg.len() + 8);
4219        for c in seg.chars() {
4220            if c.is_ascii() && (c.is_ascii_alphanumeric() || c == '_' || c == '$') {
4221                s.push(c);
4222            } else {
4223                s.push_str("_u");
4224                let _ = write!(s, "{:x}", c as u32);
4225            }
4226        }
4227        s
4228    };
4229    // `out` is pure ASCII from here: the identifier-position repairs.
4230    if is_java_keyword_name(&out)
4231        || is_restricted_type_name(&out)
4232        || out.chars().next().is_some_and(|c| c.is_ascii_digit())
4233    {
4234        out.insert(0, '_');
4235    }
4236    // A lone `_` is a reserved IDENTIFIER since Java 9.
4237    if out == "_" {
4238        out = "__".to_string();
4239    }
4240    out
4241}
4242
4243/// Restricted contextual TYPE names — legal as member/local names
4244/// (rt.jar compiles `var` locals), illegal in class declarations and
4245/// type references. Consulted only on CLASS-name paths.
4246pub(crate) fn is_restricted_type_name(s: &str) -> bool {
4247    matches!(s, "var" | "yield" | "record" | "sealed" | "permits")
4248}
4249
4250fn is_java_keyword_name(s: &str) -> bool {
4251    matches!(
4252        s,
4253        "abstract"
4254            | "assert"
4255            | "boolean"
4256            | "break"
4257            | "byte"
4258            | "case"
4259            | "catch"
4260            | "char"
4261            | "class"
4262            | "const"
4263            | "continue"
4264            | "default"
4265            | "do"
4266            | "double"
4267            | "else"
4268            | "enum"
4269            | "extends"
4270            | "final"
4271            | "finally"
4272            | "float"
4273            | "for"
4274            | "goto"
4275            | "if"
4276            | "implements"
4277            | "import"
4278            | "instanceof"
4279            | "int"
4280            | "interface"
4281            | "long"
4282            | "native"
4283            | "new"
4284            | "package"
4285            | "private"
4286            | "protected"
4287            | "public"
4288            | "return"
4289            | "short"
4290            | "static"
4291            | "strictfp"
4292            | "super"
4293            | "switch"
4294            | "synchronized"
4295            | "this"
4296            | "throw"
4297            | "throws"
4298            | "transient"
4299            | "try"
4300            | "void"
4301            | "volatile"
4302            | "while"
4303            | "true"
4304            | "false"
4305            | "null"
4306            // `_` is a reserved identifier since Java 9 (Alipay's
4307            // instant-run fields are named `_`) — the `_<name>` mapping
4308            // turns it into `__`, matching jdc-core's call sites.
4309            | "_"
4310    )
4311}
4312
4313fn sanitize_ref(name: &str) -> String {
4314    sanitize_fq(name)
4315}
4316
4317/// Dotted source form of an internal name.
4318pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
4319    print_class_name(pool, internal)
4320}
4321
4322/// Unused import silencer.
4323#[allow(dead_code)]
4324fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
4325    let _ = java_type_to_generic;
4326}
4327
4328#[cfg(test)]
4329mod sanitize_tests {
4330    use super::sanitize_fq;
4331
4332    /// The property the file writer depends on: distinct class names must
4333    /// reach distinct output paths. The lossy predecessor of this mapping
4334    /// folded every non-identifier character to `_`, so 22,636 classes of
4335    /// `bin.mt.plus` (names built from Thai/Yi/Syriac code points) became
4336    /// three (`l.__`, `l.___`, `l._`) and all but three were overwritten.
4337    #[test]
4338    fn distinct_obfuscated_names_stay_distinct() {
4339        // The three names that collapsed onto `l/__.java` in the field.
4340        let names = ["l.᩻ܶ", "l.᩻ۡ", "l.֫᩷", "l.֡", "l.᩻᩶ۛ"];
4341        let mut mapped: Vec<String> = names.iter().map(|n| sanitize_fq(n)).collect();
4342        let before = mapped.len();
4343        mapped.sort();
4344        mapped.dedup();
4345        assert_eq!(mapped.len(), before, "sanitizer folded distinct names: {mapped:?}");
4346        assert!(
4347            mapped.iter().all(|m| m.is_ascii()),
4348            "sanitizer must stay ASCII: {mapped:?}"
4349        );
4350        // Self-describing: the escape carries the original code point.
4351        assert_eq!(sanitize_fq("l.᩻ܶ"), "l._u1a7b_u736");
4352    }
4353
4354    /// Every output must be a legal Java identifier segment: no leading
4355    /// digit, no keyword, not the lone `_` reserved since Java 9.
4356    #[test]
4357    fn output_is_a_legal_identifier() {
4358        for (input, want) in [
4359            ("do", "_do"),
4360            ("_", "__"),
4361            ("0Xx", "_0Xx"),
4362            ("Collection$-EL", "Collection$_u2dEL"),
4363            ("᩻", "_u1a7b"),
4364            ("a", "a"),
4365            ("A$B", "A$B"),
4366            ("x_y", "x_y"),
4367        ] {
4368            assert_eq!(sanitize_fq(input), want, "input {input:?}");
4369        }
4370    }
4371
4372    /// Documented residual: ident-safe ASCII passes through unchanged, so a
4373    /// literal `_u1a7b` collides with the single character U+1A7B. The
4374    /// collision is real — which is why `lossy_sanitize_renames` still owns
4375    /// a repair pass — and this test pins the shape the guard must catch.
4376    #[test]
4377    fn lookalike_collision_is_known_and_bounded() {
4378        assert_eq!(sanitize_fq("_u1a7b"), sanitize_fq("᩻"));
4379    }
4380}