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