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