Skip to main content

ddc_dec/
classdec.rs

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