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