Skip to main content

ddc_dec/
classdec.rs

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