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    set_obscured_state(class.name.clone(), obscured_map.clone(), blocked.clone());
164    let mut body_buf = String::with_capacity(out.capacity() / 2);
165    let body_res = emit_class_body(pool, class, &ctx, opts, &mut body_buf, 0);
166    let recorded = take_recorded_and_clear();
167    body_res?;
168    if !pkg.is_empty() {
169        out.push('\n');
170        out.push_str("package ");
171        let dotted = pkg.replace('/', ".");
172        out.push_str(&sanitize_fq(&dotted));
173        out.push_str(";\n");
174        // Imports: the metadata map plus the expression-level
175        // discoveries, pool-known only (an import of an unknown class
176        // is a hard error), display-renamed, sorted for determinism.
177        let mut import_set: jdc_core::FxHashSet<String> =
178            obscured_map.keys().cloned().collect();
179        for r in recorded {
180            if pool.get(&r).is_some() {
181                let simple = r.rsplit('/').next().unwrap_or("");
182                if !blocked.contains(simple) {
183                    import_set.insert(r);
184                }
185            }
186        }
187        let mut imports: Vec<String> = import_set
188            .iter()
189            .map(|internal| crate::classdec::dotted(&crate::apply_class_rename(internal)))
190            .collect();
191        imports.sort();
192        for display in imports {
193            out.push_str(&format!("import {};\n", display));
194        }
195    }
196    out.push('\n');
197    out.push_str(&body_buf);
198    Ok(out)
199}
200
201/// Render the class header, fields, methods and nested member classes into
202/// `out` (used at top level and recursively for nested members).
203// `opts` is only consumed by the nested-member recursion below — that
204// is its purpose (propagating emission options into inline children).
205/// One enum constant: the source identifier plus constructor
206/// arguments beyond the compiler-mandated `(String name, int ordinal)`.
207use jdc_core::ir::expr::{ConstVal, Expr};
208use jdc_core::ir::stmt::Stmt;
209struct EnumConst {
210    #[allow(dead_code)]
211    field: String,
212    name: String,
213    extra_args: Vec<Expr>,
214}
215
216/// Collect enum constants for a true `enum` rendering. Every ACC_ENUM
217/// static field must be initialized in `<clinit>` by
218/// `Self.field = new Self("NAME", ordinal, ...)` — the shape javac/d8
219/// always emit. Returns None (caller falls back to the desugared
220/// `/* enum */ class` form) when anything is missing: R8 variance,
221/// constant-specific bodies (the field holds an anonymous subclass), or
222/// a <clinit> that failed to decompile.
223fn collect_enum_constants(
224    pool: &DexPool,
225    class: &PoolClass,
226    ctx: &DexCtx<'_>,
227) -> Option<(Vec<EnumConst>, crate::method::MethodBody)> {
228    let _ = ctx;
229    let const_fields: Vec<&PoolField> = class
230        .static_fields
231        .iter()
232        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
233        .collect();
234    // No ACC_ENUM constant fields: R8 drops them ALL when nothing
235    // outside reads them (weixin sp2/ji — only the synthetic $VALUES
236    // array survives). An ACC_ENUM CLASS still promotes: the synthetic-
237    // constant scan below lifts the constants out of the array literal.
238    // Non-enum classes have no business here.
239    if const_fields.is_empty() && class.access & crate::access::ACC_ENUM == 0 {
240        return None;
241    }
242    let clinit = class.all_methods().find(|m| &*m.name == "<clinit>")?;
243    let mut body = decompile_method(pool, class, clinit).ok().flatten()?;
244
245    // R8/d8 split the constant build across an intermediate local:
246    //   Self v0 = new Self("NAME", i, ...);
247    //   a = v0;                       // sput to the ACC_ENUM field
248    //   Self[] v5 = new Self[n]; v5[k] = v0; ...; $VALUES = v5;
249    // Pass 1 registers each intermediate (or direct) new; pass 2 binds
250    // them to the ACC_ENUM fields; references to the locals are then
251    // rewritten into constant identifiers so the $VALUES build keeps
252    // compiling once the definitions drop.
253    use std::collections::HashMap;
254    let mut const_name: Vec<String> = Vec::new();
255    let mut const_field: Vec<String> = Vec::new();
256    let mut const_extra: Vec<Vec<Expr>> = Vec::new();
257    // Declared ordinal per constant (the ctor's int arg) — the merge at
258    // the end orders field-bound and synthetic constants by it and
259    // demands a dense 0..n run (Java derives ordinals from declaration
260    // position).
261    let mut const_ord: Vec<i64> = Vec::new();
262    let mut var_of: HashMap<u32, usize> = HashMap::default(); // local id -> const idx
263    let mut drop_stmts: Vec<usize> = Vec::new();
264
265    // Collect (immutable borrows) first; the mutable passes come after.
266    if !matches!(&body.body, Stmt::Block(_)) { return None; }
267
268    // Pass 1: definitions. (immutable borrow; rewrite comes later)
269    // Rolling reaching-defs of clinit locals for resolving enum-ctor
270    // extra args (resolve_enum_arg); `tainted` flips at the first
271    // non-straight-line statement — past it, linear-scan defs are no
272    // longer provable and Local args reject the enum mode.
273    let mut defs: HashMap<u32, &Expr> = HashMap::default();
274    let mut tainted = false;
275    let self_name: std::sync::Arc<str> = class.name.as_str().into();
276    let self_ty =
277        jdc_core::ir::expr::TypeRef::J(JavaType::Object(class.name.as_str().into()));
278    // A REUSED intermediate local: `v = new Self(..); A = v; v = new
279    // Self(..); B = v;` — the first def is a LocalDef, the rest are
280    // Assigns to the same var. Register every def site; pass 2 resolves
281    // a local sput to the def site CURRENT at that statement (rolling),
282    // not to the var (Kotlin EnumEntries enums, RegexOption: 8→7 count
283    // mismatch used to abort the whole promotion).
284    let mut def_site: HashMap<usize, (u32, usize)> = HashMap::default();
285    for (i, st) in match &body.body {
286        Stmt::Block(v) => v.iter().enumerate(),
287        _ => return None,
288    } {
289        // (var, New) for both def shapes: LocalDef and Assign-to-local.
290        let def_shape: Option<(u32, &Expr)> = match st {
291            Stmt::LocalDef {
292                var,
293                init: Some(e),
294                ..
295            } => Some((*var, e)),
296            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
297                match (&**target, &**value) {
298                    (Expr::Local { var, .. }, e) => Some((*var, e)),
299                    _ => None,
300                }
301            }
302            _ => None,
303        };
304        if let Some((var, Expr::New { cls: ncls, args, .. })) = def_shape {
305            if ncls.as_ref() == class.name && args.len() >= 2 {
306                if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
307                    (&args[0], &args[1])
308                {
309                    if java_ident(n).as_ref() != &**n || n.is_empty() { return None; }
310                    var_of.insert(var, const_name.len());
311                    def_site.insert(i, (var, const_name.len()));
312                    const_ord.push(*ord0 as i64);
313                    const_name.push(n.to_string());
314                    const_field.push(String::new());
315                    const_extra.push(resolve_enum_extras(
316                        &args[2..],
317                        &defs,
318                        tainted,
319                        &var_of,
320                        &const_name,
321                        &self_name,
322                        &self_ty,
323                    )?);
324                    drop_stmts.push(i);
325                }
326            }
327        }
328        track_def(st, &mut defs, &mut tainted);
329    }
330
331    // Pass 2: sputs to the ACC_ENUM fields (direct new or intermediate).
332    // Local sputs resolve through the ROLLING def map (updated at each
333    // def-site statement) — a reused `v` must bind to the constant
334    // defined most recently, not to the var's first registration.
335    let mut defs2: HashMap<u32, &Expr> = HashMap::default();
336    let mut tainted2 = false;
337    let mut cur_def: HashMap<u32, usize> = HashMap::default();
338    for (i, st) in match &body.body {
339        Stmt::Block(v) => v.iter().enumerate(),
340        _ => return None,
341    } {
342        if let Some((var, idx)) = def_site.get(&i) {
343            cur_def.insert(*var, *idx);
344        }
345        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
346            if let Expr::Field {
347                cls,
348                name: fname,
349                is_static: true,
350                ..
351            } = &**target
352            {
353                if cls.as_ref() != class.name {
354                    continue;
355                }
356                if !const_fields.iter().any(|f| f.name.as_str() == &**fname) {
357                    continue;
358                }
359                if const_field.iter().any(|f| !f.is_empty() && f == &**fname) { { return None; } // duplicate assignment
360                }
361                let idx = match &**value {
362                    Expr::Local { var, .. } => cur_def.get(var).copied()?,
363                    Expr::New { cls: ncls, args, .. }
364                        if ncls.as_ref() == class.name && args.len() >= 2 =>
365                    {
366                        if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
367                            (&args[0], &args[1])
368                        {
369                            if java_ident(n).as_ref() != &**n || n.is_empty() { return None; }
370                            let idx = const_name.len();
371                            const_ord.push(*ord0 as i64);
372                            const_name.push(n.to_string());
373                            const_field.push(String::new());
374                            const_extra.push(resolve_enum_extras(
375                                &args[2..],
376                                &defs2,
377                                tainted2,
378                                &var_of,
379                                &const_name,
380                                &self_name,
381                                &self_ty,
382                            )?);
383                            idx
384                        } else {
385                            { return None; }
386                        }
387                    }
388                    _ => return None,
389                };
390                const_field[idx] = fname.to_string();
391                drop_stmts.push(i);
392            }
393        }
394        track_def(st, &mut defs2, &mut tainted2);
395    }
396    // Own the reaching-def values: defs2 borrows body.body, and the
397    // synthetic-constant scan below mutates the $VALUES array in place.
398    let defs2_owned: std::collections::HashMap<u32, Expr> = defs2
399        .iter()
400        .map(|(k, v)| (*k, (*v).clone()))
401        .collect();
402    drop(defs2);
403    let defs2: std::collections::HashMap<u32, &Expr> =
404        defs2_owned.iter().map(|(k, v)| (*k, v)).collect();
405
406    // Every ACC_ENUM field bound, every intermediate matched.
407    // Every ACC_ENUM FIELD must be bound to a constant. The converse
408    // need not hold: R8 drops the FIELD of a constant nothing reads
409    // externally while the $VALUES array keeps it (weixin lite/api/n's
410    // ON_DESTROY — a pass-1 local with no pass-2 sput). Field-less
411    // constants keep their ctor-string source name.
412    if const_field.iter().filter(|f| !f.is_empty()).count() != const_fields.len() {
413               return None;
414    }
415    // Obfuscated enums rename the ACC_ENUM FIELD (d/e/f) while the ctor's
416    // name STRING keeps the source identifier — the promoted constant
417    // must be declared under the FIELD name: every reference in the
418    // pool resolves through it. Using the name string broke all
419    // cross-file references (weixin +2.9k when newly-promoted enums
420    // swapped d/e/f for TEXT_ENTER_EDITING/…).
421    for i in 0..const_name.len() {
422        let field = &const_field[i];
423        if !field.is_empty() && field != &const_name[i] {
424            let id = java_ident(field);
425            if id.is_empty() || id.as_ref() != field.as_str() {
426                { return None; }
427            }
428            const_name[i] = field.clone();
429        }
430    }
431    {
432        let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::default();
433        if !const_name.iter().all(|c| seen.insert(c.as_str())) { return None; }
434    }
435
436    // Synthetic constants: entries built INLINE inside the $VALUES
437    // array (`new Self("NAME", ord, ..)` with no ACC_ENUM field — R8
438    // drops the field when nothing outside reads it; weixin u2/i's
439    // "CONSTANT"). Promote them so the header declares the constant:
440    // the raw `new` in the array is "无法实例化枚举类", and every
441    // constant after the gap carries a SHIFTED ordinal (e was 2 in the
442    // dex, renders as position 1).
443    let mut synth: Vec<(i64, String, Vec<Expr>)> = Vec::new();
444    {
445        let self_cls: &str = class.name.as_str();
446        if let Stmt::Block(vs) = &mut body.body {
447            for st in vs.iter_mut() {
448                crate::passes::walk_stmt_exprs(st, &mut |e| {
449                    crate::passes::deep_rewrite(e, &mut |x| {
450                        let Expr::NewArray { elem, init: Some(list), .. } = x else {
451                            return;
452                        };
453                        if elem.erased() != JavaType::Object(self_cls.into()) {
454                            return;
455                        }
456                        for slot in list.iter_mut() {
457                            let (n, ord, rest) = match slot {
458                                Expr::New { cls, args, raw: false, .. }
459                                    if cls.as_ref() == self_cls && args.len() >= 2 =>
460                                {
461                                    match (&args[0], &args[1]) {
462                                        (
463                                            Expr::Const(ConstVal::Str(sv)),
464                                            Expr::Const(ConstVal::Int(o)),
465                                        ) => (sv.to_string(), *o as i64, args[2..].to_vec()),
466                                        _ => continue,
467                                    }
468                                }
469                                _ => continue,
470                            };
471                            if java_ident(&n).as_ref() != n.as_str() || n.is_empty() {
472                                continue;
473                            }
474                            if const_name.contains(&n)
475                                || synth.iter().any(|(_, s2, _)| *s2 == n)
476                            {
477                                continue;
478                            }
479                            let Some(extras) = resolve_enum_extras(
480                                &rest,
481                                &defs2,
482                                tainted2,
483                                &var_of,
484                                &const_name,
485                                &self_name,
486                                &self_ty,
487                            ) else {
488                                continue;
489                            };
490                            *slot = Expr::Field {
491                                owner: None,
492                                cls: self_name.clone(),
493                                name: std::sync::Arc::from(n.as_str()),
494                                ty: self_ty.clone(),
495                                is_static: true,
496                            };
497                            synth.push((ord, n, extras));
498                        }
499                    });
500                });
501            }
502        }
503    }
504
505    // Pass 3: rewrite references to the intermediate locals into the
506    // constant identifiers (static-field reads on Self). POSITION-AWARE:
507    // a REUSED local must read as the constant defined most recently at
508    // that statement — a var-level map made every `arr[k] = v` in the
509    // $VALUES build reference the LAST constant (weixin +2.9k when this
510    // regressed values() contents).
511    {
512        let mut cur: HashMap<u32, usize> = HashMap::default();
513        if let Stmt::Block(vs) = &mut body.body {
514            for (i, st) in vs.iter_mut().enumerate() {
515                if let Some((var, idx)) = def_site.get(&i) {
516                    cur.insert(*var, *idx);
517                }
518                crate::passes::walk_stmt_exprs(st, &mut |e| {
519                    crate::passes::deep_rewrite(e, &mut |x| {
520                        if let Expr::Local { var, .. } = x {
521                            if let Some(&idx) = cur.get(var) {
522                                let name: std::sync::Arc<str> =
523                                    std::sync::Arc::from(const_name[idx].as_str());
524                                *x = Expr::Field {
525                                    owner: None,
526                                    cls: self_name.clone(),
527                                    name,
528                                    ty: self_ty.clone(),
529                                    is_static: true,
530                                };
531                            }
532                        }
533                    });
534                });
535            }
536        }
537    }
538
539    // Pass 4: drop the definitions and their sputs.
540    if let Stmt::Block(vs) = &mut body.body {
541        let drop_set: std::collections::HashSet<usize> =
542            drop_stmts.iter().copied().collect();
543        vs.retain(|_| true);
544        let mut idx = 0usize;
545        vs.retain(|_| {
546            let keep = !drop_set.contains(&idx);
547            idx += 1;
548            keep
549        });
550    }
551    // The resolved args consumed the clinit locals' readers; prune the
552    // now-dead defs from the remnant (impure inits survive as bare
553    // expression statements — drop_dead_locals' standard contract).
554    crate::passes::drop_dead_locals(&mut body.body);
555
556    let mut merged: Vec<(i64, EnumConst)> = const_ord
557        .into_iter()
558        .zip(
559            const_field
560                .into_iter()
561                .zip(const_name)
562                .zip(const_extra)
563                .map(|((field, name), extra_args)| EnumConst {
564                    field,
565                    name,
566                    extra_args,
567                }),
568        )
569        .collect();
570    for (ord, name, extra_args) in synth {
571        merged.push((
572            ord,
573            EnumConst {
574                field: name.clone(),
575                name,
576                extra_args,
577            },
578        ));
579    }
580    if merged.is_empty() {
581        return None; // nothing extracted — keep the desugared form
582    }
583    merged.sort_by_key(|(o, _)| *o);
584    // Ordinals must be UNIQUE and ASCENDING; gaps are legal — R8 drops
585    // unused constants wholesale (weixin lite/api/n starts at ordinal
586    // 1). Java derives ordinal() from declaration position, so a gap
587    // shifts every later constant. Pad each gap with a synthetic
588    // constant (`_r<k>`) to keep positions faithful — only when no
589    // constant carries extra ctor args (a pad cannot invent them).
590    // The synthetic values()/valueOf() the true-enum render suppresses
591    // then see the pad (values() drift vs the dex $VALUES array — the
592    // compilable-and-ordinal-faithful side of the trade).
593    let has_extras = merged.iter().any(|(_, c)| !c.extra_args.is_empty());
594    let mut member_taken: jdc_core::FxHashSet<String> = merged
595        .iter()
596        .map(|(_, c)| c.name.clone())
597        .chain(
598            class
599                .static_fields
600                .iter()
601                .chain(class.instance_fields.iter())
602                .map(|f| f.name.to_string()),
603        )
604        .chain(class.all_methods().map(|m| m.name.to_string()))
605        .collect();
606    let mut padded: Vec<EnumConst> = Vec::with_capacity(merged.len());
607    let mut expect: i64 = 0;
608    let mut pad_k = 0u32;
609    for (ord, c) in merged {
610        if ord < expect {
611            { return None; } // duplicate/colliding ordinals — unfaithful
612        }
613        if ord > expect {
614            if has_extras {
615                { return None; } // cannot synthesize the missing ctor args
616            }
617            while expect < ord {
618                let pname = loop {
619                    let cand = format!("_r{pad_k}");
620                    pad_k += 1;
621                    if !member_taken.contains(&cand) {
622                        member_taken.insert(cand.clone());
623                        break cand;
624                    }
625                };
626                padded.push(EnumConst {
627                    field: pname.clone(),
628                    name: pname,
629                    extra_args: Vec::new(),
630                });
631                expect += 1;
632            }
633        }
634        expect = ord + 1;
635        padded.push(c);
636    }
637    let out = padded;
638    Some((out, body))
639}
640
641/// Update the rolling reaching-def map for enum-arg resolution. Only
642/// flat single-target definitions keep the tracking sound; any control
643/// flow taints it (a linear scan can no longer prove WHICH definition
644/// reaches later capture sites).
645fn track_def<'e>(
646    st: &'e Stmt,
647    defs: &mut std::collections::HashMap<u32, &'e Expr>,
648    tainted: &mut bool,
649) {
650    match st {
651        Stmt::LocalDef { var, init, .. } => match init {
652            Some(e) => {
653                defs.insert(*var, e);
654            }
655            None => {
656                defs.remove(var);
657            }
658        },
659        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
660            if let Expr::Local { var, .. } = &**target {
661                if matches!(op, jdc_core::ir::expr::AssignOp::Plain) {
662                    defs.insert(*var, value);
663                } else {
664                    defs.remove(var);
665                }
666            }
667        }
668        // Plain expression statements (calls) don't define locals.
669        Stmt::ExprStmt(_) => {}
670        _ => *tainted = true,
671    }
672}
673
674/// Resolve every enum-ctor extra arg to a self-contained expression, or
675/// reject the enum mode (None). R8 reuses ONE register across all
676/// constant constructions — revenuecat's LogIntent builds 11 of 12
677/// emoji lists through the same `list` local, reassigned between the
678/// `new Self(.., list)` sites — and enum constants render OUTSIDE the
679/// clinit where no local is in scope: an unresolved `Local` used to
680/// print as the vt-dummy name (`DEBUG(var0)` — per-constant
681/// cannot-find). Each Local is replaced by its reaching pure definition
682/// (recursively) or, when it names an earlier constant's intermediate,
683/// by a static-field reference to that constant. Any expression shape
684/// WITHOUT locals passes through untouched (enum args may be arbitrary
685/// expressions — BinOp/Cast/Method/New — each renders exactly once per
686/// constant, so no duplication concern applies). Only an unresolvable
687/// Local (missing/tainted def, depth > 4) rejects the whole enum
688/// detection; the class then falls back to plain-field rendering, which
689/// always compiles.
690fn resolve_enum_extras(
691    extras: &[Expr],
692    defs: &std::collections::HashMap<u32, &Expr>,
693    tainted: bool,
694    var_of: &std::collections::HashMap<u32, usize>,
695    const_name: &[String],
696    self_name: &std::sync::Arc<str>,
697    self_ty: &jdc_core::ir::expr::TypeRef,
698) -> Option<Vec<Expr>> {
699    extras
700        .iter()
701        .map(|e| {
702            let mut out = e.clone();
703            let mut fail = false;
704            resolve_locals_in(
705                &mut out, defs, tainted, var_of, const_name, self_name, self_ty, 0,
706                &mut fail,
707            );
708            if fail {
709                None
710            } else {
711                Some(out)
712            }
713        })
714        .collect()
715}
716
717#[allow(clippy::too_many_arguments)]
718fn resolve_locals_in(
719    e: &mut Expr,
720    defs: &std::collections::HashMap<u32, &Expr>,
721    tainted: bool,
722    var_of: &std::collections::HashMap<u32, usize>,
723    const_name: &[String],
724    self_name: &std::sync::Arc<str>,
725    self_ty: &jdc_core::ir::expr::TypeRef,
726    depth: u32,
727    fail: &mut bool,
728) {
729    crate::passes::deep_rewrite(e, &mut |x| {
730        if let Expr::Local { var, .. } = x {
731            // An earlier constant's intermediate: a static-field read of
732            // that constant (declared above — backward reference, legal
733            // in enum ctor args).
734            if let Some(&ci) = var_of.get(var) {
735                if let Some(nm) = const_name.get(ci) {
736                    *x = Expr::Field {
737                        owner: None,
738                        cls: self_name.clone(),
739                        name: std::sync::Arc::from(nm.as_str()),
740                        ty: self_ty.clone(),
741                        is_static: true,
742                    };
743                    return;
744                }
745            }
746            if tainted || depth > 4 {
747                *fail = true;
748                return;
749            }
750            let Some(d) = defs.get(var).copied() else {
751                *fail = true;
752                return;
753            };
754            let mut sub = d.clone();
755            resolve_locals_in(
756                &mut sub, defs, tainted, var_of, const_name, self_name, self_ty,
757                depth + 1, fail,
758            );
759            if *fail {
760                return;
761            }
762            *x = sub;
763        }
764    });
765}
766
767/// Render enum-constant constructor arguments via the shared expression
768/// emitter (the VarTable is irrelevant for argument printing — no local
769/// names appear — but the API requires one).
770fn render_enum_args(ctx: &DexCtx<'_>, pool: &DexPool, args: &[Expr], out: &mut String) {
771    let dummy_vt = jdc_core::var::VarTable::default();
772    let mut p = Printer::new(ctx, &dummy_vt);
773    for (i, a) in args.iter().enumerate() {
774        if i > 0 {
775            out.push_str(", ");
776        }
777        p.expr(a, 0, out);
778    }
779    let _ = pool;
780}
781
782#[allow(clippy::only_used_in_recursion)]
783fn emit_class_body(
784    pool: &DexPool,
785    class: &PoolClass,
786    ctx: &DexCtx<'_>,
787    opts: &ClassOptions,
788    out: &mut String,
789    depth: usize,
790) -> anyhow::Result<()> {
791    let ind = indent(depth);
792    // The class's own header must use the RENAMED display name — the
793    // ctor path (is_init) already renames; a header declaring `class a`
794    // while the file/ctor say `a_2` leaves the ctor looking like a
795    // method with no return type (MinisApp's Y2.a vs y2.a package
796    // case-collision: 2,518 javac parse errors).
797    let cname = crate::apply_class_rename(&class.name);
798    let (_, simple) = split_name(&cname);
799    // A class emitted as its OWN top-level file (depth 0) must declare a
800    // flat `$` name — `class Outer.Inner` is not declarable at file
801    // scope. An INLINE nested member (depth > 0) declares its own
802    // segment inside the parent's body.
803    let simple = if depth == 0 {
804        simple.to_string()
805    } else {
806        // R8 names can END in `$`: an empty last `$`-segment would
807        // render `class  {` — keep the whole simple name.
808        let seg = simple.rsplit('$').next().unwrap_or(&simple);
809        if seg.is_empty() {
810            simple.to_string()
811        } else {
812            seg.to_string()
813        }
814    };
815    // A class NAMED `var`-style (taobao ships `tb.var`) cannot be
816    // declared — restricted contextual type names escape here and in
817    // the ctor, file name, and every type reference.
818    let simple = if is_restricted_type_name(&simple) {
819        format!("_{simple}")
820    } else {
821        simple
822    };
823    let is_iface = class.is_interface();
824    let is_enum = class.is_enum();
825
826    let mut head = String::new();
827    let a = class.access;
828    if a & ACC_PUBLIC != 0 || widen_class(&class.name) {
829        head.push_str("public ");
830    }
831    // Inline nested members need their `static` (interfaces/annotations
832    // are implicitly static; a missing `static` on a member class makes
833    // every `new Report(...)` site an "outer instance required" error).
834    if depth > 0 && !is_iface && ctx.nested_is_static(&class.name) {
835        head.push_str("static ");
836    }
837    if a & ACC_FINAL != 0 && !is_enum {
838        head.push_str("final ");
839    }
840    if a & ACC_ABSTRACT != 0 && !is_iface {
841        head.push_str("abstract ");
842    }
843    if a & ACC_ANNOTATION != 0 {
844        head.push('@');
845    }
846    let mut enum_consts: Option<(Vec<EnumConst>, crate::method::MethodBody)> = if is_enum {
847        collect_enum_constants(pool, class, ctx)
848    } else {
849        None
850    };
851    if let Some(ecs) = &enum_consts {
852        // True `enum` declaration: constants render in the header, the
853        // desugared boilerplate (const fields, their <clinit> inits —
854        // stripped by strip_enum_const_inits — and the ACC_ENUM flags)
855        // disappears. R8-renamed values()/valueOf() stay (they do not
856        // collide with the compiler-generated ones); javac-named ones
857        // are skipped at the method loop below.
858        head.push_str("enum ");
859        head.push_str(&java_ident(&simple));
860        let _ = ecs;
861    } else if is_enum {
862        // An enum with constant-specific bodies carries ACC_ABSTRACT —
863        // `abstract final` is an illegal modifier combination; abstract
864        // (already pushed above) suppresses the hardcoded final.
865        let abstract_ = a & ACC_ABSTRACT != 0;
866        head.push_str(if abstract_ {
867            "/* enum */ class "
868        } else {
869            "/* enum */ final class "
870        });
871        head.push_str(&java_ident(&simple));
872    } else if is_iface {
873        head.push_str("interface ");
874        head.push_str(&java_ident(&simple));
875    } else {
876        head.push_str("class ");
877        head.push_str(&java_ident(&simple));
878    }
879    // The obscured-super import was emitted at the package line; the
880    // clause must render the SIMPLE name (the qualified form binds to
881    // the class itself even with the import present).
882    let render_super = |sup: &String| -> String { print_class_name(pool, sup) };
883    if is_iface {
884        // A @interface cannot declare extends at all (JLS 9.6): the dex
885        // interface table lists java/lang/annotation/Annotation for
886        // every annotation type — rendering it produced "对于
887        // @interfaces, 不允许 'extends'" (weibo 1100).
888        let is_annot = a & ACC_ANNOTATION != 0;
889        if !class.interfaces.is_empty() && !is_annot {
890            head.push_str(" extends ");
891            head.push_str(&join_dotted(pool, &class.interfaces));
892        }
893    } else {
894        if let Some(sup) = &class.super_name {
895            if sup != "java/lang/Object" && !is_enum {
896                head.push_str(" extends ");
897                head.push_str(&render_super(sup));
898            }
899        }
900        if !class.interfaces.is_empty() {
901            head.push_str(if is_iface {
902                " extends "
903            } else {
904                " implements "
905            });
906            head.push_str(&join_dotted(pool, &class.interfaces));
907        }
908    }
909
910    out.push_str(&ind);
911    out.push_str(&head);
912    out.push_str(" {\n");
913
914    // True-enum constant list: the constants lead the body, ahead of
915    // any remaining fields.
916    if let Some((ecs, _)) = &enum_consts {
917        for (i, ec) in ecs.iter().enumerate() {
918            out.push_str(&indent(depth + 1));
919            out.push_str(&java_ident(&ec.name));
920            if !ec.extra_args.is_empty() {
921                let mut line = String::from("(");
922                render_enum_args(ctx, pool, &ec.extra_args, &mut line);
923                line.push(')');
924                out.push_str(&line);
925            }
926            if i + 1 < ecs.len() {
927                out.push_str(",\n");
928            } else {
929                out.push_str(";\n");
930            }
931        }
932        out.push('\n');
933    }
934
935    // Fields.
936    // Kotlin `object`/lazy singletons: the dex static value for INSTANCE
937    // is null while the clinit assigns it — rendering `= null` as the
938    // field initializer made the clinit assignment illegal ("无法为
939    // static final 变量 INSTANCE 分配值", okio SegmentPool family). A
940    // static final whose static value is null AND which the clinit
941    // sputs renders as a blank final. Scan the clinit's raw SPuts once.
942    let clinit_sputs: Option<jdc_core::FxHashSet<(std::sync::Arc<str>, std::sync::Arc<str>)>> =
943        // Gate: only classes with a null-valued final static can produce
944        // a blank final — skip the clinit decode otherwise (decoding it
945        // for every class cost seconds on 98k-class corpora).
946        (|| {
947            let has_null_final = class.static_fields.iter().enumerate().any(|(i, f)| {
948                f.access & crate::access::ACC_FINAL != 0
949                    && matches!(class.static_values.get(i), Some(StaticValue::Null))
950            });
951            if !has_null_final {
952                return None;
953            }
954            let m = class.all_methods().find(|m| &*m.name == "<clinit>")?;
955            let dex = pool.dex(m.dex_idx)?;
956            let ci = dex.code_at(m.code_off)?;
957            let mut set: jdc_core::FxHashSet<(
958                std::sync::Arc<str>,
959                std::sync::Arc<str>,
960            )> = jdc_core::FxHashSet::default();
961            for ins in &ci.insns {
962                if let InsnKind::SPut { field_idx, .. } = ins.kind {
963                    let fid = dex.field(field_idx);
964                    // clinit only writes own-class fields here; match
965                    // (name, type) against the field list below.
966                    set.insert((
967                        std::sync::Arc::from(dex.string(fid.name_idx)),
968                        std::sync::Arc::from(dex.type_name(fid.type_idx)),
969                    ));
970                }
971            }
972            Some(set)
973        })();
974    let mut field_emitted = false;
975    for (i, f) in class.static_fields.iter().enumerate() {
976        // Enum constant fields became the header list above.
977        if enum_consts.is_some() && f.access & crate::access::ACC_ENUM != 0 {
978            continue;
979        }
980        if field_emitted || !class.instance_fields.is_empty() {
981            out.push('\n');
982        }
983        field_emitted = true;
984        // A blank-final: static final, null static value, clinit-assigned.
985        let blank_final = f.access & crate::access::ACC_FINAL != 0
986            && matches!(class.static_values.get(i), Some(StaticValue::Null))
987            && clinit_sputs
988                .as_ref()
989                .is_some_and(|s| {
990                    s.contains(&(
991                        std::sync::Arc::from(f.name.as_str()),
992                        std::sync::Arc::from(f.desc.as_str()),
993                    ))
994                });
995        emit_field(
996            pool,
997            f,
998            if blank_final {
999                None
1000            } else {
1001                class.static_values.get(i)
1002            },
1003            &class.name,
1004            out,
1005            depth + 1,
1006            true,
1007            class.is_interface(),
1008        );
1009    }
1010    if !class.instance_fields.is_empty() && !class.static_fields.is_empty() {
1011        out.push('\n');
1012    }
1013    for (i, f) in class.instance_fields.iter().enumerate() {
1014        if i > 0 {
1015            out.push('\n');
1016        }
1017        emit_field(pool, f, None, &class.name, out, depth + 1, false, false);
1018    }
1019
1020    // Methods.
1021    // Duplicated (name, parameter-types) pairs in one class are javac
1022    // "method is already defined" errors (weibo: 11.5k hits). Source
1023    // cannot declare them, so exactly one survives: covariant bridges
1024    // (`Object get(int)` beside `ByteString get(int)` — the compiler
1025    // GENERATES bridges), plus R8 output that lost the bridge flag
1026    // (okio Buffer.clone, interface getView re-declarations). A
1027    // non-bridge method outranks a bridge for the same signature
1028    // (bridge bodies are delegation stubs); first occurrence otherwise.
1029    // Never drop a signature outright — the last method standing for a
1030    // key is always rendered.
1031    fn sig_key(m: &PoolMethod) -> (&str, &str) {
1032        let d: &str = &m.desc;
1033        let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1034        let hi = d.find(')').unwrap_or(d.len());
1035        (&*m.name, &d[lo..hi])
1036    }
1037    let methods: Vec<&PoolMethod> = class.all_methods().collect();
1038    let mut claim: jdc_core::FxHashMap<(&str, &str), usize> = jdc_core::FxHashMap::default();
1039    for (i, m) in methods.iter().enumerate() {
1040        let key = sig_key(m);
1041        match claim.get(&key) {
1042            Some(&j)
1043                if m.access & crate::access::ACC_BRIDGE == 0
1044                    && methods[j].access & crate::access::ACC_BRIDGE != 0 =>
1045            {
1046                claim.insert(key, i);
1047            }
1048            None => {
1049                claim.insert(key, i);
1050            }
1051            _ => {}
1052        }
1053    }
1054    // NOTE: a same-signature bridge is already retired by the claim map
1055    // above. An ERASURE-shaped bridge (SAM/variance: params are the
1056    // Object-erased types of a sibling's) must NOT be skipped: with raw
1057    // (non-generic) interface rendering the bridge is exactly what
1058    // satisfies the interface — skipping it turned every Kotlin lambda
1059    // class into "不是抽象的, 并且未覆盖…invoke(Object,Object)" (reqable
1060    // +17), while the "对invoke的引用不明确" it appeared to fix was a
1061    // missing-kotlin-classpath cascade, not a ddc bug.
1062    let mut emitted_any = !class.static_fields.is_empty() || !class.instance_fields.is_empty();
1063    for (i, m) in methods.iter().enumerate() {
1064        if &*m.name == "<clinit>" {
1065            continue; // rendered after the fields
1066        }
1067        if claim.get(&sig_key(m)) != Some(&i) {
1068            continue;
1069        }
1070        // True-enum rendering: javac auto-generates values()/valueOf()
1071        // — the original-named ones must not re-declare (R8-renamed
1072        // copies stay, they do not collide).
1073        if let Some((ecs, _)) = &enum_consts {
1074            let d = m.parsed_desc();
1075            let self_arr = d.as_ref().map(|d| d.ret == JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into()))));
1076            let self_ret = d.as_ref().map(|d| d.ret == JavaType::Object(class.name.as_str().into()));
1077            let no_args = d.as_ref().map(|d| d.args.is_empty()).unwrap_or(false);
1078            let one_str = d
1079                .as_ref()
1080                .map(|d| d.args.len() == 1 && matches!(d.args[0], JavaType::Object(ref n) if n.as_ref() == "java/lang/String"))
1081                .unwrap_or(false);
1082            let static_ = m.is_static();
1083            if static_ && no_args && self_arr == Some(true) && &*m.name == "values" {
1084                continue;
1085            }
1086            if static_ && one_str && self_ret == Some(true) && &*m.name == "valueOf" {
1087                continue;
1088            }
1089            // Enum constructors must be private in source form.
1090            let m_owned: Option<PoolMethod> = if &*m.name == "<init>" {
1091                let mut c = (*m).clone();
1092                c.access = (c.access & !(crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED | crate::access::ACC_PRIVATE)) | crate::access::ACC_PRIVATE;
1093                Some(c)
1094            } else {
1095                None
1096            };
1097            let m_ref: &PoolMethod = m_owned.as_ref().unwrap_or(m);
1098            let mark = out.len();
1099            if emitted_any {
1100                out.push('\n');
1101            }
1102            if emit_method(pool, class, ctx, m_ref, depth + 1, true, out)? {
1103                emitted_any = true;
1104            } else {
1105                out.truncate(mark);
1106            }
1107            let _ = ecs;
1108            continue;
1109        }
1110        let mark = out.len();
1111        if emitted_any {
1112            out.push('\n');
1113        }
1114        if emit_method(pool, class, ctx, m, depth + 1, enum_consts.is_some(), out)? {
1115            emitted_any = true;
1116        } else {
1117            out.truncate(mark);
1118        }
1119    }
1120    // Inherited-ctor bridges: dex method refs resolve through the
1121    // hierarchy, so `new C(args)` against a class C with NO declared
1122    // <init> legally targets the SUPERCLASS ctor (weixin tenpay
1123    // `new m(map)` — m declares nothing, i.<init>(HashMap) does). Java
1124    // has no inherited constructors: without a bridge, C's implicit
1125    // default ctor is the only one and every arg-carrying construction
1126    // fails ("无法将类 m中的构造器 m应用到给定类型; 需要: 没有参数").
1127    // Mirror the nearest ctor-declaring ancestor's public/protected
1128    // ctors as thin `super(..)` delegations.
1129    if !class.is_interface()
1130        && enum_consts.is_none()
1131        && !class.all_methods().any(|m| &*m.name == "<init>")
1132    {
1133        if let Some(mut sup) = class.super_name.clone() {
1134            loop {
1135                if sup == "java/lang/Object" {
1136                    break;
1137                }
1138                let Some(sc) = pool.get(&sup) else { break };
1139                let ctors: Vec<&PoolMethod> = sc
1140                    .all_methods()
1141                    .filter(|m| &*m.name == "<init>")
1142                    .collect();
1143                if !ctors.is_empty() {
1144                    for sm in ctors {
1145                        if sm.access & crate::access::ACC_PRIVATE != 0
1146                            || (sm.access
1147                                & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
1148                                == 0)
1149                        {
1150                            continue; // private/package-private: no legal bridge
1151                        }
1152                        let Some(d) = sm.parsed_desc() else { continue };
1153                        let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
1154                            "public "
1155                        } else {
1156                            "protected "
1157                        };
1158                        if emitted_any {
1159                            out.push('\n');
1160                        }
1161                        out.push_str(&format!("    {}", "    ".repeat(depth)));
1162                        out.push_str(mods);
1163                        out.push_str(&java_ident(&simple));
1164                        out.push('(');
1165                        let mut names = Vec::with_capacity(d.args.len());
1166                        for (i, a) in d.args.iter().enumerate() {
1167                            if i > 0 {
1168                                out.push_str(", ");
1169                            }
1170                            out.push_str(&type_name(pool, a));
1171                            let nm = format!("p{}", i + 1);
1172                            out.push(' ');
1173                            out.push_str(&nm);
1174                            names.push(nm);
1175                        }
1176                        out.push_str(") {\n");
1177                        out.push_str(&format!("    {}    super({});\n", "    ".repeat(depth), names.join(", ")));
1178                        out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
1179                        emitted_any = true;
1180                    }
1181                    break;
1182                }
1183                match &sc.super_name {
1184                    Some(n) => sup = n.clone(),
1185                    None => break,
1186                }
1187            }
1188        }
1189    }
1190
1191    // Referenced-but-undeclared inherited-ctor bridges. The block above
1192    // covers a class with NO declared ctor; this covers a class that
1193    // DECLARES ctors but is missing one that dex callers reach THROUGH
1194    // it. Dex resolves a `<init>` method-ref up the superclass chain, so
1195    // a subclass `super(2, recv, owner, name, sig, flags)` may target
1196    // `m.<init>(int,Object,Class,String,String,int)` that `m`
1197    // (FunctionReferenceImpl) does NOT declare — its super `l` does.
1198    // Java has no inherited ctors, so without a bridge on `m` every such
1199    // subclass fails ("无法将类 m中的构造器 m应用到给定类型"). Mirror each
1200    // referenced sig C lacks from the nearest ancestor declaring it, as a
1201    // thin `super(..)` delegation. Ref-gated (only sigs actually called
1202    // through C) so ordinary classes gain nothing.
1203    if !class.is_interface()
1204        && enum_consts.is_none()
1205        && class.all_methods().any(|m| &*m.name == "<init>")
1206    {
1207        if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
1208            fn arg_sig(d: &str) -> &str {
1209                let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1210                let hi = d.find(')').unwrap_or(d.len());
1211                &d[lo..hi]
1212            }
1213            let declared: jdc_core::FxHashSet<&str> = class
1214                .all_methods()
1215                .filter(|m| &*m.name == "<init>")
1216                .map(|m| arg_sig(&m.desc))
1217                .collect();
1218            let mut missing: Vec<&String> = refs
1219                .iter()
1220                .filter(|s| !declared.contains(s.as_str()))
1221                .collect();
1222            missing.sort();
1223            for sig in missing {
1224                // Nearest ancestor declaring a public/protected <init>
1225                // with this exact arg signature.
1226                let mut sup = class.super_name.clone();
1227                let mut found: Option<&PoolMethod> = None;
1228                while let Some(s) = sup {
1229                    if s == "java/lang/Object" {
1230                        break;
1231                    }
1232                    let Some(sc) = pool.get(&s) else { break };
1233                    if let Some(sm) = sc.all_methods().find(|m| {
1234                        &*m.name == "<init>"
1235                            && arg_sig(&m.desc) == sig.as_str()
1236                            && m.access & crate::access::ACC_PRIVATE == 0
1237                            && m.access
1238                                & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
1239                                != 0
1240                    }) {
1241                        found = Some(sm);
1242                        break;
1243                    }
1244                    sup = sc.super_name.clone();
1245                }
1246                let Some(sm) = found else { continue };
1247                let Some(d) = sm.parsed_desc() else { continue };
1248                let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
1249                    "public "
1250                } else {
1251                    "protected "
1252                };
1253                if emitted_any {
1254                    out.push('\n');
1255                }
1256                out.push_str(&format!("    {}", "    ".repeat(depth)));
1257                out.push_str(mods);
1258                out.push_str(&java_ident(&simple));
1259                out.push('(');
1260                let mut names = Vec::with_capacity(d.args.len());
1261                for (i, a) in d.args.iter().enumerate() {
1262                    if i > 0 {
1263                        out.push_str(", ");
1264                    }
1265                    out.push_str(&type_name(pool, a));
1266                    let nm = format!("p{}", i + 1);
1267                    out.push(' ');
1268                    out.push_str(&nm);
1269                    names.push(nm);
1270                }
1271                out.push_str(") {\n");
1272                out.push_str(&format!(
1273                    "    {}    super({});\n",
1274                    "    ".repeat(depth),
1275                    names.join(", ")
1276                ));
1277                out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
1278                emitted_any = true;
1279            }
1280        }
1281    }
1282
1283    // Framework-ancestor ctor bridge. The two blocks above mirror POOL
1284    // ancestors; both stop at a framework (off-pool) superclass. A class
1285    // whose DIRECT superclass is a framework type (in android.jar) that
1286    // declares a ctor the class lacks also needs a bridge — `super(args)`
1287    // resolves against the jar. Only the DIRECT-super-is-framework case is
1288    // safe: an intermediate pool ancestor with no matching ctor would
1289    // break the `super()` chain (that ancestor needs its own bridge, which
1290    // its own refs may not trigger). Covers weixin g3 extends
1291    // org.xml.sax.SAXException (`new g3(String)` → SAXException(String)),
1292    // the custom-exception / Parcelable family the pool walk can't reach.
1293    if !class.is_interface() && enum_consts.is_none() {
1294        let direct_fw = class
1295            .super_name
1296            .as_ref()
1297            .map(|s| s != "java/lang/Object" && pool.get(s).is_none())
1298            .unwrap_or(false);
1299        if direct_fw {
1300            if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
1301                fn arg_sig(d: &str) -> &str {
1302                    let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1303                    let hi = d.find(')').unwrap_or(d.len());
1304                    &d[lo..hi]
1305                }
1306                let declared: jdc_core::FxHashSet<&str> = class
1307                    .all_methods()
1308                    .filter(|m| &*m.name == "<init>")
1309                    .map(|m| arg_sig(&m.desc))
1310                    .collect();
1311                let mut missing: Vec<&String> = refs
1312                    .iter()
1313                    .filter(|s| !declared.contains(s.as_str()))
1314                    .collect();
1315                missing.sort();
1316                for sig in missing {
1317                    // A no-ctor class already gets Java's default
1318                    // `C(){super();}` — never bridge the empty sig for it.
1319                    if sig.is_empty() && declared.is_empty() {
1320                        continue;
1321                    }
1322                    let args = split_arg_descs(sig);
1323                    if emitted_any {
1324                        out.push('\n');
1325                    }
1326                    out.push_str(&format!("    {}", "    ".repeat(depth)));
1327                    out.push_str("public ");
1328                    out.push_str(&java_ident(&simple));
1329                    out.push('(');
1330                    let mut names = Vec::with_capacity(args.len());
1331                    for (i, a) in args.iter().enumerate() {
1332                        if i > 0 {
1333                            out.push_str(", ");
1334                        }
1335                        out.push_str(&type_name(pool, a));
1336                        let nm = format!("p{}", i + 1);
1337                        out.push(' ');
1338                        out.push_str(&nm);
1339                        names.push(nm);
1340                    }
1341                    out.push_str(") {\n");
1342                    out.push_str(&format!(
1343                        "    {}    super({});\n",
1344                        "    ".repeat(depth),
1345                        names.join(", ")
1346                    ));
1347                    out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
1348                    emitted_any = true;
1349                }
1350            }
1351        }
1352    }
1353
1354    // Static initializer. INTERFACES cannot carry a `static { }` block in
1355    // Java — their clinit only assigns constants, which static_values (or
1356    // the `= null` default) already render as field initializers; skip
1357    // the block entirely.
1358    let skip_clinit = class.is_interface();
1359    if let Some((_, clinit_body)) = enum_consts.take() {
1360        // True-enum <clinit>: the constant assignments are already gone;
1361        // render the remainder (the $VALUES array build) directly.
1362        // Skip an empty remainder (all statements were constant inits).
1363        let empty = match &clinit_body.body {
1364            Stmt::Block(v) => v.iter().all(|s| matches!(s, Stmt::Block(b) if b.is_empty())),
1365            _ => false,
1366        };
1367        if !empty {
1368            if emitted_any {
1369                out.push('\n');
1370            }
1371            out.push_str(&indent(depth + 1));
1372            out.push_str("static {\n");
1373            let p = Printer::new(ctx, &clinit_body.vt);
1374            let text = p.with_indent(depth + 2).into_string(&clinit_body.body);
1375            for line in text.lines() {
1376                if line.trim().is_empty() {
1377                    continue;
1378                }
1379                out.push_str(line);
1380                out.push('\n');
1381            }
1382            out.push_str(&indent(depth + 1));
1383            out.push_str("}\n");
1384            emitted_any = true;
1385        }
1386    } else if let Some(clinit) = (!skip_clinit)
1387        .then(|| class.all_methods().find(|m| &*m.name == "<clinit>"))
1388        .flatten()
1389    {
1390        let mark = out.len();
1391        if emitted_any {
1392            out.push('\n');
1393        }
1394        if emit_method(pool, class, ctx, clinit, depth + 1, false, out)? {
1395            emitted_any = true;
1396        } else {
1397            out.truncate(mark);
1398        }
1399    }
1400
1401    // Nested member classes (clean `$` tails only; anonymous/local/lambda
1402    // classes are emitted as their own top-level files by the driver).
1403    for nested in nested_members(pool, class, ctx) {
1404        if emitted_any {
1405            out.push('\n');
1406        }
1407        let nested_ctx = DexCtx::new(pool, nested);
1408        out.push('\n');
1409        emit_class_body(pool, nested, &nested_ctx, opts, out, depth + 1)?;
1410        emitted_any = true;
1411    }
1412
1413    out.push_str(&ind);
1414    out.push_str("}\n");
1415    Ok(())
1416}
1417
1418/// Member classes of `class` (direct children by the `$` chain / member
1419/// annotations), excluding anonymous / local / lambda shapes.
1420fn nested_members<'a>(
1421    pool: &'a DexPool,
1422    class: &'a PoolClass,
1423    ctx: &DexCtx<'_>,
1424) -> Vec<&'a PoolClass> {
1425    let mut out = Vec::new();
1426    for name in pool.children_of(&class.name) {
1427        let Some(pc) = pool.get(name) else { continue };
1428        // Only a REAL `outer$tail` name is an inline member: the
1429        // children index also carries annotation-derived outers
1430        // (EnclosingClass) with no naming relationship to the child
1431        // (obfuscated apps pair a 1-char name with a long enclosing
1432        // descriptor) — the blind slice panicked there (alipay
1433        // `a.a.a.a.c`, exposed once children_of actually returned data).
1434        let Some(rest) = name
1435            .strip_prefix(class.name.as_str())
1436            .and_then(|t| t.strip_prefix('$'))
1437        else {
1438            continue;
1439        };
1440        if rest.is_empty() || rest.starts_with('-') {
1441            continue;
1442        }
1443        if ctx.find_outer(name).as_deref() != Some(class.name.as_str()) {
1444            continue;
1445        }
1446        let tail = rest.rsplit('$').next().unwrap_or(rest);
1447        if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
1448            continue; // anonymous
1449        }
1450        if tail.starts_with(|c: char| c.is_ascii_digit()) {
1451            continue; // local
1452        }
1453        out.push(pc);
1454    }
1455    out
1456}
1457
1458// Eight parameters are all load-bearing (pool, field, static value,
1459// owner class, output, depth, staticness, interface-init requirement);
1460// bundling them into a struct would obscure each call site.
1461#[allow(clippy::too_many_arguments)]
1462fn emit_field(
1463    pool: &DexPool,
1464    f: &crate::PoolField,
1465    init: Option<&StaticValue>,
1466    f_class: &str,
1467    out: &mut String,
1468    depth: usize,
1469    is_static: bool,
1470    require_init: bool,
1471) {
1472    let ind = indent(depth);
1473    let mut line = String::new();
1474    let a = f.access;
1475    if a & ACC_PUBLIC != 0 || widen_field(f_class, &f.name) {
1476        line.push_str("public ");
1477    } else if a & ACC_PRIVATE != 0 {
1478        line.push_str("private ");
1479    } else if a & ACC_PROTECTED != 0 {
1480        line.push_str("protected ");
1481    }
1482    if is_static {
1483        line.push_str("static ");
1484    }
1485    if a & ACC_FINAL != 0 {
1486        line.push_str("final ");
1487    }
1488    if a & ACC_SYNTHETIC != 0 {
1489        line.push_str("/* synthetic */ ");
1490    }
1491    if a & ACC_TRANSIENT_HINT != 0 {
1492        line.push_str("transient ");
1493    }
1494    if a & ACC_VOLATILE_HINT != 0 {
1495        line.push_str("volatile ");
1496    }
1497    let ty = type_name(pool, &desc_type(&f.desc));
1498    line.push_str(&ty);
1499    line.push(' ');
1500    let fname = jdc_core::rename::field_display(f_class, &f.name, &f.desc).unwrap_or(&*f.name);
1501    line.push_str(&java_ident(fname));
1502    let mut rendered = None;
1503    if let Some(v) = init {
1504        rendered = render_static_value(pool, v, f_class);
1505    }
1506    if rendered.is_none() && require_init {
1507        // Interface fields MUST have an initializer in Java; the dex may
1508        // not carry a static_values entry for a compile-time-constant the
1509        // compiler folded away. Keep it compilable.
1510        let default = match desc_type(&f.desc) {
1511            JavaType::Boolean => "false",
1512            JavaType::Byte | JavaType::Short | JavaType::Char | JavaType::Int => "0",
1513            JavaType::Long => "0L",
1514            JavaType::Float => "0.0F",
1515            JavaType::Double => "0.0",
1516            _ => "null",
1517        };
1518        rendered = Some(default.to_string());
1519    }
1520    if let Some(text) = rendered {
1521        // A long field holding an `Int(i64)` static value needs the `L`
1522        // suffix: without it the literal is an int and overflows
1523        // (`long d = -6343169151696340687` failed javac).
1524        let text = if matches!(desc_type(&f.desc), JavaType::Long)
1525            && text.chars().all(|c| c.is_ascii_digit() || c == '-')
1526        {
1527            format!("{text}L")
1528        } else {
1529            text
1530        };
1531        line.push_str(" = ");
1532        line.push_str(&text);
1533    }
1534    line.push(';');
1535    out.push_str(&ind);
1536    out.push_str(&line);
1537    out.push('\n');
1538}
1539
1540// DEX field access_flags bits 0x40/0x80 are bridge/varargs for METHODS and
1541// volatile(0x40)/transient(0x80) for fields.
1542pub const ACC_VOLATILE_HINT: u32 = 0x40;
1543pub const ACC_TRANSIENT_HINT: u32 = 0x80;
1544
1545fn render_static_value(pool: &DexPool, v: &StaticValue, owner: &str) -> Option<String> {
1546    Some(match v {
1547        StaticValue::Int(i) => i.to_string(),
1548        StaticValue::Float(f) => format_float(*f as f64, true),
1549        StaticValue::Double(d) => format_float(*d, false),
1550        StaticValue::Str(s) => format!("\"{}\"", escape_string(s)),
1551        StaticValue::Type(t) => format!("{}.class", dotted(t)),
1552        StaticValue::Boolean(b) => b.to_string(),
1553        StaticValue::Null => "null".into(),
1554        StaticValue::Field(cls, name) => {
1555            let n = java_ident(name);
1556            if cls == owner {
1557                n.into_owned()
1558            } else {
1559                format!("{}.{}", print_class_name(pool, cls), n)
1560            }
1561        }
1562        StaticValue::Other => return None,
1563    })
1564}
1565
1566/// Render one method straight into the class buffer `out`. Returns
1567/// whether anything was written (skips: no descriptor, deferred to a
1568/// monitored thread). The old shape returned a per-method String that
1569/// the caller copied in — one extra full copy of every method body.
1570/// `enum_promoted`: the class rendered as a true `enum` declaration
1571/// (constants in the header), which changes what a ctor signature may
1572/// declare.
1573fn emit_method(
1574    pool: &DexPool,
1575    class: &PoolClass,
1576    ctx: &DexCtx<'_>,
1577    m: &PoolMethod,
1578    depth: usize,
1579    enum_promoted: bool,
1580    out: &mut String,
1581) -> anyhow::Result<bool> {
1582    let ind = indent(depth);
1583    let desc = m.parsed_desc();
1584
1585    // Signature.
1586    let mut sig = String::with_capacity(192);
1587    let a = m.access;
1588    if a & ACC_PUBLIC != 0 || widen_method(&class.name, &m.name, &m.desc) {
1589        sig.push_str("public ");
1590    } else if a & ACC_PRIVATE != 0 {
1591        sig.push_str("private ");
1592    } else if a & ACC_PROTECTED != 0 {
1593        sig.push_str("protected ");
1594    }
1595    let is_clinit = &*m.name == "<clinit>";
1596    let is_init = &*m.name == "<init>";
1597    if a & ACC_STATIC != 0 || is_clinit {
1598        sig.push_str("static ");
1599    }
1600    if a & ACC_FINAL != 0 {
1601        sig.push_str("final ");
1602    }
1603    // `abstract synchronized` is an illegal combination — obfuscated
1604    // builds mark abstract bridges synchronized (WhatsApp
1605    // SQLiteOpenHelper).
1606    if a & (ACC_SYNCHRONIZED | ACC_DECLARED_SYNCHRONIZED) != 0 && a & ACC_ABSTRACT == 0 {
1607        sig.push_str("synchronized ");
1608    }
1609    if a & ACC_NATIVE != 0 {
1610        sig.push_str("native ");
1611    }
1612    if a & ACC_ABSTRACT != 0 {
1613        sig.push_str("abstract ");
1614    }
1615    if a & ACC_SYNTHETIC != 0 {
1616        sig.push_str("/* synthetic */ ");
1617    }
1618
1619    // Body (needed for parameter names even for abstract methods).
1620    let mut body = decompile_method(pool, class, m).ok().flatten();
1621    // An interface method WITH a body is a `default` method (JLS 9.4.3)
1622    // unless static/private — dex carries no `default` flag, so the
1623    // plain form rendered an abstract signature with a body and javac
1624    // rejected every one ("接口抽象方法不能带有主体", weixin 1138). A
1625    // default REQUIRES a body: only push it once the body is confirmed
1626    // (a failed decompile renders a body-less declaration).
1627    if class.is_interface()
1628        && a & (ACC_STATIC | ACC_ABSTRACT | ACC_NATIVE | ACC_PRIVATE | ACC_ANNOTATION) == 0
1629        && body.is_some()
1630    {
1631        sig.insert_str(0, "default ");
1632    }
1633    let param_names: Vec<String> = body
1634        .as_ref()
1635        .map(|b| {
1636            let mut ps: Vec<(u16, String)> =
1637                b.vt.vars
1638                    .iter()
1639                    .filter(|v| v.is_param && v.name != "this")
1640                    .map(|v| (v.slot, v.name.clone()))
1641                    .collect();
1642            ps.sort_by_key(|(s, _)| *s);
1643            ps.into_iter().map(|(_, n)| n).collect()
1644        })
1645        .unwrap_or_else(|| {
1646            (0..desc.as_ref().map(|d| d.args.len()).unwrap_or(0))
1647                .map(|i| format!("p{}", i + 1))
1648                .collect()
1649        });
1650
1651    // Non-static member-inner ctor: the synthetic outer instance rides
1652    // as args[0] (typed as the direct enclosing class). Every emission
1653    // site already passes it implicitly — qualified `outer.new Inner(..)`,
1654    // `this.new Inner(..)` from inside the outer, `super(..)` after
1655    // skip_outer_arg — so the signature drops the param and the body's
1656    // references become `Outer.this`. Without this, every construction
1657    // site fails javac arity ("无法将类…构造器…应用到给定类型" — the
1658    // guava androidx inner-class families, d8 capture lambdas).
1659    let mut inner_arg0 = 0usize;
1660    if is_init {
1661        let tail = class.name.rsplit('$').next().unwrap_or("");
1662        let digit_simple = !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit());
1663        if let Some(d) = desc.as_ref() {
1664            if let Some(JavaType::Object(outer)) = d.args.first() {
1665                // The direct enclosing class: nesting annotation when
1666                // present, else the `$`-chain parent (find_outer_name —
1667                // d8 lambdas are `Outer$$ExternalSyntheticLambdaN`, where
1668                // a plain rsplit leaves a trailing `$`). It must agree
1669                // with the this$0 type for the rewrite to fire.
1670                let enclosing: Option<String> = class
1671                    .nesting
1672                    .enclosing_class
1673                    .clone()
1674                    .or_else(|| crate::find_outer_name(pool, &class.name));
1675                let direct = enclosing.as_deref() == Some(outer.as_ref());
1676                if direct && !digit_simple && ctx.class_has_this0(&class.name) {
1677                    let param0 = body.as_ref().and_then(|b| {
1678                        b.vt.vars
1679                            .iter()
1680                            .find(|v| v.is_param && v.name != "this")
1681                            .map(|v| v.id)
1682                    });
1683                    let written = match (&body, param0) {
1684                        (Some(b), Some(p0)) => crate::passes::local_is_written(&b.body, p0),
1685                        _ => false,
1686                    };
1687                    if let (Some(p0), false) = (param0, written) {
1688                        if let Some(b) = body.as_mut() {
1689                            // this()-delegations to the SAME class drop the
1690                            // outer arg (all of its ctors share the strip);
1691                            // a super target joins when it is an inner of
1692                            // the same enclosing family.
1693                            let mut eligible: Vec<String> = vec![class.name.clone()];
1694                            if let Some(sup) = &class.super_name {
1695                                let sup_enclosing =
1696                                    crate::find_outer_name(pool, sup);
1697                                if sup_enclosing.as_deref() == Some(outer.as_ref())
1698                                    && ctx.class_has_this0(sup)
1699                                {
1700                                    eligible.push(sup.clone());
1701                                }
1702                            }
1703                            let renamed = crate::apply_class_rename(outer);
1704                            let display = dotted(renamed.as_ref());
1705                            let ty =
1706                                jdc_core::ir::expr::TypeRef::J(JavaType::Object(outer.clone()));
1707                            crate::passes::rewrite_inner_ctor_outer_param(
1708                                &mut b.body,
1709                                p0,
1710                                &display,
1711                                &ty,
1712                                &eligible,
1713                            );
1714                            inner_arg0 = 1;
1715                        }
1716                    }
1717                }
1718            }
1719        }
1720    }
1721
1722    if is_clinit {
1723        // `static { ... }` — the caller strips the method name/params.
1724    } else {
1725        let Some(d) = &desc else { return Ok(false) };
1726        if is_init {
1727            // The ctor name must equal the DECLARED class name of its
1728            // file: flat `$` at depth 0 (own file), own segment when
1729            // inlined in the parent at depth > 0. Case-renamed classes
1730            // use their display name.
1731            let dname = crate::apply_class_rename(&class.name);
1732            let (_, simple) = split_name(&dname);
1733            // emit_method's depth is the METHOD indent = class depth + 1:
1734            // own-file classes (depth 0 header → method depth 1) need the
1735            // flat `$` ctor name; inline nested members use their segment.
1736            let base = if depth <= 1 {
1737                simple.to_string()
1738            } else {
1739                // R8 names can END in `$` (`ThreadMsg$$$`): the last
1740                // `$`-segment is empty — keep the whole simple name.
1741                let seg = simple.rsplit('$').next().unwrap_or(&simple);
1742                if seg.is_empty() {
1743                    simple.to_string()
1744                } else {
1745                    seg.to_string()
1746                }
1747            };
1748            let name = if is_restricted_type_name(&base) {
1749                format!("_{base}")
1750            } else {
1751                base
1752            };
1753            sig.push_str(&java_ident(&name));
1754        } else {
1755            sig.push_str(&type_name(pool, &d.ret));
1756            sig.push(' ');
1757            let mname =
1758                jdc_core::rename::field_display(&class.name, &m.name, &m.desc).unwrap_or(&*m.name);
1759            sig.push_str(&java_ident(mname));
1760        }
1761        sig.push('(');
1762        // A promoted enum ctor's dex descriptor carries the compiler-
1763        // synthesized `(String name, int ordinal)` prefix — JLS forbids
1764        // declaring those (they are implicit in the `A(args)` constant
1765        // declarations the promotion emits, and strip_enum_ctor_super
1766        // already removed the `super(name, ordinal, ..)` delegation).
1767        // Skip the pair in the signature, or every constant declaration
1768        // fails javac arity ("无法将枚举…构造器…应用到给定类型"). Only
1769        // when the body never reads the two params — code that genuinely
1770        // uses them keeps the declared form.
1771        let mut arg0 = 0;
1772        if enum_promoted
1773            && is_init
1774            && matches!(d.args.first(), Some(JavaType::Object(s)) if s.as_ref() == "java/lang/String")
1775            && matches!(d.args.get(1), Some(JavaType::Int))
1776        {
1777            // The Kotlin default-arg bridge ctor reads name/ordinal in
1778            // its `this(str, p2, ..)` delegation only — the LEADING pair
1779            // of a this()-delegation drops together with the params, so
1780            // those reads do not block the strip (the constants' extra
1781            // args match the bridge's user params, not the 2-param user
1782            // ctor).
1783            let synthetic: Vec<u32> = body
1784                .as_ref()
1785                .map(|b| {
1786                    b.vt
1787                        .vars
1788                        .iter()
1789                        .filter(|v| v.is_param && v.name != "this")
1790                        .take(2)
1791                        .map(|v| v.id)
1792                        .collect()
1793                })
1794                .unwrap_or_default();
1795            // The delegation-leading extension only applies to the
1796            // Kotlin default-arg BRIDGE: its descriptor ends with
1797            // kotlin/jvm/internal/DefaultConstructorMarker. A REAL user
1798            // ctor whose first two params are (String, int) and merely
1799            // forwards them must keep its signature (weixin +2.9k when
1800            // ungated).
1801            let is_bridge = matches!(
1802                d.args.last(),
1803                Some(JavaType::Object(m)) if m.as_ref() == "kotlin/jvm/internal/DefaultConstructorMarker"
1804            );
1805            let mut lead = [0usize, 0usize];
1806            let refs_ok = body.as_ref().is_some_and(|b| {
1807                let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
1808                let mut c = b.body.clone();
1809                let cls_name = class.name.as_str();
1810                crate::passes::walk_stmt_exprs(&mut c, &mut |e| {
1811                    if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
1812                        if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
1813                            for (k, a) in args.iter().enumerate() {
1814                                if let Expr::Local { var: v, .. } = a {
1815                                    if let Some(pi) = synthetic.iter().position(|p| p == v) {
1816                                        if k == pi && k < 2 {
1817                                            lead[pi] += 1;
1818                                        }
1819                                    }
1820                                }
1821                            }
1822                        }
1823                    }
1824                });
1825                synthetic.iter().enumerate().all(|(pi, id)| {
1826                    let total = uses.get(id).copied().unwrap_or(0);
1827                    if is_bridge {
1828                        total == lead[pi]
1829                    } else {
1830                        total == 0
1831                    }
1832                })
1833            });
1834            if refs_ok {
1835                arg0 = 2;
1836                // Drop the leading (name, ordinal) args of the this()
1837                // delegations.
1838                if let (Some(b), true) = (body.as_mut(), is_bridge) {
1839                    let cls_name = class.name.as_str();
1840                    let syn = synthetic;
1841                    crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
1842                        if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
1843                            if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
1844                                let mut drop_n = 0;
1845                                for a in args.iter().take(2) {
1846                                    if let Expr::Local { var: v, .. } = a {
1847                                        if syn.contains(v) && drop_n == v - syn[0] {
1848                                            drop_n += 1;
1849                                            continue;
1850                                        }
1851                                    }
1852                                    break;
1853                                }
1854                                for _ in 0..drop_n {
1855                                    args.remove(0);
1856                                }
1857                            }
1858                        }
1859                    });
1860                }
1861            }
1862        }
1863        let n = d.args.len();
1864        let varargs = a & ACC_VARARGS != 0 && n > 0;
1865        let arg0 = arg0 + inner_arg0;
1866        for (i, arg) in d.args.iter().enumerate().skip(arg0) {
1867            if i > arg0 {
1868                sig.push_str(", ");
1869            }
1870            let name = param_names
1871                .get(i)
1872                .cloned()
1873                .unwrap_or_else(|| format!("p{}", i));
1874            if varargs && i + 1 == n {
1875                if let JavaType::Array(inner) = arg {
1876                    sig.push_str(&type_name(pool, inner));
1877                    sig.push_str("...");
1878                } else {
1879                    sig.push_str(&type_name(pool, arg));
1880                }
1881            } else {
1882                sig.push_str(&type_name(pool, arg));
1883            }
1884            sig.push(' ');
1885            // Param names come from dex debug info — obfuscated apps
1886            // name them `_` (reserved since Java 9) or after keywords.
1887            sig.push_str(&java_ident(&name));
1888        }
1889        sig.push(')');
1890    }
1891
1892    if is_clinit {
1893        let Some(b) = body else { return Ok(false) };
1894        // Direct render: the printer starts at the method's ABSOLUTE
1895        // indent and appends into the same buffer that carries the
1896        // header — no intermediate body string, no per-line re-indent
1897        // pass (two full copies of every method body saved).
1898        out.push_str(&ind);
1899        out.push_str("static {\n");
1900        let hdr = out.len();
1901        let printer = Printer::new(ctx, &b.vt)
1902            .with_indent(depth + 1)
1903            .with_output(std::mem::take(out));
1904        let t_print = std::time::Instant::now();
1905        let mut rendered = printer.into_string(&b.body);
1906        crate::method::phase_hit(3, t_print);
1907        if rendered[hdr..].trim().is_empty() {
1908            rendered.truncate(hdr);
1909        }
1910        rendered.push_str(&ind);
1911        rendered.push_str("}\n");
1912        *out = rendered;
1913        return Ok(true);
1914    }
1915    if a & (ACC_ABSTRACT | ACC_NATIVE) != 0 || body.is_none() {
1916        out.push_str(&ind);
1917        out.push_str(&sig);
1918        out.push_str(";\n");
1919        return Ok(true);
1920    }
1921    let Some(b) = body else { return Ok(false) };
1922
1923    // Direct render at the absolute indent level (see the clinit path).
1924    let mut printer = Printer::new(ctx, &b.vt).with_indent(depth + 1);
1925    match &b.desc.ret {
1926        JavaType::Boolean => {
1927            printer = printer.with_ret_bool(true);
1928        }
1929        JavaType::Char => {
1930            printer = printer.with_ret_char(true);
1931        }
1932        JavaType::Byte => {
1933            printer = printer.with_ret_narrow(true, false);
1934        }
1935        JavaType::Short => {
1936            printer = printer.with_ret_narrow(false, true);
1937        }
1938        _ => {}
1939    }
1940    out.push_str(&ind);
1941    out.push_str(&sig);
1942    out.push_str(" {\n");
1943    let hdr = out.len();
1944    let printer = printer.with_output(std::mem::take(out));
1945    let t_print = std::time::Instant::now();
1946    let mut rendered = printer.into_string(&b.body);
1947    crate::method::phase_hit(3, t_print);
1948    if rendered[hdr..].trim().is_empty() {
1949        rendered.truncate(hdr);
1950    }
1951    rendered.push_str(&ind);
1952    rendered.push_str("}\n");
1953    *out = rendered;
1954    Ok(true)
1955}
1956
1957// ---------------------------------------------------------------------------
1958// Naming helpers
1959// ---------------------------------------------------------------------------
1960
1961fn indent(depth: usize) -> String {
1962    "    ".repeat(depth)
1963}
1964
1965/// `$`-separated nesting rendered with dots — but ONLY when every
1966/// segment is a clean Java identifier (a genuine member class chain).
1967/// Anonymous (`Outer$1`), Kotlin synthetic (`Version$bigInteger$2`,
1968/// `...$$inlined$collect$1`) and local-class tails are NOT member
1969/// classes Java can name; the whole name stays flat with `$` (ddc emits
1970/// them as their own top-level files).
1971/// Kotlin emits method names like `invokeSuspend$lambda-0` — `-` (and
1972/// any other non-identifier character) is not legal Java. Deterministic
1973/// mapping, applied identically at declaration and call sites.
1974pub(crate) fn java_ident(name: &str) -> std::borrow::Cow<'_, str> {
1975    // env::var_os is an environ lock+scan — java_ident runs per IDENTIFIER
1976    // (millions per APK), where it profiled as __NSGetEnviron.
1977    static DBG_IDENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1978    if *DBG_IDENT.get_or_init(|| std::env::var_os("DDC_DBG_IDENT").is_some()) {
1979        eprintln!("[ident] {name:?}");
1980    }
1981    let clean = name
1982        .chars()
1983        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
1984    // Kotlin names fields/methods `default` (Companion.default) — no Java
1985    // program can declare or reference a keyword; the identical mapping
1986    // lives in jdc-core's call-site sanitizer.
1987    let keyword = matches!(
1988        name,
1989        "abstract"
1990            | "assert"
1991            | "boolean"
1992            | "break"
1993            | "byte"
1994            | "case"
1995            | "catch"
1996            | "char"
1997            | "class"
1998            | "const"
1999            | "continue"
2000            | "default"
2001            | "do"
2002            | "double"
2003            | "else"
2004            | "enum"
2005            | "extends"
2006            | "final"
2007            | "finally"
2008            | "float"
2009            | "for"
2010            | "goto"
2011            | "if"
2012            | "implements"
2013            | "import"
2014            | "instanceof"
2015            | "int"
2016            | "interface"
2017            | "long"
2018            | "native"
2019            | "new"
2020            | "package"
2021            | "private"
2022            | "protected"
2023            | "public"
2024            | "return"
2025            | "short"
2026            | "static"
2027            | "strictfp"
2028            | "super"
2029            | "switch"
2030            | "synchronized"
2031            | "this"
2032            | "throw"
2033            | "throws"
2034            | "transient"
2035            | "try"
2036            | "void"
2037            | "volatile"
2038            | "while"
2039            | "true"
2040            | "false"
2041            | "null"
2042            // `_` is a reserved identifier since Java 9 (Alipay's
2043            // instant-run fields are named `_`) — the `_<name>` mapping
2044            // turns it into `__`, matching jdc-core's call sites.
2045            | "_"
2046    );
2047    // A simple name may not START with a digit either (WhatsApp nests
2048    // `X/0Xx`): the declaration site and every reference (jdc-core's
2049    // sanitize_source_name) prefix the same underscore.
2050    let digit_start = name.chars().next().is_some_and(|c| c.is_ascii_digit());
2051    if clean && !keyword && !digit_start {
2052        std::borrow::Cow::Borrowed(name)
2053    } else if keyword || digit_start {
2054        std::borrow::Cow::Owned(format!("_{name}"))
2055    } else {
2056        // Non-ASCII single chars (Alipay names a field `支`) map to a
2057        // lone `_` — itself reserved since Java 9. Escape it.
2058        let mapped: String = name
2059            .chars()
2060            .map(|c| {
2061                if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
2062                    c
2063                } else {
2064                    '_'
2065                }
2066            })
2067            .collect();
2068        if mapped == "_" {
2069            std::borrow::Cow::Owned("__".to_string())
2070        } else {
2071            std::borrow::Cow::Owned(mapped)
2072        }
2073    }
2074}
2075
2076fn split_name(internal: &str) -> (String, String) {
2077    match internal.rfind('/') {
2078        Some(i) => (internal[..i].to_string(), internal[i + 1..].to_string()),
2079        None => (String::new(), internal.to_string()),
2080    }
2081}
2082
2083/// Dotted source form of an internal name.
2084pub fn dotted(internal: &str) -> String {
2085    let mut out = internal.replace('/', ".");
2086    // `$` → `.` only when the following segment can start a Java
2087    // identifier (anonymous/synthetic tails stay `$`).
2088    let mut i = 0;
2089    while let Some(p) = out[i..].find('$') {
2090        let at = i + p;
2091        // A LEADING `$` (ProGuard keeps `$Gson$Types`) is part of the
2092        // source name — dotting it produced a leading `.Gson.Types`.
2093        let head_ok = at > 0
2094            && out[..at]
2095                .chars()
2096                .next_back()
2097                .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
2098        let tail_ok = out[at + 1..]
2099            .chars()
2100            .next()
2101            .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
2102        if tail_ok && head_ok {
2103            out.replace_range(at..at + 1, ".");
2104            i = at + 1;
2105        } else {
2106            i = at + 1;
2107        }
2108    }
2109    // Every `.`-segment must start an identifier: WhatsApp's `X/0Hl`
2110    // reached `extends` with the digit start unmapped.
2111    sanitize_fq(&out)
2112}
2113
2114fn join_dotted(pool: &DexPool, names: &[String]) -> String {
2115    names
2116        .iter()
2117        .map(|n| print_class_name(pool, n))
2118        .collect::<Vec<_>>()
2119        .join(", ")
2120}
2121
2122/// A printable type name (arrays render with `[]` suffixes). Nested class
2123/// names dot their `$` when the outer chain is known to the pool.
2124/// Split a concatenated method-descriptor ARGUMENT string
2125/// (`"Ljava/lang/String;I[Ljava/lang/Object;"`) into per-parameter
2126/// `JavaType`s. Used by the framework-ancestor ctor bridge, where the
2127/// referenced sig comes from the method-id table (no pool ancestor
2128/// method to read a parsed descriptor from).
2129fn split_arg_descs(sig: &str) -> Vec<JavaType> {
2130    let mut out = Vec::new();
2131    let b = sig.as_bytes();
2132    let mut i = 0usize;
2133    while i < b.len() {
2134        let start = i;
2135        while i < b.len() && b[i] == b'[' {
2136            i += 1;
2137        }
2138        if i < b.len() {
2139            if b[i] == b'L' {
2140                while i < b.len() && b[i] != b';' {
2141                    i += 1;
2142                }
2143                if i < b.len() {
2144                    i += 1; // consume ';'
2145                }
2146            } else {
2147                i += 1; // primitive single char
2148            }
2149        }
2150        if i > start {
2151            out.push(desc_type(&sig[start..i]));
2152        } else {
2153            break; // malformed: avoid an infinite loop
2154        }
2155    }
2156    out
2157}
2158
2159pub fn type_name(pool: &DexPool, t: &JavaType) -> String {
2160    match t {
2161        JavaType::Void => "void".into(),
2162        JavaType::Object(n) => print_class_name(pool, n),
2163        JavaType::Array(inner) => format!("{}[]", type_name(pool, inner)),
2164        other => other.to_java(false),
2165    }
2166}
2167
2168/// `com/foo/Outer$Inner` → `com.foo.Outer.Inner` — each `$` dots only when
2169/// its left side names a known class (literal `$` top-level names survive).
2170// ---------------------------------------------------------------------------
2171// The per-class import map (JLS 6.4.2 obscuring repair).
2172//
2173// A referenced FQN whose FIRST segment equals an in-scope class simple
2174// name binds the qualifier to the CLASS, not the package — `class j3
2175// implements j3.g` is cyclic inheritance and collapses javac's
2176// attribution for the whole package (the Object-cascade root, weixin
2177// 84 files + contagion). The repair: emit `import j3.g;` and render
2178// the SIMPLE name in every type position. The set is populated per
2179// class from a metadata pre-scan (supertypes, field types, method
2180// signature descriptors) and read by print_class_name and the
2181// DexCtx::obscured_simple implementation (which jdc-core's shorten /
2182// java_type_name consult).
2183struct ObscureState {
2184    /// The ROOT class being rendered (its simple name is the obscuring
2185    /// in-scope name).
2186    class: String,
2187    /// Metadata pre-scan results: internal → simple render.
2188    map: jdc_core::FxHashMap<String, String>,
2189    /// Expression-level obscured refs DISCOVERED during the body render
2190    /// (their imports emit at assembly time).
2191    recorded: jdc_core::FxHashSet<String>,
2192    /// Simple names of the CURRENT PACKAGE's own classes: importing a
2193    /// name that collides would SHADOW every same-package use of it in
2194    /// this file (JLS 7.5.1) — those refs stay qualified (obscured,
2195    /// erroring) rather than corrupt.
2196    blocked: jdc_core::FxHashSet<String>,
2197}
2198
2199thread_local! {
2200    static OBSCURE: std::cell::RefCell<Option<ObscureState>> =
2201        const { std::cell::RefCell::new(None) };
2202}
2203
2204/// Install the per-class render state (call at class render entry).
2205pub(crate) fn set_obscured_state(
2206    class: String,
2207    map: jdc_core::FxHashMap<String, String>,
2208    blocked: jdc_core::FxHashSet<String>,
2209) {
2210    OBSCURE.with(|m| {
2211        *m.borrow_mut() = Some(ObscureState {
2212            class,
2213            map,
2214            recorded: jdc_core::FxHashSet::default(),
2215            blocked,
2216        });
2217    });
2218}
2219
2220/// Simple names this class's file renders through the import layer
2221/// (metadata map values + dynamically recorded nested tails). A method
2222/// local with one of these names captures the imported simple name in
2223/// expression position (locals beat single-type imports — `h.e` binds
2224/// a local int `h`, 无法取消引用int); deshadow_locals renames them.
2225/// Empty when no render state is installed (progressive paths).
2226pub(crate) fn obscured_simples_snapshot() -> jdc_core::FxHashSet<String> {
2227    OBSCURE.with(|m| {
2228        let mut out = jdc_core::FxHashSet::default();
2229        let Ok(st) = m.try_borrow() else { return out };
2230        let Some(st) = st.as_ref() else { return out };
2231        for simple in st.map.values() {
2232            out.insert(simple.clone());
2233        }
2234        for internal in &st.recorded {
2235            if let Some(tail) = internal.rsplit(['/', '$']).next() {
2236                if !tail.is_empty() {
2237                    out.insert(tail.to_string());
2238                }
2239            }
2240        }
2241        out
2242    })
2243}
2244
2245/// Take the recorded expression-level refs and clear the state.
2246pub(crate) fn take_recorded_and_clear() -> jdc_core::FxHashSet<String> {
2247    OBSCURE.with(|m| {
2248        m.borrow_mut()
2249            .take()
2250            .map(|st| st.recorded)
2251            .unwrap_or_default()
2252    })
2253}
2254
2255// ---- cross-package access widening ----------------------------------
2256// ART tolerates cross-package access to non-public classes/members that
2257// R8/d8 preserved; Java source cannot express it ("是在不可访问的类或
2258// 接口中定义的", "k在kotlinx.coroutines中不是公共的", "unknownFields 在
2259// l6 中是 protected"). The census scan records every cross-package
2260// reference; the installer keeps the non-public targets and the
2261// declaration renders widen them to public.
2262
2263struct WidenState {
2264    classes: jdc_core::FxHashSet<String>,
2265    methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>>,
2266    fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>,
2267}
2268
2269static WIDEN: std::sync::OnceLock<WidenState> = std::sync::OnceLock::new();
2270
2271pub(crate) fn install_access_widening(pool: &crate::DexPool) {
2272    let t0 = std::time::Instant::now();
2273    let raw = crate::refscan::access_widening_scan(pool.dexes());
2274    let mut classes: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
2275    for c in &raw.classes {
2276        if let Some(pc) = pool.get_if_materialized(c) {
2277            if pc.access & crate::access::ACC_PUBLIC == 0 {
2278                classes.insert(c.clone());
2279            }
2280        }
2281    }
2282    let mut methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>> =
2283        jdc_core::FxHashMap::default();
2284    for (owner, set) in &raw.methods {
2285        let Some(pc) = pool.get_if_materialized(owner) else {
2286            continue;
2287        };
2288        let is_enum = pc.access & crate::access::ACC_ENUM != 0;
2289        let mut keep: jdc_core::FxHashSet<(String, String)> =
2290            jdc_core::FxHashSet::default();
2291        for (name, desc) in set {
2292            // Enum ctors are source-level private-only; clinit is never
2293            // referenced. Widening either is a javac "modifier not
2294            // allowed here".
2295            if name == "<clinit>" || (is_enum && name == "<init>") {
2296                continue;
2297            }
2298            let nonpub = pc
2299                .all_methods()
2300                .any(|m| &*m.name == name.as_str() && &*m.desc == desc.as_str()
2301                    && m.access & crate::access::ACC_PUBLIC == 0);
2302            if nonpub {
2303                keep.insert((name.clone(), desc.clone()));
2304            }
2305        }
2306        if !keep.is_empty() {
2307            methods.insert(owner.clone(), keep);
2308        }
2309    }
2310    // Override-closure DOWN the hierarchy: a widened (public) super
2311    // method whose subclass override stays protected/package is
2312    // "无法覆盖...更低的访问权限" (lark +823). Every transitive
2313    // subclass declaring the same (name, desc) non-public widens too.
2314    {
2315        let mut subs: jdc_core::FxHashMap<String, Vec<String>> =
2316            jdc_core::FxHashMap::default();
2317        for name in &pool.order {
2318            let Some(pc) = pool.get_if_materialized(name) else {
2319                continue;
2320            };
2321            if let Some(sup) = &pc.super_name {
2322                subs.entry(sup.clone()).or_default().push(name.clone());
2323            }
2324            for i in &pc.interfaces {
2325                subs.entry(i.clone()).or_default().push(name.clone());
2326            }
2327        }
2328        let seeds: Vec<(String, String, String)> = methods
2329            .iter()
2330            .flat_map(|(o, set)| {
2331                set.iter().map(|(n, d)| (o.clone(), n.clone(), d.clone()))
2332            })
2333            .collect();
2334        for (owner, mname, mdesc) in seeds {
2335            let mut queue: Vec<String> = match subs.get(&owner) {
2336                Some(v) => v.clone(),
2337                None => continue,
2338            };
2339            let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
2340            while let Some(c) = queue.pop() {
2341                if !seen.insert(c.clone()) {
2342                    continue;
2343                }
2344                let Some(pc) = pool.get_if_materialized(&c) else {
2345                    continue;
2346                };
2347                let declares_nonpub = pc.all_methods().any(|m| {
2348                    &*m.name == mname.as_str()
2349                        && &*m.desc == mdesc.as_str()
2350                        && m.access & crate::access::ACC_PUBLIC == 0
2351                });
2352                if declares_nonpub {
2353                    methods
2354                        .entry(c.clone())
2355                        .or_default()
2356                        .insert((mname.clone(), mdesc.clone()));
2357                }
2358                if let Some(v) = subs.get(&c) {
2359                    queue.extend(v.iter().cloned());
2360                }
2361            }
2362        }
2363    }
2364    let mut fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
2365        jdc_core::FxHashMap::default();
2366    for (owner, set) in &raw.fields {
2367        let Some(pc) = pool.get_if_materialized(owner) else {
2368            continue;
2369        };
2370        let keep: jdc_core::FxHashSet<String> = set
2371            .iter()
2372            .filter(|name| {
2373                pc.static_fields
2374                    .iter()
2375                    .chain(pc.instance_fields.iter())
2376                    .any(|f| f.name == name.as_str()
2377                        && f.access & crate::access::ACC_PUBLIC == 0)
2378            })
2379            .cloned()
2380            .collect();
2381        if !keep.is_empty() {
2382            fields.insert(owner.clone(), keep);
2383        }
2384    }
2385    if std::env::var("DDC_STATS").is_ok() {
2386        eprintln!(
2387            "[renames] access widening: classes={} method-owners={} field-owners={} scan={:?}",
2388            classes.len(),
2389            methods.len(),
2390            fields.len(),
2391            t0.elapsed()
2392        );
2393    }
2394    let _ = WIDEN.set(WidenState { classes, methods, fields });
2395}
2396
2397#[inline]
2398pub(crate) fn widen_class(internal: &str) -> bool {
2399    WIDEN.get().is_some_and(|w| w.classes.contains(internal))
2400}
2401
2402#[inline]
2403pub(crate) fn widen_method(owner: &str, name: &str, desc: &str) -> bool {
2404    WIDEN.get().is_some_and(|w| {
2405        w.methods
2406            .get(owner)
2407            .is_some_and(|s| s.contains(&(name.to_string(), desc.to_string())))
2408    })
2409}
2410
2411#[inline]
2412pub(crate) fn widen_field(owner: &str, name: &str) -> bool {
2413    WIDEN.get().is_some_and(|w| {
2414        w.fields.get(owner).is_some_and(|s| s.contains(name))
2415    })
2416}
2417
2418/// Render-time lookup: the metadata map first; otherwise an
2419/// expression-level ref whose first segment equals the root class's
2420/// simple name — record it (its import emits at assembly) and render
2421/// the simple name NOW.
2422pub(crate) fn obscured_render_pub(internal: &str) -> Option<String> {
2423    OBSCURE.with(|m| {
2424        let mut st = m.borrow_mut();
2425        let st = st.as_mut()?;
2426        if let Some(simple) = st.map.get(internal) {
2427            return Some(simple.clone());
2428        }
2429        let first = internal.split('/').next().unwrap_or("");
2430        let own = st.class.rsplit('/').next().unwrap_or("");
2431        if first == own && internal.split('/').count() >= 2 {
2432            // A NESTED internal's in-scope simple name is its `$` tail
2433            // (`x/a$b` imports/renders as `b`) — the slash-tail left the
2434            // `$` in the render (`t2$a` flat against a nested emission).
2435            let simple = internal
2436                .rsplit(['/', '$'])
2437                .next()
2438                .unwrap_or("")
2439                .to_string();
2440            if !simple.is_empty() && !st.blocked.contains(&simple) {
2441                st.recorded.insert(internal.to_string());
2442                return Some(simple);
2443            }
2444        }
2445        None
2446    })
2447}
2448
2449/// The pre-scan: internal names referenced by the class's metadata
2450/// (supertypes, field types, method descriptors) whose first segment
2451/// equals the class's own simple name and which exist in the pool —
2452/// these get imports and simple-name renders.
2453fn compute_obscured_renders(pool: &DexPool, class: &PoolClass) -> jdc_core::FxHashMap<String, String> {
2454    let own_simple = class.name.rsplit('/').next().unwrap_or("");
2455    if own_simple.is_empty() {
2456        return jdc_core::FxHashMap::default();
2457    }
2458    let mut refs: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
2459    for sup in class.interfaces.iter() {
2460        refs.insert(sup.clone());
2461    }
2462    if let Some(sup) = &class.super_name {
2463        refs.insert(sup.clone());
2464    }
2465    for f in class.static_fields.iter().chain(class.instance_fields.iter()) {
2466        if let JavaType::Object(n) = crate::desc_type(&f.desc) {
2467            refs.insert(n.to_string());
2468        }
2469    }
2470    for m in class.all_methods() {
2471        if let Some(d) = m.parsed_desc() {
2472            for a in d.args.iter() {
2473                if let JavaType::Object(n) = a {
2474                    refs.insert(n.to_string());
2475                }
2476            }
2477            if let JavaType::Object(n) = &d.ret {
2478                refs.insert(n.to_string());
2479            }
2480        }
2481    }
2482    let mut out: jdc_core::FxHashMap<String, String> = jdc_core::FxHashMap::default();
2483    for r in refs {
2484        // First segment == the class's own simple name: the qualified
2485        // render would be obscured. The name must exist in the pool for
2486        // the import to resolve. The KEY stays internal (renders look
2487        // refs up by internal name), but the import line and the
2488        // simple name use the RENAMED display — a collision-renamed
2489        // class renders under its display name and an import of the
2490        // internal name does not resolve.
2491        let first = r.split('/').next().unwrap_or("");
2492        if first == own_simple && r.split('/').count() >= 2 && pool.get(&r).is_some() {
2493            // Own-family refs (the class itself, its nested members)
2494            // render through member scope — an import would be redundant
2495            // (self-import) or shadow a same-package sibling named like
2496            // the tail (`import t.t2.a` hijacks every bare `a` that
2497            // meant sibling class t.a — lark t/t2).
2498            if r == class.name || r.starts_with(&format!("{}$", class.name)) {
2499                continue;
2500            }
2501            let display = crate::apply_class_rename(&r);
2502            let simple = display
2503                .rsplit(['/', '$'])
2504                .next()
2505                .unwrap_or("")
2506                .to_string();
2507            if !simple.is_empty() {
2508                out.insert(r.clone(), simple);
2509            }
2510        }
2511    }
2512    out
2513}
2514
2515pub fn print_class_name(pool: &DexPool, internal: &str) -> String {
2516    if let Some(simple) = obscured_render_pub(internal) {
2517        return sanitize_ref(&simple);
2518    }
2519    let cow = crate::apply_class_rename(internal);
2520    let internal: &str = &cow;
2521    // Flat EMISSION UNITS: a digit-tail member (anonymous / d8-lambda
2522    // shape, `Outer$lruCache$1`) is emitted as its own top-level file
2523    // whose simple name keeps every `$` — the `$` boundaries inside
2524    // that unit are part of the NAME, not nesting. References must use
2525    // the flat unit name; the generic loop below dotted the first
2526    // boundary (`LruCacheKt.lruCache$1`) against the declaration
2527    // `class LruCacheKt$lruCache$1` — every use of the type failed and
2528    // javac's attribution for the whole file collapsed (3627 pure-
2529    // cascade files on weibo).
2530    if internal.contains('$') && pool.get(internal).is_some() {
2531        if let Some(root) = emission_root(pool, internal) {
2532            if root.contains('$') {
2533                let below = &internal[root.len()..];
2534                if below.is_empty() {
2535                    // The unit itself: the flat name IS the reference.
2536                    return sanitize_ref(&internal.replace('/', "."));
2537                }
2538                let segs: Vec<&str> = below[1..].split('$').collect();
2539                if segs.iter().any(|s| s.is_empty()) {
2540                    // R8 tails like `ThreadMsg$$$` cannot be dotted at
2541                    // all — keep the whole name flat.
2542                    return sanitize_ref(&internal.replace('/', "."));
2543                }
2544                let mut out = root.replace('/', ".");
2545                for s in segs {
2546                    out.push('.');
2547                    out.push_str(s);
2548                }
2549                return sanitize_ref(&out);
2550            }
2551        }
2552    }
2553    let mut out = String::new();
2554    // `known` must test the ACCUMULATED internal prefix, not the bare
2555    // inter-`$` segment: the per-segment shape checked `pool.get("a")`
2556    // for the second level of `s5/o$a$b`, missed, and rendered the
2557    // undeclarable reference `s5.o.a$b` (找不到符号) for every nested-
2558    // nested type.
2559    let mut off = 0usize;
2560    loop {
2561        let rest = &internal[off..];
2562        match rest.find('$') {
2563            Some(i) => {
2564                let seg = &rest[..i];
2565                let prefix = &internal[..off + i];
2566                let known = pool.get(prefix).is_some()
2567                    || jdc_core::rename::is_renamed_display(prefix)
2568                    // The FULL name is not a pool class: this `$` cannot
2569                    // be a literal name (pool literal classes — an app's
2570                    // own `View$OnUnhandledKeyEventListener` — keep their
2571                    // `$` here AND at their declaration), so it can only
2572                    // be an external framework nesting boundary
2573                    // (`View$OnClickListener` → `.OnClickListener`).
2574                    || !pool.get(internal).is_some();
2575                // The `$` may only become a nesting dot when the tail
2576                // segment STARTS a Java identifier: R8's desugared-
2577                // library names carry `$` inside PACKAGE paths
2578                // (`j$/util/...` dotted into `j..util`) and suffixes
2579                // like `Collection$-EL` or anonymous `RequestId$1`
2580                // cannot be dotted under any reading.
2581                let tail_ok = rest[i + 1..]
2582                    .chars()
2583                    .next()
2584                    .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
2585                out.push_str(&seg.replace('/', "."));
2586                out.push_str(if known && tail_ok { "." } else { "$" });
2587                off += i + 1;
2588            }
2589            None => {
2590                out.push_str(&rest.replace('/', "."));
2591                return sanitize_ref(&out);
2592            }
2593        }
2594    }
2595}
2596
2597/// The top-level EMISSION UNIT ancestor of a pool class: walking the
2598/// `$`-chain upward (outer_of), the first ancestor that is itself
2599/// emitted as its own compilation unit — a digit-tail rest marks a
2600/// flat boundary (see top_level_classes). Clean members render inline
2601/// in their outer, so only the unit's own name may carry `$`.
2602fn emission_root(pool: &DexPool, internal: &str) -> Option<String> {
2603    let mut cur = internal.to_string();
2604    loop {
2605        let outer = pool.outer_of(&cur)?;
2606        let rest = cur
2607            .strip_prefix(outer)
2608            .and_then(|t| t.strip_prefix('$'))
2609            .unwrap_or("");
2610        if !crate::clean_member_tail(rest) {
2611            return Some(cur);
2612        }
2613        cur = outer.to_string();
2614    }
2615}
2616
2617/// Class-file names may contain characters Java source identifiers
2618/// cannot (`Collection$-EL`); the deterministic mapping matches the
2619/// declaration sites (java_ident).
2620/// Every `.`-segment of a fully-qualified name must start a Java
2621/// identifier: obfuscators emit `package do;` and `..badge.new..` paths.
2622pub(crate) fn sanitize_fq(dotted: &str) -> String {
2623    dotted
2624        .split('.')
2625        .map(|seg| {
2626            if is_java_keyword_name(seg)
2627                || is_restricted_type_name(seg)
2628                || seg.chars().next().is_some_and(|c| c.is_ascii_digit())
2629            {
2630                format!("_{seg}")
2631            } else if seg
2632                .chars()
2633                .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
2634            {
2635                seg.to_string()
2636            } else {
2637                // Non-ASCII single chars (rimet nests classes named `ˆ`
2638                // / `ァ`) map to a lone `_` — reserved since Java 9.
2639                let mapped: String = seg
2640                    .chars()
2641                    .map(|c| {
2642                        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
2643                            c
2644                        } else {
2645                            '_'
2646                        }
2647                    })
2648                    .collect();
2649                if mapped == "_" {
2650                    "__".to_string()
2651                } else {
2652                    mapped
2653                }
2654            }
2655        })
2656        .collect::<Vec<_>>()
2657        .join(".")
2658}
2659
2660/// Restricted contextual TYPE names — legal as member/local names
2661/// (rt.jar compiles `var` locals), illegal in class declarations and
2662/// type references. Consulted only on CLASS-name paths.
2663pub(crate) fn is_restricted_type_name(s: &str) -> bool {
2664    matches!(s, "var" | "yield" | "record" | "sealed" | "permits")
2665}
2666
2667fn is_java_keyword_name(s: &str) -> bool {
2668    matches!(
2669        s,
2670        "abstract"
2671            | "assert"
2672            | "boolean"
2673            | "break"
2674            | "byte"
2675            | "case"
2676            | "catch"
2677            | "char"
2678            | "class"
2679            | "const"
2680            | "continue"
2681            | "default"
2682            | "do"
2683            | "double"
2684            | "else"
2685            | "enum"
2686            | "extends"
2687            | "final"
2688            | "finally"
2689            | "float"
2690            | "for"
2691            | "goto"
2692            | "if"
2693            | "implements"
2694            | "import"
2695            | "instanceof"
2696            | "int"
2697            | "interface"
2698            | "long"
2699            | "native"
2700            | "new"
2701            | "package"
2702            | "private"
2703            | "protected"
2704            | "public"
2705            | "return"
2706            | "short"
2707            | "static"
2708            | "strictfp"
2709            | "super"
2710            | "switch"
2711            | "synchronized"
2712            | "this"
2713            | "throw"
2714            | "throws"
2715            | "transient"
2716            | "try"
2717            | "void"
2718            | "volatile"
2719            | "while"
2720            | "true"
2721            | "false"
2722            | "null"
2723            // `_` is a reserved identifier since Java 9 (Alipay's
2724            // instant-run fields are named `_`) — the `_<name>` mapping
2725            // turns it into `__`, matching jdc-core's call sites.
2726            | "_"
2727    )
2728}
2729
2730fn sanitize_ref(name: &str) -> String {
2731    sanitize_fq(name)
2732}
2733
2734/// Dotted source form of an internal name.
2735pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
2736    print_class_name(pool, internal)
2737}
2738
2739/// Unused import silencer.
2740#[allow(dead_code)]
2741fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
2742    let _ = java_type_to_generic;
2743}